Skip to main content
Back to Blog
Tutorials
3 min read
November 7, 2024

How to Build a Changelog Page in Next.js

Create a changelog page for your product using Next.js and Markdown. Release entries, version filtering, and RSS feed.

Ryel Banfield

Founder & Lead Developer

A changelog builds trust and keeps users informed. Here is how to create one powered by Markdown files.

Step 1: Content Structure

content/changelog/
  2026-05-01-dark-mode.md
  2026-04-28-api-v2.md
  2026-04-15-performance-updates.md

Step 2: Changelog Entry Format

---
title: "Dark Mode Support"
date: "2026-05-01"
version: "2.4.0"
type: "feature"
---

We have added dark mode support across the entire application.

## What's New
- Automatic dark mode based on system preferences
- Manual toggle in settings
- All components updated for dark mode

## Bug Fixes
- Fixed sidebar not collapsing on mobile
- Fixed chart colors in dark contexts

Step 3: Parse Changelog Entries

// lib/changelog.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";

export interface ChangelogEntry {
  slug: string;
  title: string;
  date: string;
  version: string;
  type: "feature" | "improvement" | "bugfix" | "breaking";
  content: string;
}

export function getChangelog(): ChangelogEntry[] {
  const dir = path.join(process.cwd(), "content/changelog");
  const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));

  return files
    .map((file) => {
      const raw = fs.readFileSync(path.join(dir, file), "utf-8");
      const { data, content } = matter(raw);
      return {
        slug: file.replace(".md", ""),
        title: data.title,
        date: data.date,
        version: data.version,
        type: data.type,
        content,
      };
    })
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

Step 4: Changelog Page

// app/(site)/changelog/page.tsx
import { getChangelog } from "@/lib/changelog";

const TYPE_STYLES = {
  feature: "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
  improvement: "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
  bugfix: "bg-orange-100 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400",
  breaking: "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
};

export default function ChangelogPage() {
  const entries = getChangelog();

  return (
    <div className="mx-auto max-w-3xl px-4 py-16">
      <h1 className="text-3xl font-bold">Changelog</h1>
      <p className="mt-2 text-gray-500">
        All notable changes to our platform.
      </p>

      <div className="mt-12 space-y-12">
        {entries.map((entry) => (
          <article key={entry.slug} className="relative pl-8">
            {/* Timeline dot */}
            <div className="absolute left-0 top-1.5 h-3 w-3 rounded-full border-2 border-blue-600 bg-white dark:bg-gray-950" />
            {/* Timeline line */}
            <div className="absolute left-1.5 top-4 -bottom-12 w-px bg-gray-200 dark:bg-gray-800" />

            <div className="flex flex-wrap items-center gap-2">
              <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLES[entry.type]}`}>
                {entry.type}
              </span>
              <span className="text-sm font-medium text-gray-900 dark:text-white">
                v{entry.version}
              </span>
              <span className="text-sm text-gray-500">
                {new Date(entry.date).toLocaleDateString("en-US", {
                  month: "long",
                  day: "numeric",
                  year: "numeric",
                })}
              </span>
            </div>

            <h2 className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
              {entry.title}
            </h2>

            <div className="prose prose-sm prose-gray mt-3 max-w-none dark:prose-invert">
              {/* Render markdown content */}
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

Step 5: Filter by Type

"use client";

import { useState } from "react";

const TYPES = ["all", "feature", "improvement", "bugfix", "breaking"];

export function ChangelogFilter({
  entries,
}: {
  entries: ChangelogEntry[];
}) {
  const [filter, setFilter] = useState("all");

  const filtered =
    filter === "all"
      ? entries
      : entries.filter((e) => e.type === filter);

  return (
    <div>
      <div className="mb-8 flex gap-2">
        {TYPES.map((type) => (
          <button
            key={type}
            onClick={() => setFilter(type)}
            className={`rounded-full px-3 py-1 text-sm capitalize transition ${
              filter === type
                ? "bg-blue-600 text-white"
                : "bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400"
            }`}
          >
            {type}
          </button>
        ))}
      </div>

      <div className="space-y-12">
        {filtered.map((entry) => (
          <ChangelogEntry key={entry.slug} entry={entry} />
        ))}
      </div>
    </div>
  );
}

Step 6: RSS Feed for Changelog

// app/changelog/feed.xml/route.ts
import { getChangelog } from "@/lib/changelog";

export async function GET() {
  const entries = getChangelog();
  const baseUrl = "https://rcbsoftware.com";

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>RCB Software Changelog</title>
    <link>${baseUrl}/changelog</link>
    <description>Latest updates and changes</description>
    ${entries
      .slice(0, 20)
      .map(
        (entry) => `
    <item>
      <title>${entry.title} (v${entry.version})</title>
      <link>${baseUrl}/changelog#${entry.slug}</link>
      <pubDate>${new Date(entry.date).toUTCString()}</pubDate>
      <description>${entry.title} - ${entry.type}</description>
    </item>`
      )
      .join("")}
  </channel>
</rss>`;

  return new Response(xml, {
    headers: { "Content-Type": "application/xml" },
  });
}

Need a Product Website?

We build SaaS websites with changelogs, documentation, and product marketing pages. Contact us to discuss your project.

changelogrelease notesNext.jscontenttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles