From 5b773be2ce1e84b5a008f125537f15d786a0ac8d Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:00:16 -0700 Subject: [PATCH 1/2] Make post topic optional; guard all consumers and add line-clamp fallback --- src/components/PostRow.astro | 6 ++++- src/content/config.ts | 4 +++- src/lib/topics.ts | 9 ++++---- src/pages/authors/[handle]/index.astro | 1 + src/pages/blog/[slug].astro | 31 ++++++++++++++++++++------ src/pages/index.astro | 4 ++-- src/pages/rss.xml.ts | 2 +- src/styles/global.css | 1 + src/write/editor/editor-theme.css | 6 +++++ src/write/meta/MetaForm.tsx | 4 ++-- src/write/serialize/toMdx.test.ts | 11 +++++++++ src/write/serialize/toMdx.ts | 7 ++++-- src/write/serialize/validate.ts | 1 - 13 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/components/PostRow.astro b/src/components/PostRow.astro index b432653..f0f002c 100644 --- a/src/components/PostRow.astro +++ b/src/components/PostRow.astro @@ -18,7 +18,11 @@ const { post, heading: Heading = 'h3', showChip = true, meta = 'byline' } = Astr
{post.data.title} - {showChip && {topicName(post.data.topicId)}} + { + showChip && post.data.topicId && ( + {topicName(post.data.topicId)} + ) + }

{post.data.summary}

diff --git a/src/content/config.ts b/src/content/config.ts index 6a67f0c..2e8083b 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -39,7 +39,9 @@ const posts = defineCollection({ // Display name is derived from topicId via topicName(); kept optional for // backward compat with existing frontmatter but no longer rendered. topic: z.string().optional(), - topicId: z.enum(TOPIC_IDS), + // Optional: a post can belong to a topic hub, or stand alone. Missing topic + // only removes it from /topics/ — it stays in /blog, search, tags, feeds. + topicId: z.enum(TOPIC_IDS).optional(), // A writer-suggested topic not yet in TOPIC_IDS — surfaced for a maintainer // (or automation) to create the topic or remap topicId. Ignored by rendering. proposedTopic: z.string().optional(), diff --git a/src/lib/topics.ts b/src/lib/topics.ts index 41211a4..d214fd2 100644 --- a/src/lib/topics.ts +++ b/src/lib/topics.ts @@ -13,18 +13,19 @@ export const TOPICS: Topic[] = entries const TOPIC_BY_ID = new Map(TOPICS.map((t) => [t.id, t])); -/** Canonical display name for a topic ID; falls back to the ID if unknown. */ -export function topicName(id: string): string { +/** Canonical display name for a topic ID; '' when a post has no topic, or the ID if unknown. */ +export function topicName(id: string | undefined): string { + if (!id) return ''; return TOPIC_BY_ID.get(id)?.name ?? id; } -export function countPostsByTopic( +export function countPostsByTopic( posts: T[], ): Record { const counts: Record = {}; for (const t of TOPICS) counts[t.id] = 0; for (const p of posts) { - if (counts[p.data.topicId] !== undefined) counts[p.data.topicId]++; + if (p.data.topicId && counts[p.data.topicId] !== undefined) counts[p.data.topicId]++; } return counts; } diff --git a/src/pages/authors/[handle]/index.astro b/src/pages/authors/[handle]/index.astro index 20401d8..fa0576c 100644 --- a/src/pages/authors/[handle]/index.astro +++ b/src/pages/authors/[handle]/index.astro @@ -30,6 +30,7 @@ const posts = (await resolvePostAuthors(matching)).sort(sortPostsByDate); const recent = posts.slice(0, RECENT_LIMIT); const topicCounts = posts.reduce>((acc, p) => { + if (!p.data.topicId) return acc; const name = topicName(p.data.topicId); acc[name] = (acc[name] ?? 0) + 1; return acc; diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index d71cc40..aefad8b 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -36,9 +36,17 @@ const authors = await resolveAuthors(post.data.authors); const authorNames = authors.map((a) => a.data.name).join(', '); const all = await getCollection('posts', ({ data }) => !data.draft); +const postTags = new Set(post.data.tags ?? []); const related = await resolvePostAuthors( all - .filter((a) => a.data.topicId === post.data.topicId && a.id !== post.id) + .filter((a) => { + if (a.id === post.id) return false; + // With a topic, relate by topic; without one, fall back to shared tags so + // topicless posts still get a meaningful "related" strip (never each other). + return post.data.topicId + ? a.data.topicId === post.data.topicId + : (a.data.tags ?? []).some((t) => postTags.has(t)); + }) .sort(sortPostsByDate) .slice(0, 6), ); @@ -89,13 +97,22 @@ const breadcrumbJsonLd = { '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: SITE.url }, + ...(post.data.topicId + ? [ + { + '@type': 'ListItem', + position: 2, + name: topicLabel, + item: `${SITE.url}/topics/${post.data.topicId}`, + }, + ] + : []), { '@type': 'ListItem', - position: 2, - name: topicLabel, - item: `${SITE.url}/topics/${post.data.topicId}`, + position: post.data.topicId ? 3 : 2, + name: post.data.title, + item: canonical, }, - { '@type': 'ListItem', position: 3, name: post.data.title, item: canonical }, ], }; --- @@ -117,13 +134,13 @@ const breadcrumbJsonLd = {
← Back to archive