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 */} +
+ ( +

+ ), + h2: ({ ...props }) =>

, + h3: ({ ...props }) =>

, + a: ({ ...props }) => ( + + ), + blockquote: ({ ...props }) => ( +
+ ), + code: ({ className, children, ...props }) => { + // Check if this is an inline code or code block + const isInline = !className + if (isInline) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) + }, + pre: ({ ...props }) => ( +
+							),
+							ul: ({ ...props }) => 
    , + ol: ({ ...props }) =>
      , + li: ({ ...props }) =>
    1. , + p: ({ ...props }) =>

      , + strong: ({ ...props }) => , + hr: ({ ...props }) =>


      , + }}> + {post.content} + +

+ + {/* Footer */} +
+ + + Back to Blog + +
+
+
+ ) +}