From aa4c2d921ffa6088f961078ee8494346ff674695 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 27 Jan 2026 19:43:12 +0000 Subject: [PATCH] feat(blog): add /blog/[slug] post page with dynamic rendering - Create dynamic route for individual blog posts - Use force-dynamic for request-time publish gating - Require Node.js runtime for filesystem reads - Return 404 for drafts, future posts, or invalid slugs - Render Markdown with react-markdown + remark-gfm - No raw HTML rendering (safe by default) - Include consistent typography and styling Closes: MKT-69 --- .../web-roo-code/src/app/blog/[slug]/page.tsx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 apps/web-roo-code/src/app/blog/[slug]/page.tsx diff --git a/apps/web-roo-code/src/app/blog/[slug]/page.tsx b/apps/web-roo-code/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000000..82645a5cc9 --- /dev/null +++ b/apps/web-roo-code/src/app/blog/[slug]/page.tsx @@ -0,0 +1,173 @@ +import type { Metadata } from "next" +import { notFound } from "next/navigation" +import Link from "next/link" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { ArrowLeft } from "lucide-react" +import { SEO } from "@/lib/seo" +import { ogImageUrl } from "@/lib/og" +import { getBlogPostBySlug, formatPostDatePt } from "@/lib/blog" + +// Force dynamic rendering to evaluate publish gating at request-time +export const dynamic = "force-dynamic" + +// Require Node.js runtime for filesystem reads +export const runtime = "nodejs" + +interface BlogPostPageProps { + params: Promise<{ slug: string }> +} + +export async function generateMetadata({ params }: BlogPostPageProps): Promise { + const { slug } = await params + const post = getBlogPostBySlug(slug) + + if (!post) { + return { + title: "Post Not Found", + } + } + + const PATH = `/blog/${post.slug}` + + return { + title: post.title, + description: post.description, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: post.title, + description: post.description, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: ogImageUrl(post.title, post.description), + width: 1200, + height: 630, + alt: post.title, + }, + ], + locale: SEO.locale, + type: "article", + publishedTime: post.publish_date, + }, + twitter: { + card: SEO.twitterCard, + title: post.title, + description: post.description, + images: [ogImageUrl(post.title, post.description)], + }, + keywords: [...SEO.keywords, ...post.tags], + } +} + +export default async function BlogPostPage({ params }: BlogPostPageProps) { + const { slug } = await params + const post = getBlogPostBySlug(slug) + + // Return 404 if post not found or not published + if (!post) { + notFound() + } + + return ( +
+
+ {/* Back link */} + + + Back to Blog + + + {/* Post Header */} +
+

{post.title}

+
+ + {post.tags.length > 0 && ( + <> + +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ + )} +
+
+ + {/* Post Content */} + + + {/* Footer */} +
+ + + Back to Blog + +
+
+
+ ) +}