diff --git a/_posts/post template.md b/_posts/post template.md index 13dd880..24dd258 100644 --- a/_posts/post template.md +++ b/_posts/post template.md @@ -4,4 +4,8 @@ date: author: "" category: title: +responseTo: "column/slug" +followUpTo: + - "column/slug" + - "column/slug" --- diff --git a/bun.lock b/bun.lock index c36c7ea..8e94f6c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "slacker-news-astro", "dependencies": { - "@astrojs/mdx": "^5.0.3", + "@astrojs/mdx": "^5.0.4", "@astrojs/rss": "^4.0.18", "astro": "^6.1.9", }, diff --git a/src/content.config.ts b/src/content.config.ts index 2d5efc8..7585623 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -8,7 +8,9 @@ const posts = defineCollection({ date: z.coerce.date(), author: z.string().optional(), category: z.string().optional(), - excerpt: z.string().optional() + excerpt: z.string().optional(), + responseTo: z.union([z.string(), z.array(z.string())]).optional(), + followUpTo: z.union([z.string(), z.array(z.string())]).optional() }) }); diff --git a/src/lib/content.ts b/src/lib/content.ts index 39701fa..2d370fd 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -9,6 +9,12 @@ export type SiteConfig = { description: string; }; +export type PostReference = { + slug: string; + url: string; + title: string; +}; + export type Post = { slug: string; url: string; @@ -23,6 +29,10 @@ export type Post = { alt: string; }; entry: CollectionEntry<"posts">; + responseTo?: string[]; + followUpTo?: string[]; + responses?: PostReference[]; + followUps?: PostReference[]; }; export type ChangelogEntry = { @@ -145,7 +155,7 @@ export async function getSiteConfig(): Promise { export async function getPosts(): Promise { const posts = await getCollection("posts"); - return posts + const processedPosts : Post[] = posts .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()) .map((entry) => { const bodyBlocks = getBodyBlocks(entry.body!); @@ -153,6 +163,18 @@ export async function getPosts(): Promise { const paragraphs = extractTextBlocks(entry.body!); const [category] = entry.id.split("/", 1); + const responseTo = entry.data.responseTo + ? Array.isArray(entry.data.responseTo) + ? entry.data.responseTo + : [entry.data.responseTo] + : undefined; + + const followUpTo = entry.data.followUpTo + ? Array.isArray(entry.data.followUpTo) + ? entry.data.followUpTo + : [entry.data.followUpTo] + : undefined; + return { slug: entry.id, url: `/${entry.id}/`, @@ -163,9 +185,40 @@ export async function getPosts(): Promise { excerpt: toExcerpt({ body: entry.body!, data: entry.data }), paragraphs, leadingImage, - entry + entry, + responseTo, + followUpTo, } satisfies Post; }); + + for (const post of processedPosts) { + if (post.followUpTo) { + post.followUps = processedPosts + .filter((p) => post.followUpTo!.includes(p.slug)) + .map((p) => ({ slug: p.slug, url: p.url, title: p.title })); + } + } + + for (const post of processedPosts) { + for (const otherPost of processedPosts) { + if (otherPost.responseTo?.includes(post.slug)) { + if (!post.responses) post.responses = []; + const exists = post.responses.some((r) => r.slug === otherPost.slug); + if (!exists) { + post.responses.push({ slug: otherPost.slug, url: otherPost.url, title: otherPost.title }); + } + } + if (otherPost.followUpTo?.includes(post.slug)) { + if (!post.followUps) post.followUps = []; + const exists = post.followUps.some((f) => f.slug === otherPost.slug); + if (!exists) { + post.followUps.push({ slug: otherPost.slug, url: otherPost.url, title: otherPost.title }); + } + } + } + } + + return processedPosts; } export async function getFrontpageData(): Promise { diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index e551bbe..989bded 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -4,15 +4,42 @@ import { formatStoryDate, getPosts, getSiteConfig } from "../lib/content"; import { render } from "astro:content"; export async function getStaticPaths() { - return (await getPosts()).map((post) => ({ + const posts = await getPosts(); + return posts.map((post) => ({ params: { slug: post.slug }, - props: { post } + props: { slug: post.slug } })); } const site = await getSiteConfig(); -const { post } = Astro.props; +const allPosts = await getPosts(); +const slugParam = Astro.props?.slug ?? Astro.params.slug; +const slug = Array.isArray(slugParam) + ? slugParam.join("/") + : typeof slugParam === "string" + ? slugParam + : ""; + +if (!slug) { + throw new Error("Missing slug for story page"); +} + +const post = allPosts.find((entry) => entry.slug === slug); +if (!post) { + throw new Error(`No story found for slug "${slug}"`); +} + const { Content } = await render(post.entry); + +const responseLinks = post.responseTo?.map((slug) => { + const target = allPosts.find((p) => p.slug === slug); + return target ? { slug: target.slug, title: target.title, url: target.url } : null; +}).filter(Boolean) ?? []; + +const followUpLinks = post.followUpTo?.map((slug) => { + const target = allPosts.find((p) => p.slug === slug); + return target ? { slug: target.slug, title: target.title, url: target.url } : null; +}).filter(Boolean) ?? []; --- + {responseLinks.length > 0 && ( +
+ This is a response to: + +
+ )} + + {followUpLinks.length > 0 && ( +
+ This is a follow-up to: + +
+ )} + + {post.responses && post.responses.length > 0 && ( +
+ Responses to this piece: + +
+ )} + + {post.followUps && post.followUps.length > 0 && ( +
+ Follow-ups to this piece: + +
+ )} +
@@ -46,3 +117,64 @@ const { Content } = await render(post.entry);
+ +