import type { Metadata } from "next"; import fs from "fs"; import path from "path"; export const metadata: Metadata = { title: "Changelog", description: "cmux release notes and version history. New features, bug fixes, and changes for the native macOS terminal.", }; interface ChangelogSection { heading: string; items: string[]; } interface ChangelogVersion { version: string; date: string; intro?: string; sections: ChangelogSection[]; } function parseChangelog(markdown: string): ChangelogVersion[] { const versions: ChangelogVersion[] = []; let current: ChangelogVersion | null = null; let currentSection: ChangelogSection | null = null; for (const line of markdown.split("\n")) { const versionMatch = line.match(/^## \[(.+?)\] - (.+)$/); if (versionMatch) { if (current) versions.push(current); current = { version: versionMatch[1], date: versionMatch[2], sections: [], }; currentSection = null; continue; } if (!current) continue; const sectionMatch = line.match(/^### (.+)$/); if (sectionMatch) { currentSection = { heading: sectionMatch[1], items: [] }; current.sections.push(currentSection); continue; } const itemMatch = line.match(/^- (.+)$/); if (itemMatch) { if (currentSection) { currentSection.items.push(itemMatch[1]); } else { // Items without a ### heading (e.g. 1.0.x initial release) if (!current.sections.length) { currentSection = { heading: "", items: [] }; current.sections.push(currentSection); } current.sections[current.sections.length - 1].items.push( itemMatch[1] ); } continue; } // Non-empty lines that aren't headings or items (intro text) const trimmed = line.trim(); if (trimmed && !trimmed.startsWith("#")) { current.intro = trimmed; } } if (current) versions.push(current); return versions; } function InlineMarkdown({ text }: { text: string }) { const parts = text.split(/(`[^`]+`|\[[^\]]+\]\([^)]+\))/g); return ( <> {parts.map((part, i) => { if (part.startsWith("`") && part.endsWith("`")) { return {part.slice(1, -1)}; } const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); if (linkMatch) { return ( {linkMatch[1]} ); } return {part}; })} ); } export default function ChangelogPage() { const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md"); const markdown = fs.readFileSync(changelogPath, "utf-8"); const versions = parseChangelog(markdown); return ( <>

Changelog

All notable changes to cmux are documented here.

{versions.map((v) => (

{v.version}{" "} — {v.date}

{v.intro &&

{v.intro}

} {v.sections.map((section, i) => (
{section.heading &&

{section.heading}

}
))}
))} ); }