page.tsx115 lines · main
1import { DocsShell } from '../../components/shell';
2import { CHANGELOG_ENTRIES, type ChangelogEntry, type ChangelogTag } from './entries';
3
4export const metadata = {
5 title: 'changelog',
6};
7
8type Entry = ChangelogEntry;
9type Tag = ChangelogTag;
10
11
12const TAG_STYLE: Record<Tag, string> = {
13 feat: 'bg-[var(--color-primary)] text-[var(--color-text-inverse)]',
14 fix: 'border border-[var(--color-border)] text-[var(--color-text-muted)]',
15 security: 'bg-[var(--color-text-error)] text-[var(--color-text-inverse)]',
16 docs: 'border border-[var(--color-border)] text-[var(--color-text-muted)]',
17 infra: 'border border-[var(--color-border)] text-[var(--color-text-muted)]',
18 chore: 'border border-[var(--color-border)] text-[var(--color-text-subtle)]',
19};
20
21export default function ChangelogPage() {
22 const grouped = groupByMonth(CHANGELOG_ENTRIES);
23 return (
24 <DocsShell>
25 <div className="flex items-start justify-between gap-4">
26 <h1 className="font-mono text-2xl tracking-tight">changelog</h1>
27 <a
28 href="/changelog/feed.xml"
29 className="rounded-md border border-[var(--color-border)] px-3 py-1 font-mono text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
30 >
31 rss feed
32 </a>
33 </div>
34 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
35 what landed, in reverse chronological order. one entry per shipped change worth
36 knowing about — security, features, infrastructure, fixes.
37 </p>
38
39 <div className="mt-6 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
40 briven ran dogfood-first through the first half of 2026 and graduates to{' '}
41 <strong>public beta on 2026-05-21</strong>. everything prior is internal validation on
42 j&apos;s own products; everything after is open to anyone with a credit card.
43 </div>
44
45 <div className="mt-12 flex flex-col gap-12">
46 {grouped.map(({ month, entries }) => (
47 <section key={month}>
48 <h2 className="font-mono text-xl tracking-tight">{month}</h2>
49 <ul className="mt-6 flex flex-col gap-6">
50 {entries.map((entry) => (
51 <li key={`${entry.date}-${entry.title}`} className="flex flex-col gap-2">
52 <div className="flex items-center gap-3">
53 <time className="font-mono text-xs text-[var(--color-text-subtle)]">
54 {entry.date}
55 </time>
56 <div className="flex gap-1">
57 {entry.tags.map((tag) => (
58 <span
59 key={tag}
60 className={`rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wide ${TAG_STYLE[tag]}`}
61 >
62 {tag}
63 </span>
64 ))}
65 </div>
66 </div>
67 <p className="font-mono text-sm">{entry.title}</p>
68 <p className="font-mono text-sm text-[var(--color-text-muted)]">{entry.body}</p>
69 </li>
70 ))}
71 </ul>
72 </section>
73 ))}
74 </div>
75 </DocsShell>
76 );
77}
78
79function groupByMonth(entries: readonly Entry[]): { month: string; entries: Entry[] }[] {
80 const buckets = new Map<string, Entry[]>();
81 for (const entry of entries) {
82 const month = entry.date.slice(0, 7); // yyyy-mm
83 const list = buckets.get(month) ?? [];
84 list.push(entry);
85 buckets.set(month, list);
86 }
87 // Sort entries within each bucket newest-first, then sort buckets newest-first.
88 for (const list of buckets.values()) {
89 list.sort((a, b) => b.date.localeCompare(a.date));
90 }
91 return [...buckets.entries()]
92 .sort(([a], [b]) => b.localeCompare(a))
93 .map(([month, list]) => ({ month: formatMonth(month), entries: list }));
94}
95
96function formatMonth(yyyymm: string): string {
97 const [yearStr, monthStr] = yyyymm.split('-');
98 const year = Number(yearStr);
99 const monthIdx = Number(monthStr) - 1;
100 const monthNames = [
101 'january',
102 'february',
103 'march',
104 'april',
105 'may',
106 'june',
107 'july',
108 'august',
109 'september',
110 'october',
111 'november',
112 'december',
113 ];
114 return `${monthNames[monthIdx] ?? yyyymm} ${year}`;
115}