Developing a Changelog Page with Update History
A changelog is a chronological list of product changes: new features, improvements, bug fixes. A properly formatted changelog retains users and reduces churn—people see the product is evolving.
Entry Structure
Each entry contains: date, version (if software), change type, title, description, sometimes screenshot or GIF.
Change types: New (new feature), Improved (enhancement), Fixed (bug fix), Deprecated (deprecated).
Markdown Files as Data Source
A simple approach—store changelog in Markdown files in the repository:
content/changelog/
├── 2025-03-15.md
├── 2025-02-28.md
└── 2025-01-10.md
---
date: 2025-03-15
version: "2.4.0"
---
## New: Telegram Integration
Now you can receive notifications about new orders directly in Telegram. ...
## Improved: Catalog Loading Speed
Optimized database queries — catalog page loads 40% faster.
## Fixed: Error when paying via Apple Pay in Safari
// lib/changelog.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
export function getChangelogEntries() {
const dir = path.join(process.cwd(), 'content/changelog');
return fs.readdirSync(dir)
.filter(f => f.endsWith('.md'))
.sort().reverse()
.map(filename => {
const raw = fs.readFileSync(path.join(dir, filename), 'utf-8');
const { data, content } = matter(raw);
return { ...data, content, slug: filename.replace('.md', '') };
});
}
Changelog Page
// app/changelog/page.tsx (Next.js App Router)
export default async function ChangelogPage() {
const entries = getChangelogEntries();
return (
<div className="max-w-2xl mx-auto py-12">
<h1 className="text-3xl font-bold mb-10">What's New</h1>
<div className="space-y-12">
{entries.map(entry => (
<article key={entry.slug}>
<div className="flex items-center gap-3 mb-4">
<time className="text-sm text-gray-500">
{new Date(entry.date).toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' })}
</time>
{entry.version && (
<span className="text-xs bg-gray-100 px-2 py-0.5 rounded font-mono">v{entry.version}</span>
)}
</div>
<div className="prose prose-sm" dangerouslySetInnerHTML={{ __html: renderMarkdown(entry.content) }} />
</article>
))}
</div>
</div>
);
}
RSS Feed for Changelog
// ChangelogFeedController
public function rss(): Response
{
$entries = ChangelogEntry::latest('published_at')->take(20)->get();
$xml = view('feeds.changelog-rss', compact('entries'))->render();
return response($xml)->header('Content-Type', 'application/rss+xml');
}
Email Newsletter on Publication
// When entry is published — send to subscribers
public function handle(ChangelogEntryPublished $event): void
{
$subscribers = ChangelogSubscriber::all();
foreach ($subscribers->chunk(100) as $chunk) {
Mail::to($chunk->pluck('email'))->queue(new ChangelogDigestMail($event->entry));
}
}
Timeline
Changelog page with Markdown source, RSS feed, and email subscription: 2 working days.







