Loading lessons...
Project: Full-Stack Markdown Blog with App Router
Project: Full-Stack Markdown Blog with App Router
In this hands-on project, you will build a full-stack static blog using the Next.js App Router, React Server Components, generateStaticParams, and Incremental Static Regeneration (ISR).
Architecture Overview
app/page.tsx: Server Component listing all blog posts statically.app/posts/[slug]/page.tsx: Dynamic Server Component pre-rendering post content usinggenerateStaticParams().lib/posts.ts: Helper module fetching posts from disk or database.
1. Data Access Layer (lib/posts.ts)
import { cache } from 'react';
export type Post = {
slug: string;
title: string;
content: string;
date: string;
};
const posts: Post[] = [
{
slug: 'hello-nextjs',
title: 'Getting Started with Next.js App Router',
content: 'Next.js App Router changes how we compose full-stack applications...',
date: '2026-09-01',
},
{
slug: 'mastering-rsc',
title: 'Mastering React Server Components',
content: 'React Server Components execute strictly on the server...',
date: '2026-09-05',
},
];
// React.cache memoizes request per render pass
export const getPosts = cache(async (): Promise<Post[]> => {
return posts;
});
export const getPostBySlug = cache(async (slug: string): Promise<Post | undefined> => {
return posts.find((p) => p.slug === slug);
});
2. Blog Post List Page (app/page.tsx)
import Link from 'next/link';
import { getPosts } from '@/lib/posts';
export const revalidate = 3600; // ISR revalidation every 1 hour
export default async function BlogIndexPage() {
const posts = await getPosts();
return (
<main className="max-w-3xl mx-auto p-8">
<h1 className="text-3xl font-extrabold mb-6">Latest Tech Posts</h1>
<div className="space-y-4">
{posts.map((post) => (
<article key={post.slug} className="p-4 border rounded shadow-sm hover:shadow-md">
<Link href={`/posts/${post.slug}`} className="text-xl font-bold text-blue-600 hover:underline">
{post.title}
</Link>
<p className="text-xs text-gray-500 mt-1">Published: {post.date}</p>
</article>
))}
</div>
</main>
);
}
3. Dynamic Post View Page (app/posts/[slug]/page.tsx)
import { notFound } from 'next/navigation';
import { getPosts, getPostBySlug } from '@/lib/posts';
// Pre-generate static paths at build time!
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound(); // Triggers app/not-found.tsx
}
return (
<article className="max-w-2xl mx-auto p-8">
<h1 className="text-4xl font-bold">{post.title}</h1>
<p className="text-sm text-gray-500 mt-2">Date: {post.date}</p>
<div className="mt-6 prose leading-relaxed">{post.content}</div>
</article>
);
}
Summary Table
| Feature | Implementation | Purpose |
|---|---|---|
| Static Path Extraction | generateStaticParams() | Pre-renders dynamic post URLs at build time |
| ISR Cache | export const revalidate = 3600 | Revalidates static pages every hour |
| 404 Guard | notFound() | Renders custom 404 boundary if slug is missing |
TL;DR
- Combine
generateStaticParamsand React Server Components for maximum SSG performance. - Use
notFound()inside async page components to catch missing parameters. - Leverage
export const revalidate = Nto allow automatic background post updates.