Stop 11 · Next.jsProjects · 2/4

Blog

Part of the Frontend Developer Roadmap

In short

A blog is the classic Next.js project. You'll use dynamic routes, static generation, ISR and metadata together, which is exactly what content sites at real companies do.

A blog built on Next.js with SSG/ISR, dynamic routes for posts, and metadata generation — a very common real-world Next.js use case.

The jaadu holds an open diary as new blank pages float in and slot themselves inside.

Think of it like this

A diary that updates itself

Imagine a printed diary where new pages appear on their own every few hours, and each page has a neat title on top. Your blog is that: posts are printed in advance, refreshed in the background, each with its own label.

Watch it happen

From .md file to blog page

.md files
gray-matter
Post list
Slugs
HTML pages

1/5content/posts holds three files, each with a title and date at the top.

The big ideas

What you need to know

Posts from Markdown or a CMS

Read .md or .mdx files from a folder, or fetch from a headless CMS like Sanity or Contentful.

SSG plus ISR

Prebuild posts with generateStaticParams, and use revalidate if content comes from a CMS.

SEO basics

generateMetadata per post, a sitemap of all posts, and readable slugs.

List and detail

A /blog index with pagination or tags, and /blog/[slug] for each post.

How it works

Build plan

  1. 1

    Content source

    Write three Markdown posts with a title, date and slug at the top.

  2. 2

    Blog index

    List posts newest first with title, date and a short excerpt.

  3. 3

    Post page

    A [slug] route with generateStaticParams and notFound() for bad slugs.

  4. 4

    SEO layer

    Add generateMetadata, a sitemap and Open Graph images.

See it in code

Walk through the code

ts
1// lib/posts.ts2import fs from "node:fs/promises";3import path from "node:path";4import matter from "gray-matter";5 6const DIR = path.join(process.cwd(), "content/posts");7 8export async function getAllPosts() {9  const files = await fs.readdir(DIR);10  const posts = await Promise.all(files.map(async (file) => {11    const { data } = matter(await fs.readFile(path.join(DIR, file), "utf8"));12    return { slug: file.replace(/\.mdx?$/, ""), title: data.title, date: data.date };13  }));14  return posts.sort((a, b) => +new Date(b.date) - +new Date(a.date));15}

1/4Node's file system and path modules, plus gray-matter to read front matter.

gray-matter reads the title and date from the top of each file. Since this runs in Server Components, using the file system is fine.

Save yourself the pain

Common mistakes

Oops

Importing fs in a Client Component.

Do this instead

File system code only works on the server. Keep it in Server Components or lib files they call.

Oops

Using titles with spaces and capitals as URLs.

Do this instead

Turn titles into clean slugs like my-first-internship, and never change them after publishing.

Your mission

Milestone 1: three posts live

~45 min

Quick check

Your posts come from a CMS that editors update a few times a day. What's a good choice?

Go deeper

  1. 1Use `generateStaticParams` to pre-render all post pages at build time.
  2. 2Add ISR so new posts (from a CMS or markdown files) appear without a full redeploy.
  3. 3Generate per-post metadata dynamically from the post's title and excerpt.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge