Restructure navigation around What's up / Opine and the reader's utilities
validate / validate (push) Successful in 22s
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:
@@ -12,12 +12,16 @@ import { site } from '../site.config';
|
||||
<p class="footer-label">Read independently</p>
|
||||
<a href="/rss.xml">RSS 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>
|
||||
</div>
|
||||
<div>
|
||||
<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}/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 class="shell colophon">
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
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('briefings', ({ data }) => !data.draft),
|
||||
getCollection('episodes', ({ data }) => !data.draft),
|
||||
getCollection('guides', ({ data }) => !data.draft),
|
||||
]);
|
||||
|
||||
const visibleNavigation = navigation.filter((item) => {
|
||||
if (item.href === '/stories') return stories.length > 0;
|
||||
if (item.href === '/briefing') return briefings.length > 0;
|
||||
if (item.href === '/whats-up') return stories.some(({ data }) => data.type === 'report');
|
||||
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 === '/guides') return guides.length > 0;
|
||||
if (item.href === '/book') return site.book.enabled;
|
||||
if (item.href === '/forum') return Boolean(site.forumUrl);
|
||||
return true;
|
||||
});
|
||||
---
|
||||
@@ -28,7 +28,6 @@ const visibleNavigation = navigation.filter((item) => {
|
||||
|
||||
<nav aria-label="Primary navigation">
|
||||
{visibleNavigation.map((item) => <a href={item.href}>{item.label}</a>)}
|
||||
<a class="rss" href="/rss.xml">RSS</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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>
|
||||
@@ -16,6 +16,7 @@ const stories = defineCollection({
|
||||
loader: glob({ base: './src/content/stories', pattern: '**/*.md' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
type: z.enum(['report', 'essay']),
|
||||
description: z.string(),
|
||||
published: z.coerce.date(),
|
||||
editorialOrder: z.number().int().positive(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 12
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 9
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 20
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 14
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 26
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 16
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 21
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 17
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 22
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 23
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 25
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 4
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 19
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 10
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 6
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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?"
|
||||
published: 2026-07-14
|
||||
editorialOrder: 7
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 13
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 28
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 8
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 18
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 24
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 3
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 15
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 27
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 5
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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."
|
||||
published: 2026-07-14
|
||||
editorialOrder: 11
|
||||
|
||||
@@ -6,6 +6,32 @@ export interface MaintenanceCommit {
|
||||
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[] {
|
||||
try {
|
||||
const output = execFileSync(
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -11,6 +11,7 @@ export const site = {
|
||||
repositoryUrl: 'https://git.frrn.life/ana/trust-issues',
|
||||
newsletterUrl: '',
|
||||
podcastFeedUrl: '/podcast/feed.xml',
|
||||
forumUrl: '',
|
||||
contactUrl: '',
|
||||
book: {
|
||||
enabled: false,
|
||||
@@ -23,10 +24,10 @@ export const site = {
|
||||
} as const;
|
||||
|
||||
export const navigation = [
|
||||
{ href: '/stories', label: 'Stories' },
|
||||
{ href: '/briefing', label: 'Briefing' },
|
||||
{ href: '/whats-up', label: "What's up" },
|
||||
{ href: '/opine', label: 'Opine' },
|
||||
{ href: '/podcast', label: 'Podcast' },
|
||||
{ href: '/guides', label: 'Guides' },
|
||||
{ href: '/book', label: 'Book' },
|
||||
{ href: '/about', label: 'About' },
|
||||
{ href: '/guides', label: 'Guides' },
|
||||
{ href: '/forum', label: 'Forum' },
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user