import { CallToActionBox } from "@/components/blog/cta"; import { Breadcrumb, formatDate, JsonLd } from "@/components/blog/parts"; import { TableOfContents } from "@/components/blog/toc"; import { authorOf, blogPostingLd, breadcrumbLd, categoryOf, crumbs, } from "@/lib/blog/feeds"; import { getPublishedPost, listPublishedPosts } from "@/lib/blog/loader"; import { parseBody, renderBody } from "@/lib/blog/markdown"; import { BLOG_PATH, categoryPath, postImagePath, postMarkdownPath, postPath, RELATED_POST_COUNT, } from "@/lib/blog/routes"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; export async function generateStaticParams() { return (await listPublishedPosts()).map((p) => ({ slug: p.slug })); } export async function generateMetadata({ params, }: PageProps<"/blog/[slug]">): Promise { const { slug } = await params; const post = await getPublishedPost(slug); if (!post) return {}; const image = post.image ?? postImagePath(post.slug); return { title: post.title, description: post.description, keywords: [post.primaryKeyword, ...post.secondaryKeywords], authors: [ { name: authorOf(post.author).name, url: authorOf(post.author).url }, ], alternates: { canonical: postPath(post.slug), types: { "text/markdown": postMarkdownPath(post.slug) }, }, openGraph: { type: "article", title: post.title, description: post.description, url: postPath(post.slug), publishedTime: post.publishedAt, modifiedTime: post.updatedAt ?? post.publishedAt, images: [{ url: image, width: 1200, height: 630, alt: post.title }], }, twitter: { card: "summary_large_image", title: post.title, description: post.description, images: [image], }, }; } export default async function PostPage({ params }: PageProps<"/blog/[slug]">) { const { slug } = await params; const post = await getPublishedPost(slug); if (!post) notFound(); const category = categoryOf(post.category); const author = authorOf(post.author); const { headings, links } = parseBody(post.body); const html = renderBody(post.body); // Related: same category, the ones this post links to first, then the newest const linked = new Set( links .map((l) => l.href.split("#")[0]) .filter((h) => h.startsWith(`${BLOG_PATH}/`)), ); const siblings = (await listPublishedPosts()).filter( (p) => p.category === post.category && p.slug !== post.slug, ); const related = [ ...siblings.filter((p) => linked.has(postPath(p.slug))), ...siblings.filter((p) => !linked.has(postPath(p.slug))), ].slice(0, RELATED_POST_COUNT); return (

{post.title}

By{" "} {author.name} Published {formatDate(post.publishedAt)} {post.updatedAt && Updated {formatDate(post.updatedAt)}}

{related.length > 0 && category && (
    {related.map((p) => (
  • {p.title} {p.description}
  • ))}

All posts in {category.name}

)}
); }