Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 1bfa919

Browse files
committed
feat(blog): add /blog index page with dynamic rendering
- Create blog index page at /blog - Use force-dynamic for request-time publish gating - Require Node.js runtime for filesystem reads - Display posts with title, description, and date - Add empty state when no posts are published - Include tags display (up to 3) Closes: MKT-68
1 parent fff8e13 commit 1bfa919

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Metadata } from "next"
2+
import Link from "next/link"
3+
import { SEO } from "@/lib/seo"
4+
import { ogImageUrl } from "@/lib/og"
5+
import { getAllBlogPosts, formatPostDatePt } from "@/lib/blog"
6+
7+
// Force dynamic rendering to evaluate publish gating at request-time
8+
export const dynamic = "force-dynamic"
9+
10+
// Require Node.js runtime for filesystem reads
11+
export const runtime = "nodejs"
12+
13+
const TITLE = "Blog"
14+
const DESCRIPTION =
15+
"Insights on AI-powered software development, engineering workflows, and the future of coding with Roo Code."
16+
const PATH = "/blog"
17+
18+
export const metadata: Metadata = {
19+
title: TITLE,
20+
description: DESCRIPTION,
21+
alternates: {
22+
canonical: `${SEO.url}${PATH}`,
23+
},
24+
openGraph: {
25+
title: TITLE,
26+
description: DESCRIPTION,
27+
url: `${SEO.url}${PATH}`,
28+
siteName: SEO.name,
29+
images: [
30+
{
31+
url: ogImageUrl(TITLE, DESCRIPTION),
32+
width: 1200,
33+
height: 630,
34+
alt: TITLE,
35+
},
36+
],
37+
locale: SEO.locale,
38+
type: "website",
39+
},
40+
twitter: {
41+
card: SEO.twitterCard,
42+
title: TITLE,
43+
description: DESCRIPTION,
44+
images: [ogImageUrl(TITLE, DESCRIPTION)],
45+
},
46+
keywords: [...SEO.keywords, "blog", "engineering blog", "AI development"],
47+
}
48+
49+
export default function BlogIndex() {
50+
// Get all published posts (sorted newest first)
51+
const posts = getAllBlogPosts()
52+
53+
return (
54+
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
55+
<div className="mx-auto max-w-4xl">
56+
{/* Page Header */}
57+
<div className="mb-12 text-center">
58+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">Blog</h1>
59+
<p className="mt-4 text-lg text-muted-foreground">{DESCRIPTION}</p>
60+
</div>
61+
62+
{/* Posts List */}
63+
{posts.length === 0 ? (
64+
<EmptyState />
65+
) : (
66+
<div className="space-y-8">
67+
{posts.map((post) => (
68+
<article
69+
key={post.slug}
70+
className="group rounded-lg border border-border bg-card p-6 transition-colors hover:border-primary/50">
71+
<Link href={`/blog/${post.slug}`} className="block">
72+
<h2 className="text-xl font-semibold tracking-tight group-hover:text-primary sm:text-2xl">
73+
{post.title}
74+
</h2>
75+
<p className="mt-2 text-muted-foreground line-clamp-2">{post.description}</p>
76+
<div className="mt-4 flex items-center gap-4 text-sm text-muted-foreground">
77+
<time dateTime={post.publish_date}>
78+
Posted {formatPostDatePt(post.publish_date)}
79+
</time>
80+
{post.tags.length > 0 && (
81+
<>
82+
<span className="text-border"></span>
83+
<div className="flex flex-wrap gap-2">
84+
{post.tags.slice(0, 3).map((tag) => (
85+
<span
86+
key={tag}
87+
className="rounded-full bg-muted px-2 py-0.5 text-xs">
88+
{tag}
89+
</span>
90+
))}
91+
</div>
92+
</>
93+
)}
94+
</div>
95+
</Link>
96+
</article>
97+
))}
98+
</div>
99+
)}
100+
</div>
101+
</div>
102+
)
103+
}
104+
105+
function EmptyState() {
106+
return (
107+
<div className="rounded-lg border border-dashed border-border p-12 text-center">
108+
<h2 className="text-lg font-medium">No posts yet</h2>
109+
<p className="mt-2 text-muted-foreground">Check back soon for new content.</p>
110+
</div>
111+
)
112+
}

0 commit comments

Comments
 (0)