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
+10
View File
@@ -8,6 +8,16 @@ We ask one recurring question with cheerful suspicion: when a system claims to b
Stories begin with people and consequences. Technical machinery is explained in plain English. Jargon is defined when it is first used. Outrage is reserved for what the evidence supports. Stories begin with people and consequences. Technical machinery is explained in plain English. Jargon is defined when it is first used. Outrage is reserved for what the evidence supports.
## Sections
Stories are typed, and the boundary is a commitment readers can check: a piece pegged to a system or event in the world is a **report** (What's up); an argument that would survive the news cycle is an **essay** (Opine). Reports are expected to age into `historical` status; essays carry review dates that mean something.
Subjects are overlapping tags, not exclusive departments. A story about the EU wallet may legitimately be identity, privacy, and institutions at once.
## Corrections page
The corrections page is generated from revision history: any commit whose message begins with `Correction:` appears there automatically. The page cannot list fewer corrections than the repository contains.
## Authorship and maintenance ## Authorship and maintenance
An author and a maintainer are different roles. Every published item identifies its author. Maintained material also names the person responsible for review. An author and a maintainer are different roles. Every published item identifies its author. Maintained material also names the person responsible for review.
+5 -1
View File
@@ -12,12 +12,16 @@ import { site } from '../site.config';
<p class="footer-label">Read independently</p> <p class="footer-label">Read independently</p>
<a href="/rss.xml">RSS feed</a><br /> <a href="/rss.xml">RSS feed</a><br />
<a href={site.podcastFeedUrl}>Podcast feed</a><br /> <a href={site.podcastFeedUrl}>Podcast feed</a><br />
<a href="/briefing">Weekly briefing</a><br />
<a href={site.repositoryUrl}>Public repository</a> <a href={site.repositoryUrl}>Public repository</a>
</div> </div>
<div> <div>
<p class="footer-label">Editorial record</p> <p class="footer-label">Editorial record</p>
<a href="/about">About</a><br />
<a href="/corrections">Corrections</a><br />
<a href={`${site.repositoryUrl}/src/branch/main/EDITORIAL.md`}>Commitments</a><br /> <a href={`${site.repositoryUrl}/src/branch/main/EDITORIAL.md`}>Commitments</a><br />
<a href={`${site.repositoryUrl}/commits/branch/main`}>Revision history</a> <a href={`${site.repositoryUrl}/commits/branch/main`}>Revision history</a><br />
{site.contactUrl && <a href={site.contactUrl}>Contact</a>}
</div> </div>
</div> </div>
<div class="shell colophon"> <div class="shell colophon">
+4 -5
View File
@@ -2,19 +2,19 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import { navigation, site } from '../site.config'; import { navigation, site } from '../site.config';
const [stories, briefings, episodes, guides] = await Promise.all([ const [stories, episodes, guides] = await Promise.all([
getCollection('stories', ({ data }) => !data.draft), getCollection('stories', ({ data }) => !data.draft),
getCollection('briefings', ({ data }) => !data.draft),
getCollection('episodes', ({ data }) => !data.draft), getCollection('episodes', ({ data }) => !data.draft),
getCollection('guides', ({ data }) => !data.draft), getCollection('guides', ({ data }) => !data.draft),
]); ]);
const visibleNavigation = navigation.filter((item) => { const visibleNavigation = navigation.filter((item) => {
if (item.href === '/stories') return stories.length > 0; if (item.href === '/whats-up') return stories.some(({ data }) => data.type === 'report');
if (item.href === '/briefing') return briefings.length > 0; if (item.href === '/opine') return stories.some(({ data }) => data.type === 'essay');
if (item.href === '/podcast') return episodes.length > 0 || Boolean(site.podcastFeedUrl); if (item.href === '/podcast') return episodes.length > 0 || Boolean(site.podcastFeedUrl);
if (item.href === '/guides') return guides.length > 0; if (item.href === '/guides') return guides.length > 0;
if (item.href === '/book') return site.book.enabled; if (item.href === '/book') return site.book.enabled;
if (item.href === '/forum') return Boolean(site.forumUrl);
return true; return true;
}); });
--- ---
@@ -28,7 +28,6 @@ const visibleNavigation = navigation.filter((item) => {
<nav aria-label="Primary navigation"> <nav aria-label="Primary navigation">
{visibleNavigation.map((item) => <a href={item.href}>{item.label}</a>)} {visibleNavigation.map((item) => <a href={item.href}>{item.label}</a>)}
<a class="rss" href="/rss.xml">RSS</a>
</nav> </nav>
</div> </div>
</header> </header>
+127
View File
@@ -0,0 +1,127 @@
---
import type { CollectionEntry } from 'astro:content';
import StoryCard from './StoryCard.astro';
interface Props {
title: string;
count: string;
note?: string;
noteHref?: string;
entries: CollectionEntry<'stories'>[];
emptyMessage: string;
activeTag?: string;
}
const { title, count, note, noteHref, entries, emptyMessage, activeTag } = Astro.props;
const tagCounts = new Map<string, number>();
for (const entry of entries) {
for (const tag of entry.data.tags) {
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
}
}
// Only subjects carried by 2+ of the listed stories get a filter link
// (which also guarantees the target tag page exists).
const tags = [...tagCounts.entries()]
.filter(([, count]) => count >= 2)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
---
<section class="shell archive-head">
<h1>{title}</h1>
<p>{count}{note && noteHref && <a href={noteHref}>{note}</a>}</p>
</section>
{tags.length > 1 && (
<nav class="shell tag-row" aria-label="Browse by subject">
{tags.map(([tag, n]) => (
<a href={`/tags/${tag}`} class:list={{ active: tag === activeTag }}>{tag}<span>{n}</span></a>
))}
</nav>
)}
<section class="shell archive-list">
{entries.length > 0 ? entries.map((entry, index) => <StoryCard entry={entry} index={index} />) : <p class="empty-state">{emptyMessage}</p>}
</section>
<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 a {
margin-left: 1rem;
color: var(--blue);
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.9rem;
padding: 0.85rem 0;
}
.tag-row a {
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.66rem;
letter-spacing: 0.04em;
text-decoration: none;
text-transform: uppercase;
}
.tag-row a:hover,
.tag-row a.active {
color: var(--ink);
text-decoration: underline;
text-underline-offset: 0.25em;
}
.tag-row span {
margin-left: 0.3rem;
color: var(--rule);
}
.tag-row a:hover span,
.tag-row a.active span {
color: var(--amend);
}
.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-list { grid-template-columns: 1fr; }
}
</style>
+1
View File
@@ -16,6 +16,7 @@ const stories = defineCollection({
loader: glob({ base: './src/content/stories', pattern: '**/*.md' }), loader: glob({ base: './src/content/stories', pattern: '**/*.md' }),
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
type: z.enum(['report', 'essay']),
description: z.string(), description: z.string(),
published: z.coerce.date(), published: z.coerce.date(),
editorialOrder: z.number().int().positive(), editorialOrder: z.number().int().positive(),
@@ -1,5 +1,6 @@
--- ---
title: "Aadhaar: when authentication becomes rations" title: "Aadhaar: when authentication becomes rations"
type: report
description: "A biometric match can be highly accurate at national scale and still be the wrong place to put the cost of a failed fingerprint." description: "A biometric match can be highly accurate at national scale and still be the wrong place to put the cost of a failed fingerprint."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 12 editorialOrder: 12
@@ -1,5 +1,6 @@
--- ---
title: "When does an age check become an identity check?" title: "When does an age check become an identity check?"
type: report
description: "The EU's age-verification blueprint promises a simple yes-or-no proof. Its success depends on preventing that proof from becoming a reusable tracking handle." description: "The EU's age-verification blueprint promises a simple yes-or-no proof. Its success depends on preventing that proof from becoming a reusable tracking handle."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 9 editorialOrder: 9
@@ -1,5 +1,6 @@
--- ---
title: "Certificate Transparency does not prevent mistakes" title: "Certificate Transparency does not prevent mistakes"
type: report
description: "It makes certificate authorities observable. That narrower promise has done more for trust than pretending misissuance can be designed away." description: "It makes certificate authorities observable. That narrower promise has done more for trust than pretending misissuance can be designed away."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 20 editorialOrder: 20
@@ -1,5 +1,6 @@
--- ---
title: "Estonia and the chip that broke trust" title: "Estonia and the chip that broke trust"
type: report
description: "The model digital state survived a mass identity-card vulnerability because it had revocation, remote repair, alternatives and an institution willing to act." description: "The model digital state survived a mass identity-card vulnerability because it had revocation, remote repair, alternatives and an institution willing to act."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 14 editorialOrder: 14
@@ -1,5 +1,6 @@
--- ---
title: "The EU wallet arrives before its privacy does" title: "The EU wallet arrives before its privacy does"
type: report
description: "Europe's identity wallet is real, useful, optional—and racing toward a privacy problem that selective disclosure does not solve." description: "Europe's identity wallet is real, useful, optional—and racing toward a privacy problem that selective disclosure does not solve."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 1 editorialOrder: 1
@@ -1,5 +1,6 @@
--- ---
title: "Fraud and failure leave different-shaped messes" title: "Fraud and failure leave different-shaped messes"
type: essay
description: "A fraudulent organisation may coordinate brilliantly around a lie. A genuinely failing one often loses the ability to coordinate at all." description: "A fraudulent organisation may coordinate brilliantly around a lie. A genuinely failing one often loses the ability to coordinate at all."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 26 editorialOrder: 26
@@ -1,5 +1,6 @@
--- ---
title: "Identity is the wrong abstraction" title: "Identity is the wrong abstraction"
type: essay
description: "A library needs to know that you may borrow a book. It usually does not need a universal account of who you are." description: "A library needs to know that you may borrow a book. It usually does not need a universal account of who you are."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 16 editorialOrder: 16
@@ -1,5 +1,6 @@
--- ---
title: "Money is a memory system" title: "Money is a memory system"
type: essay
description: "A balance remembers past contribution, couples strangers and compresses many kinds of value into one number. That is why monetary failure feels like amnesia." description: "A balance remembers past contribution, couples strangers and compresses many kinds of value into one number. That is why monetary failure feels like amnesia."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 21 editorialOrder: 21
@@ -1,5 +1,6 @@
--- ---
title: "The name you give a key" title: "The name you give a key"
type: essay
description: "Petnames offer a modest way through the impossible demand that online names be secure, global, decentralised and easy to remember." description: "Petnames offer a modest way through the impossible demand that online names be secure, global, decentralised and easy to remember."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 17 editorialOrder: 17
@@ -1,5 +1,6 @@
--- ---
title: "A network state still needs a lost-password desk" title: "A network state still needs a lost-password desk"
type: essay
description: "Founding a cloud community is dramatic. Handling a dead founder, stolen key, contested vote or stranded member is what makes it political." description: "Founding a cloud community is dramatic. Handling a dead founder, stolen key, contested vote or stranded member is what makes it political."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 22 editorialOrder: 22
@@ -1,5 +1,6 @@
--- ---
title: "Your old phone is the real identity test" title: "Your old phone is the real identity test"
type: essay
description: "Login demos happen on new devices. Trust is decided after a theft, a dead battery, a changed number, or a relationship someone needs to escape." description: "Login demos happen on new devices. Trust is decided after a theft, a dead battery, a changed number, or a relationship someone needs to escape."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 2 editorialOrder: 2
@@ -1,5 +1,6 @@
--- ---
title: "One score to rule a life" title: "One score to rule a life"
type: essay
description: "Global trust scores are attractive because they make people easy to compare. That is also the reason not to build one." description: "Global trust scores are attractive because they make people easy to compare. That is also the reason not to build one."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 23 editorialOrder: 23
+1
View File
@@ -1,5 +1,6 @@
--- ---
title: "Repair is a test, not a moral duty" title: "Repair is a test, not a moral duty"
type: essay
description: "Trying to fix a system can generate the evidence that the system itself—not one parameter—is wrong." description: "Trying to fix a system can generate the evidence that the system itself—not one parameter—is wrong."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 25 editorialOrder: 25
@@ -1,5 +1,6 @@
--- ---
title: "Reputation should not follow you everywhere" title: "Reputation should not follow you everywhere"
type: essay
description: "Portable reputation sounds liberating until a rating earned in one room becomes a verdict in every other room." description: "Portable reputation sounds liberating until a rating earned in one room becomes a verdict in every other room."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 4 editorialOrder: 4
@@ -1,5 +1,6 @@
--- ---
title: "The safety number nobody checks" title: "The safety number nobody checks"
type: report
description: "Signal gives every pair of correspondents a strong way to verify keys. Most people ignore it, and the design remains useful." description: "Signal gives every pair of correspondents a strong way to verify keys. Most people ignore it, and the design remains useful."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 19 editorialOrder: 19
@@ -1,5 +1,6 @@
--- ---
title: "Selective disclosure is not invisibility" title: "Selective disclosure is not invisibility"
type: essay
description: "Showing fewer fields is good privacy. It does not necessarily stop a verifier from recognising you across presentations." description: "Showing fewer fields is good privacy. It does not necessarily stop a verifier from recognising you across presentations."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 10 editorialOrder: 10
@@ -1,5 +1,6 @@
--- ---
title: "Small communities do not scale. They federate." title: "Small communities do not scale. They federate."
type: essay
description: "The intimacy that makes a neighbourhood, forum or practice group trustworthy is exactly what disappears when it becomes one enormous room." description: "The intimacy that makes a neighbourhood, forum or practice group trustworthy is exactly what disappears when it becomes one enormous room."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 6 editorialOrder: 6
@@ -1,5 +1,6 @@
--- ---
title: "The Sovereign Individual's favourite verb is exit" title: "The Sovereign Individual's favourite verb is exit"
type: essay
description: "Digital sovereignty begins as a technical promise and ends as a political question: who can leave, what can they take, and who is left behind?" description: "Digital sovereignty begins as a technical promise and ends as a political question: who can leave, what can they take, and who is left behind?"
published: 2026-07-14 published: 2026-07-14
editorialOrder: 7 editorialOrder: 7
@@ -1,5 +1,6 @@
--- ---
title: "The state can protect your privacy and still see everything" title: "The state can protect your privacy and still see everything"
type: report
description: "China's national internet identity service reduces the need to hand civil-ID data to platforms by concentrating authentication in a public system." description: "China's national internet identity service reduces the need to hand civil-ID data to platforms by concentrating authentication in a public system."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 13 editorialOrder: 13
@@ -1,5 +1,6 @@
--- ---
title: "Traceability beats transparency" title: "Traceability beats transparency"
type: essay
description: "Making everything visible overwhelms the people meant to benefit. Linking decisions to actors and outcomes gives them something usable." description: "Making everything visible overwhelms the people meant to benefit. Linking decisions to actors and outcomes gives them something usable."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 28 editorialOrder: 28
@@ -1,5 +1,6 @@
--- ---
title: "Trust is accepted vulnerability" title: "Trust is accepted vulnerability"
type: essay
description: "If nothing can be lost, betrayed or mishandled, you may have confidence, prediction or control—but you do not yet have trust." description: "If nothing can be lost, betrayed or mishandled, you may have confidence, prediction or control—but you do not yet have trust."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 8 editorialOrder: 8
@@ -1,5 +1,6 @@
--- ---
title: "The first connection is the dangerous one" title: "The first connection is the dangerous one"
type: essay
description: "SSH does not promise a global oracle for every server. It remembers the key it saw and becomes suspicious when reality changes." description: "SSH does not promise a global oracle for every server. It remembers the key it saw and becomes suspicious when reality changes."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 18 editorialOrder: 18
@@ -1,5 +1,6 @@
--- ---
title: "We do not need more trust" title: "We do not need more trust"
type: essay
description: "Calls to restore trust often ask the public to change its mood while leaving the reasons for distrust untouched." description: "Calls to restore trust often ask the public to change its mood while leaving the reasons for distrust untouched."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 24 editorialOrder: 24
@@ -1,5 +1,6 @@
--- ---
title: "Who sent this agent?" title: "Who sent this agent?"
type: report
description: "An AI agent can have a name, a key and a polished voice while remaining unable to show who authorised the action it is taking now." description: "An AI agent can have a name, a key and a polished voice while remaining unable to show who authorised the action it is taking now."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 3 editorialOrder: 3
@@ -1,5 +1,6 @@
--- ---
title: "Why digital identity projects keep dying" title: "Why digital identity projects keep dying"
type: report
description: "Passport, CardSpace, Persona and Web5 did not all fail for the same reason. Together they show that good identity architecture can still lose to habit." description: "Passport, CardSpace, Persona and Web5 did not all fail for the same reason. Together they show that good identity architecture can still lose to habit."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 15 editorialOrder: 15
@@ -1,5 +1,6 @@
--- ---
title: "Why smart people join high-trust groups" title: "Why smart people join high-trust groups"
type: essay
description: "Belonging, meaning and certainty can be genuine goods while the group quietly makes leaving each one more expensive." description: "Belonging, meaning and certainty can be genuine goods while the group quietly makes leaving each one more expensive."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 27 editorialOrder: 27
@@ -1,5 +1,6 @@
--- ---
title: "Worldcoin and the price of one human" title: "Worldcoin and the price of one human"
type: report
description: "Proof of personhood answers a real AI-era problem. The Orb makes the bargain unusually visible: uniqueness in exchange for an irreversible measurement." description: "Proof of personhood answers a real AI-era problem. The Orb makes the bargain unusually visible: uniqueness in exchange for an irreversible measurement."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 5 editorialOrder: 5
@@ -1,5 +1,6 @@
--- ---
title: "Your wallet has a government inside it" title: "Your wallet has a government inside it"
type: essay
description: "The credential can sit on your phone while the rules about valid issuers, revocation and recovery remain thoroughly institutional." description: "The credential can sit on your phone while the rules about valid issuers, revocation and recovery remain thoroughly institutional."
published: 2026-07-14 published: 2026-07-14
editorialOrder: 11 editorialOrder: 11
+26
View File
@@ -6,6 +6,32 @@ export interface MaintenanceCommit {
subject: string; subject: string;
} }
export function correctionCommits(): MaintenanceCommit[] {
try {
const output = execFileSync(
'git',
[
'log',
'--grep=^Correction:',
'--date=short',
'--pretty=format:%h%x09%ad%x09%s',
],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
);
return output
.trim()
.split('\n')
.filter(Boolean)
.map((line: string) => {
const [hash, date, ...subject] = line.split('\t');
return { hash, date, subject: subject.join(' ') };
});
} catch {
return [];
}
}
export function recentMaintenance(limit = 4): MaintenanceCommit[] { export function recentMaintenance(limit = 4): MaintenanceCommit[] {
try { try {
const output = execFileSync( const output = execFileSync(
+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 { getCollection } from 'astro:content';
import StoryCard from '../../components/StoryCard.astro'; import StoryArchive from '../../components/StoryArchive.astro';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
const stories = (await getCollection('stories', ({ data }) => !data.draft)) const stories = (await getCollection('stories', ({ data }) => !data.draft))
.sort((a, b) => a.data.editorialOrder - b.data.editorialOrder); .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."> <BaseLayout title="Stories" description="Reported and argued stories about trust, identity, and authority.">
<section class="shell archive-head"> <StoryArchive
<h1>Stories</h1> title="Stories"
<p>{stories.length} reports, histories and essays <span>First collection</span></p> count={`${stories.length} stories — ${reportCount} reports, ${stories.length - reportCount} essays`}
</section> entries={stories}
<section class="shell archive-list"> emptyMessage="The first story is being prepared."
{stories.length > 0 ? stories.map((entry, index) => <StoryCard entry={entry} index={index} />) : <p class="empty-state">The first story is being prepared.</p>} />
</section>
</BaseLayout> </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>
+5 -4
View File
@@ -11,6 +11,7 @@ export const site = {
repositoryUrl: 'https://git.frrn.life/ana/trust-issues', repositoryUrl: 'https://git.frrn.life/ana/trust-issues',
newsletterUrl: '', newsletterUrl: '',
podcastFeedUrl: '/podcast/feed.xml', podcastFeedUrl: '/podcast/feed.xml',
forumUrl: '',
contactUrl: '', contactUrl: '',
book: { book: {
enabled: false, enabled: false,
@@ -23,10 +24,10 @@ export const site = {
} as const; } as const;
export const navigation = [ export const navigation = [
{ href: '/stories', label: 'Stories' }, { href: '/whats-up', label: "What's up" },
{ href: '/briefing', label: 'Briefing' }, { href: '/opine', label: 'Opine' },
{ href: '/podcast', label: 'Podcast' }, { href: '/podcast', label: 'Podcast' },
{ href: '/guides', label: 'Guides' },
{ href: '/book', label: 'Book' }, { href: '/book', label: 'Book' },
{ href: '/about', label: 'About' }, { href: '/guides', label: 'Guides' },
{ href: '/forum', label: 'Forum' },
] as const; ] as const;