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.