site: topics axis + per-source indexes

Topics — the curated subscription layer: 18-value controlled vocabulary
in src/lib/topics.ts, enum-enforced by the content schema on finds and
reviews so unattended runs can't drift it. /topics/ index +
/topics/<t>/ pages with finds first-class alongside reviews, per-topic
RSS feeds that include finds (unlike category/tag feeds), "By topic"
section on /feeds/, Topics footer link and llms.txt section, dev-shim
coverage for the new feed URLs. daily-finds and build-review skills now
classify at capture/graduation time; "giftable" is a tag, never a
topic.

Per-source indexes: /sources/<slug>/ lists every review and find from a
source (catalog ∪ names in content, so retired sources resolve);
find/review colophons link "(more from this source)"; /sources/ cards
link "everything from this source →"; sub-3-item pages noindexed.

Claude-Session: https://claude.ai/code/session_01WZaczDJjL3xZ3u5spsN5AL
This commit is contained in:
2026-07-12 19:39:11 -04:00
parent be7263b912
commit 9e5fce1eb0
21 changed files with 695 additions and 14 deletions
+3 -1
View File
@@ -1,5 +1,6 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
import { TOPICS } from './lib/topics';
const stripIndex = ({ entry }: { entry: string }) =>
entry.replace(/\/index\.mdx$/, '');
@@ -18,6 +19,7 @@ const reviews = defineCollection({
subtitle: z.string(),
date: z.coerce.date(),
category: z.string(),
topics: z.array(z.enum(TOPICS)).default([]),
tags: z.array(z.string()).default([]),
link: z.string().url().optional(),
linkText: z.string().optional(),
@@ -57,7 +59,7 @@ const find = defineCollection({
linkText: z.string().optional(),
amazonLink: z.string().url().optional(),
source: z.string(),
topics: z.array(z.string()).default([]),
topics: z.array(z.enum(TOPICS)).default([]),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
promotedTo: z.string().optional(),