diff --git a/astro.config.mjs b/astro.config.mjs
index a1de583..995d878 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -12,7 +12,7 @@ const EXCLUDED_PATHS = new Set([
const FIND_ROOT = path.resolve('src/content/find');
const REVIEW_ROOT = path.resolve('src/content/reviews');
-const HOME_PAGE = path.resolve('src/pages/index.astro');
+const PICK_FILE = path.resolve('src/data/pick.json');
const VALID_SLUG = /^[a-z0-9][a-z0-9-]*$/;
const VALID_TAG = /^[a-z0-9][a-z0-9-]{0,40}$/i;
@@ -118,11 +118,7 @@ const setPickApi = {
if (!reviewPath.startsWith(REVIEW_ROOT + path.sep)) return send(res, 400, { error: 'path escape' });
try { await stat(reviewPath); } catch { return send(res, 404, { error: 'review not found' }); }
- const text = await readFile(HOME_PAGE, 'utf8');
- const re = /^(const HERO_SLUG = ')([^']+)(';)/m;
- if (!re.test(text)) return send(res, 422, { error: 'HERO_SLUG line not found in src/pages/index.astro' });
- const out = text.replace(re, (_, a, _b, c) => `${a}${slug}${c}`);
- await writeFile(HOME_PAGE, out, 'utf8');
+ await writeFile(PICK_FILE, JSON.stringify({ hero: slug }, null, 2) + '\n', 'utf8');
return send(res, 200, { slug });
} catch (err) {
diff --git a/src/content.config.ts b/src/content.config.ts
index 269cfcd..e246d4b 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -21,6 +21,7 @@ const reviews = defineCollection({
tags: z.array(z.string()).default([]),
link: z.string().url().optional(),
linkText: z.string().optional(),
+ amazonLink: z.string().url().optional(),
description: z.string().optional(),
source: z.string().optional(),
fromFind: z.string().optional(),
@@ -54,11 +55,13 @@ const find = defineCollection({
date: z.coerce.date(),
link: z.string().url(),
linkText: z.string().optional(),
+ amazonLink: z.string().url().optional(),
source: z.string(),
topics: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
promotedTo: z.string().optional(),
+ shortlist: z.boolean().default(false),
editorialTags: z.array(z.string()).default([]),
}),
});
diff --git a/src/data/pick.json b/src/data/pick.json
new file mode 100644
index 0000000..120f152
--- /dev/null
+++ b/src/data/pick.json
@@ -0,0 +1,3 @@
+{
+ "hero": "continue-y-n-game"
+}
diff --git a/src/lib/find-hero.ts b/src/lib/find-hero.ts
index 87e1abb..4fca2a8 100644
--- a/src/lib/find-hero.ts
+++ b/src/lib/find-hero.ts
@@ -1,7 +1,7 @@
import type { ImageMetadata } from 'astro';
const modules = import.meta.glob<{ default: ImageMetadata }>(
- '../content/find/**/hero.{png,jpg,jpeg,gif,webp}',
+ '../content/find/**/hero.{png,jpg,jpeg,gif,webp,avif}',
{ eager: true }
);
diff --git a/src/lib/hero.ts b/src/lib/hero.ts
index eb19b6c..56eaaf5 100644
--- a/src/lib/hero.ts
+++ b/src/lib/hero.ts
@@ -1,34 +1,17 @@
import type { ImageMetadata } from 'astro';
-import alcove from '../content/reviews/alcove/hero.png';
-import altTab from '../content/reviews/alt-tab/hero.jpg';
-import apex from '../content/reviews/apex-markdown-processor/hero.png';
+// Legacy hero assets that predate the hero.
+ + Also on Amazon ↗ + + {amazon.isAffiliate && Affiliate} +
+ )} + + {(external.isAffiliate || amazon?.isAffiliate) && ({AFFILIATE_DISCLOSURE}
)} diff --git a/src/pages/index.astro b/src/pages/index.astro index 254be49..18651ef 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -7,7 +7,10 @@ import { heroes } from '../lib/hero'; import { bundleMosaic } from '../lib/bundle-mosaic'; import { buildOgImage, buildItemList, WEBSITE, ORGANIZATION } from '../lib/seo'; -const HERO_SLUG = 'continue-y-n-game'; +import pick from '../data/pick.json'; + +// Pick of the day. Set in src/data/pick.json; null falls back to the latest review. +const HERO_SLUG: string | null = pick.hero; const INITIAL_GRID = 24; const BATCH_SIZE = 7; @@ -69,7 +72,9 @@ const tiles: Tile[] = [...reviewTiles, ...bundleTiles].sort( const heroIndex = tiles.findIndex( (t) => t.kind === 'review' && t.href === `/reviews/${HERO_SLUG}/` ); -const hero = heroIndex >= 0 ? tiles[heroIndex] : reviewTiles[0]; +// Fall back to the newest review (tiles are sorted by date desc). +const hero = + heroIndex >= 0 ? tiles[heroIndex] : tiles.find((t) => t.kind === 'review'); const rest = tiles.filter((t) => t !== hero); const lastUpdated = tiles[0]?.date; diff --git a/src/pages/reviews/[...slug].astro b/src/pages/reviews/[...slug].astro index 9950e1a..f0f0d77 100644 --- a/src/pages/reviews/[...slug].astro +++ b/src/pages/reviews/[...slug].astro @@ -39,6 +39,9 @@ const linkLabel = (review.data.linkText ?? new URL(review.data.link).hostname.replace(/^www\./, '')); const external = review.data.link ? resolveExternalLink(review.data.link) : undefined; +const amazon = review.data.amazonLink + ? resolveExternalLink(review.data.amazonLink) + : undefined; const fromFindItem = review.data.fromFind ? (await getCollection('find')).find((f) => f.id === review.data.fromFind) @@ -111,7 +114,20 @@ const categoryLabel = )} - {external?.isAffiliate && ( + {amazon && ( ++ + Also on Amazon ↗ + + {amazon.isAffiliate && Affiliate} +
+ )} + + {(external?.isAffiliate || amazon?.isAffiliate) && ({AFFILIATE_DISCLOSURE}
)} @@ -121,7 +137,7 @@ const categoryLabel = label="Make today's pick" action="set-pick" slug={review.id} - confirm={`This will set ${review.data.name} as the homepage hero (rewrites HERO_SLUG in src/pages/index.astro).`} + confirm={`This will set ${review.data.name} as the homepage hero (writes src/data/pick.json).`} /> )}