Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions app/api/rss/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { getAllEntries } from "@/lib/changelog";
import { NextResponse } from "next/server";

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
const FEED_TITLE = "ModelTrace Changelog";
const FEED_DESCRIPTION =
"Progress notes, architecture decisions, and postmortems for ModelTrace.";

function escapeXml(str: string): string {
return str
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}

function toRfc822(isoDate: string): string {
return new Date(isoDate).toUTCString();
}

export async function GET() {
const entries = getAllEntries();

const items = entries
.map((entry) => {
const link = `${SITE_URL}/changelog/${entry.slug}`;
const categories = [
...(entry.milestone ? [`<category>${escapeXml(entry.milestone)}</category>`] : []),
...entry.tags.map((t) => `<category>${escapeXml(t)}</category>`),
].join("\n ");

return `
<item>
<title>${escapeXml(entry.title)}</title>
<link>${escapeXml(link)}</link>
<guid isPermaLink="true">${escapeXml(link)}</guid>
<description>${escapeXml(entry.summary)}</description>
<pubDate>${toRfc822(entry.date)}</pubDate>
${categories}
</item>`;
})
.join("");

const lastBuild =
entries.length > 0 ? toRfc822(entries[0].date) : new Date().toUTCString();

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(FEED_TITLE)}</title>
<link>${escapeXml(SITE_URL)}/changelog</link>
<description>${escapeXml(FEED_DESCRIPTION)}</description>
<language>en-us</language>
<lastBuildDate>${lastBuild}</lastBuildDate>
<atom:link href="${escapeXml(SITE_URL)}/api/rss" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>`;

return new NextResponse(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
}
110 changes: 110 additions & 0 deletions app/changelog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getAllEntries, getEntryBySlug, formatDate } from "@/lib/changelog";
import type { EntryType } from "@/lib/changelog";
import { notFound } from "next/navigation";

const TYPE_LABELS: Record<EntryType, string> = {
changelog: "Changelog",
post: "Post",
postmortem: "Postmortem",
};

interface Props {
params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
const entries = getAllEntries();
return entries.map((e) => ({ slug: e.slug }));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const entry = await getEntryBySlug(slug);
if (!entry) return {};
return {
title: entry.title,
description: entry.summary,
openGraph: {
title: entry.title,
description: entry.summary,
type: "article",
publishedTime: entry.date,
},
};
}

export default async function ChangelogEntryPage({ params }: Props) {
const { slug } = await params;
const entry = await getEntryBySlug(slug);
if (!entry) notFound();

return (
<article className="cl-post">
{/* ── Back navigation ─────────────────────────────────────────────── */}
<Link href="/changelog" className="cl-back-link">
← Changelog
</Link>

{/* ── Post header ─────────────────────────────────────────────────── */}
<header className="cl-post-header">
<div className="cl-post-meta">
<span className="cl-type-badge" data-type={entry.type}>
{TYPE_LABELS[entry.type]}
</span>
{entry.milestone && (
<Link
href={`/changelog?milestone=${encodeURIComponent(entry.milestone)}`}
className="cl-milestone-chip"
>
{entry.milestone}
</Link>
)}
<time dateTime={entry.date} className="cl-date">
{formatDate(entry.date)}
</time>
</div>

<h1 className="cl-post-title">{entry.title}</h1>
<p className="cl-post-summary">{entry.summary}</p>

{entry.tags.length > 0 && (
<ul className="cl-tag-list" aria-label="Tags">
{entry.tags.map((t) => (
<li key={t} className="cl-tag">
{t}
</li>
))}
</ul>
)}
</header>

{/* ── Post body ───────────────────────────────────────────────────── */}
<div
className="cl-post-body prose"
dangerouslySetInnerHTML={{ __html: entry.body }}
/>

{/* ── Footer navigation ───────────────────────────────────────────── */}
<footer className="cl-post-footer">
<Link href="/changelog" className="cl-back-link">
← Back to Changelog
</Link>
<a href="/api/rss" className="cl-rss-link">
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden
>
<circle cx="6.18" cy="17.82" r="2.18" />
<path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83C19.56 11.58 12.86 4.44 4 4.44zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z" />
</svg>
Subscribe via RSS
</a>
</footer>
</article>
);
}
137 changes: 137 additions & 0 deletions app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getAllEntries, getMilestones, formatDate } from "@/lib/changelog";
import type { EntryType } from "@/lib/changelog";

export const metadata: Metadata = {
title: "Changelog",
description:
"Progress notes, architecture decisions, and postmortems for ModelTrace — tagged to grant milestones so reports assemble themselves.",
};

// Type badge colours match globals.css .cl-type-badge[data-type]
const TYPE_LABELS: Record<EntryType, string> = {
changelog: "Changelog",
post: "Post",
postmortem: "Postmortem",
};

interface Props {
searchParams: Promise<{ milestone?: string }>;
}

export default async function ChangelogIndexPage({ searchParams }: Props) {
const { milestone: activeMilestone } = await searchParams;
const allEntries = getAllEntries();
const milestones = getMilestones();

const entries = activeMilestone
? allEntries.filter((e) => e.milestone === activeMilestone)
: allEntries;

return (
<>
{/* ── Hero ────────────────────────────────────────────────────────── */}
<section className="cl-index-hero">
<div className="landing-orbs" aria-hidden />
<div className="cl-index-hero-inner">
<span className="tag">Changelog</span>
<h1 className="hero-headline">Progress, decisions, postmortems.</h1>
<p className="landing-lead">
Every entry is tagged to a grant milestone so a report can be
assembled from work written as it happened, not reconstructed after
the fact.
</p>
<p className="cl-rss-hint">
<a href="/api/rss" className="cl-rss-link">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden
>
<circle cx="6.18" cy="17.82" r="2.18" />
<path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83C19.56 11.58 12.86 4.44 4 4.44zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z" />
</svg>
RSS feed
</a>
<span className="cl-rss-sep">·</span>
<span>{allEntries.length} entries</span>
</p>
</div>
</section>

{/* ── Milestone filter bar ─────────────────────────────────────────── */}
{milestones.length > 0 && (
<nav className="cl-filter-bar" aria-label="Filter by milestone">
<Link
href="/changelog"
className={`cl-filter-pill${!activeMilestone ? " cl-filter-pill--active" : ""}`}
>
All
</Link>
{milestones.map((m) => (
<Link
key={m}
href={`/changelog?milestone=${encodeURIComponent(m)}`}
className={`cl-filter-pill${activeMilestone === m ? " cl-filter-pill--active" : ""}`}
>
{m}
</Link>
))}
</nav>
)}

{/* ── Entry list ──────────────────────────────────────────────────── */}
<section className="cl-entry-list">
{entries.length === 0 && (
<p className="cl-empty">No entries for this milestone yet.</p>
)}
{entries.map((entry) => (
<article key={entry.slug} className="cl-card">
<div className="cl-card-meta">
<span
className="cl-type-badge"
data-type={entry.type}
>
{TYPE_LABELS[entry.type]}
</span>
{entry.milestone && (
<Link
href={`/changelog?milestone=${encodeURIComponent(entry.milestone)}`}
className="cl-milestone-chip"
>
{entry.milestone}
</Link>
)}
<time dateTime={entry.date} className="cl-date">
{formatDate(entry.date)}
</time>
</div>

<h2 className="cl-card-title">
<Link href={`/changelog/${entry.slug}`}>{entry.title}</Link>
</h2>

<p className="cl-card-summary">{entry.summary}</p>

{entry.tags.length > 0 && (
<ul className="cl-tag-list" aria-label="Tags">
{entry.tags.map((t) => (
<li key={t} className="cl-tag">
{t}
</li>
))}
</ul>
)}

<Link href={`/changelog/${entry.slug}`} className="cl-read-more">
Read →
</Link>
</article>
))}
</section>
</>
);
}
7 changes: 7 additions & 0 deletions app/compliance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export const metadata: Metadata = {

export default function CompliancePage() {
return (
<section className="section">
<span className="tag">Compliance</span>
<h1>Compliance surface — product definition TBD.</h1>
<p style={{ color: "var(--muted)" }}>
Scaffold page — replace with production content, data loaders, and analytics.
</p>
</section>
<>
<section className="landing-hero" style={{ paddingBottom: "24px" }}>
<div className="landing-hero-inner" style={{ maxWidth: "800px" }}>
Expand Down
4 changes: 4 additions & 0 deletions app/contracts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export default function ContractsPage() {
return (
<section className="section">
<span className="tag">Contracts</span>
<h1>Contracts surface — product definition TBD.</h1>
<p style={{ color: "var(--muted)" }}>
Scaffold page — replace with production content, data loaders, and analytics.
<h2>Soroban contracts</h2>
<h1>Soroban contracts</h1>
<p style={{ color: "var(--muted)", maxWidth: 640, marginTop: 0 }}>
On-chain modules that power ModelTrace settlement, attestation, and
Expand Down
7 changes: 7 additions & 0 deletions app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export const metadata: Metadata = {

export default function DocsPage() {
return (
<section className="section">
<span className="tag">Documentation</span>
<h1>Technical specs, governance, and integration guides.</h1>
<p style={{ color: "var(--muted)" }}>
Scaffold page — replace with production content, data loaders, and analytics.
</p>
</section>
<>
<DocsSidebar />
<article className="docs-content">
Expand Down
Loading
Loading