Restructure navigation around What's up / Opine and the reader's utilities
validate / validate (push) Successful in 22s

- Stories gain a required type (report | essay); all 28 classified per
  the boundary rule now stated in EDITORIAL.md
- Nav: What's up · Opine · Podcast · Book · Guides · Forum, each gated
  on real content (Forum waits on a forumUrl); logo is Home
- /whats-up (reports, newest first) and /opine (essays, editorial order)
  share a StoryArchive component with subject filters
- Static /tags/<tag> pages for subjects carried by 2+ stories; tags stay
  overlapping, never exclusive departments
- /corrections: generated from revision history (commits beginning
  'Correction:'), so the page cannot under-report
- About, RSS, Corrections, Weekly briefing move to the footer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Compleet
2026-07-14 14:37:44 +01:00
parent e8403dbfb4
commit 0e7871f395
40 changed files with 388 additions and 67 deletions
+101
View File
@@ -0,0 +1,101 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { correctionCommits } from '../lib/git';
import { site } from '../site.config';
const corrections = correctionCommits();
---
<BaseLayout title="Corrections" description="Substantive corrections to published work, drawn from the publication's own revision history.">
<section class="shell corrections">
<p class="eyebrow">The record</p>
<h1>Corrections</h1>
<div class="prose">
<p>
When published work turns out to be wrong in a way that matters, the fix is recorded here — drawn
automatically from the publication's revision history, not maintained by hand. A correction is a
commit whose message begins with <code>Correction:</code>; this page cannot show fewer corrections
than the repository contains.
</p>
<p>
Typo fixes and routine maintenance don't qualify. Changed claims, wrong facts, and misattributions do.
The <a href={`${site.repositoryUrl}/src/branch/main/EDITORIAL.md`}>editorial commitments</a> say what
readers may hold us to; the <a href={`${site.repositoryUrl}/commits/branch/main`}>full revision
history</a> shows everything else.
</p>
</div>
{corrections.length > 0 ? (
<ol>
{corrections.map((commit) => (
<li>
<time datetime={commit.date}>{commit.date}</time>
<span>{commit.subject.replace(/^Correction:\s*/, '')}</span>
<a href={`${site.repositoryUrl}/commit/${commit.hash}`}><code>{commit.hash}</code></a>
</li>
))}
</ol>
) : (
<p class="none-yet">
No substantive corrections have been required yet. That sentence should get harder to keep
writing as the archive grows — if it doesn't, be suspicious.
</p>
)}
</section>
</BaseLayout>
<style>
.corrections {
padding-top: clamp(3rem, 7vw, 6rem);
}
h1 {
max-width: 18ch;
margin: 0 0 2rem;
font-family: 'Newsreader Variable', Georgia, serif;
font-size: clamp(3rem, 7vw, 5.5rem);
font-weight: 500;
letter-spacing: -0.045em;
line-height: 0.92;
}
.prose p {
font-size: 1.05rem;
}
ol {
width: min(100%, var(--reading));
margin: 3rem 0 0;
padding: 0;
list-style: none;
border-top: 1px solid var(--ink);
}
li {
display: grid;
grid-template-columns: 8rem 1fr auto;
gap: 1rem;
padding: 0.9rem 0;
border-bottom: 1px solid var(--rule);
font-size: 0.9rem;
}
time,
code {
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
}
.none-yet {
width: min(100%, var(--reading));
margin-top: 3rem;
padding: 1.5rem;
border: 1px dashed var(--rule);
color: var(--muted);
}
@media (max-width: 600px) {
li { grid-template-columns: 1fr auto; }
li span { grid-column: 1 / -1; grid-row: 1; }
}
</style>
+17
View File
@@ -0,0 +1,17 @@
---
import { getCollection } from 'astro:content';
import StoryArchive from '../components/StoryArchive.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
const essays = (await getCollection('stories', ({ data }) => !data.draft && data.type === 'essay'))
.sort((a, b) => a.data.editorialOrder - b.data.editorialOrder);
---
<BaseLayout title="Opine" description="What it means: essays on sovereignty, reputation, money, community, and trust itself.">
<StoryArchive
title="Opine"
count={`${essays.length} essays — what it means`}
entries={essays}
emptyMessage="The first essay is being prepared."
/>
</BaseLayout>
+8 -57
View File
@@ -1,67 +1,18 @@
---
import { getCollection } from 'astro:content';
import StoryCard from '../../components/StoryCard.astro';
import StoryArchive from '../../components/StoryArchive.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
const stories = (await getCollection('stories', ({ data }) => !data.draft))
.sort((a, b) => a.data.editorialOrder - b.data.editorialOrder);
const reportCount = stories.filter(({ data }) => data.type === 'report').length;
---
<BaseLayout title="Stories" description="Reported and argued stories about trust, identity, and authority.">
<section class="shell archive-head">
<h1>Stories</h1>
<p>{stories.length} reports, histories and essays <span>First collection</span></p>
</section>
<section class="shell archive-list">
{stories.length > 0 ? stories.map((entry, index) => <StoryCard entry={entry} index={index} />) : <p class="empty-state">The first story is being prepared.</p>}
</section>
<StoryArchive
title="Stories"
count={`${stories.length} stories — ${reportCount} reports, ${stories.length - reportCount} essays`}
entries={stories}
emptyMessage="The first story is being prepared."
/>
</BaseLayout>
<style>
.archive-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1.5rem;
padding: clamp(1.6rem, 4vw, 3.25rem) 0 1rem;
border-bottom: 1px solid var(--ink);
}
.archive-head h1 {
margin: 0;
font-family: 'Newsreader Variable', Georgia, serif;
font-size: clamp(2.4rem, 5vw, 4.5rem);
font-weight: 560;
letter-spacing: -0.045em;
line-height: 0.9;
}
.archive-head p {
margin: 0;
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
text-transform: uppercase;
}
.archive-head span { margin-left: 1rem; }
.archive-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin-top: 1px;
border: 1px solid var(--rule);
background: var(--rule);
}
@media (max-width: 900px) {
.archive-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
.archive-head { align-items: flex-start; flex-direction: column; gap: 0.65rem; }
.archive-head span { display: none; }
.archive-list { grid-template-columns: 1fr; }
}
</style>
+37
View File
@@ -0,0 +1,37 @@
---
import { getCollection } from 'astro:content';
import StoryArchive from '../../components/StoryArchive.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const stories = await getCollection('stories', ({ data }) => !data.draft);
const counts = new Map<string, number>();
for (const tag of stories.flatMap(({ data }) => data.tags)) {
counts.set(tag, (counts.get(tag) ?? 0) + 1);
}
// A subject earns a page once a second story carries it.
return [...counts.entries()]
.filter(([, count]) => count >= 2)
.map(([tag]) => ({
params: { tag },
props: { entries: stories.filter(({ data }) => data.tags.includes(tag)) },
}));
}
const { tag } = Astro.params;
const { entries } = Astro.props;
const sorted = entries.sort((a, b) => a.data.editorialOrder - b.data.editorialOrder);
const reportCount = sorted.filter(({ data }) => data.type === 'report').length;
---
<BaseLayout title={`Tagged: ${tag}`} description={`Reports and essays tagged ${tag}. Tags overlap — a story can belong to several subjects at once.`}>
<StoryArchive
title={tag}
count={`${sorted.length} stories — ${reportCount} reports, ${sorted.length - reportCount} essays`}
note="All tags overlap by design"
noteHref="/whats-up"
entries={sorted}
emptyMessage="Nothing carries this tag yet."
activeTag={tag}
/>
</BaseLayout>
+19
View File
@@ -0,0 +1,19 @@
---
import { getCollection } from 'astro:content';
import StoryArchive from '../components/StoryArchive.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
const reports = (await getCollection('stories', ({ data }) => !data.draft && data.type === 'report'))
.sort((a, b) => b.data.published.getTime() - a.data.published.getTime());
---
<BaseLayout title="What's up" description="What is happening: reported pieces on systems, incidents, and the people inside them.">
<StoryArchive
title="What's up"
count={`${reports.length} reports — what is happening`}
note="The weekly briefing →"
noteHref="/briefing"
entries={reports}
emptyMessage="The first report is being prepared."
/>
</BaseLayout>