Lesson 7 +10 XP

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 using generateStaticParams().
  • 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

FeatureImplementationPurpose
Static Path ExtractiongenerateStaticParams()Pre-renders dynamic post URLs at build time
ISR Cacheexport const revalidate = 3600Revalidates static pages every hour
404 GuardnotFound()Renders custom 404 boundary if slug is missing

TL;DR

  • Combine generateStaticParams and React Server Components for maximum SSG performance.
  • Use notFound() inside async page components to catch missing parameters.
  • Leverage export const revalidate = N to allow automatic background post updates.