Establish the publication foundation
validate / validate (push) Failing after 7s

This commit is contained in:
Compleet
2026-07-14 11:54:31 +01:00
commit 006955bfc8
46 changed files with 7704 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { site } from '../site.config';
---
<BaseLayout title="About" description={`About ${site.title} and its editorial approach.`}>
<section class="shell about-head">
<p class="eyebrow">About the publication</p>
<h1 class="display">Friendly on the surface. Inspectable underneath.</h1>
</section>
<section class="shell about-grid">
<aside>
<p class="portrait" aria-label="Portrait placeholder">ANA</p>
<p><strong>{site.author.name}</strong><br />Author and maintainer</p>
</aside>
<div class="prose">
<p><em>Trust Issues</em> is an independent publication about who and what we trust online, where authority actually sits, and what happens when systems meet human lives.</p>
<p>It is written by Ana. That authorship is not hidden behind an institutional voice: judgement has a source, and the person making it should be visible.</p>
<h2>Cheerful suspicion</h2>
<p>When something is described as verified, trustless, self-sovereign, privacy-preserving, safe, or autonomous, the useful next question is not whether to believe or ridicule it. It is: what does that claim mean here, who controls the exception, and where did the trust move?</p>
<h2>How maintenance works</h2>
<p>Articles carry publication and review dates, named authors and maintainers, and an honest status. Git provides a public provenance record. It does not certify that the reporting is true; corrections, sourcing, fair representation, and editorial independence remain human responsibilities.</p>
<h2>Contact</h2>
{site.contactUrl ? <p><a href={site.contactUrl}>Contact Ana privately</a>.</p> : <p>A dedicated private contact and source-safety route will be added before public launch. Until then, use an existing trusted channel rather than a public repository issue for anything sensitive.</p>}
</div>
</section>
</BaseLayout>
<style>
.about-head { padding: clamp(4rem, 9vw, 8rem) 0; }
.about-head h1 { max-width: 14ch; }
.about-grid { display: grid; grid-template-columns: minmax(180px, .5fr) 1.5fr; gap: clamp(3rem, 10vw, 10rem); align-items: start; }
aside { position: sticky; top: 2rem; font-size: .8rem; }
.portrait { display: grid; aspect-ratio: 4 / 5; margin: 0 0 1rem; place-items: center; background: var(--blue); color: var(--paper); font-family: 'Newsreader', Georgia, serif; font-size: 3rem; }
@media (max-width: 700px) { .about-grid { grid-template-columns: 1fr; } aside { position: static; max-width: 220px; } }
</style>
+43
View File
@@ -0,0 +1,43 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { site } from '../site.config';
---
<BaseLayout title="Book" description="Ana's book: argument, sample, and purchase information.">
<section class="shell book-page">
<div class="cover" aria-hidden="true">
{site.book.cover ? <img src={site.book.cover} alt="" /> : <span>THE<br />BOOK</span>}
</div>
<div>
<p class="eyebrow">The book</p>
<h1 class="display">{site.book.enabled ? site.book.title : 'The book belongs at the centre of the work.'}</h1>
<p class="lede">{site.book.enabled ? site.book.description : 'This surface is ready for the actual cover, title, argument, sample chapter, endorsements, and purchase route. Those details are intentionally not fabricated in the scaffold.'}</p>
{site.book.enabled && (
<div class="actions">
{site.book.sampleUrl && <a class="button" href={site.book.sampleUrl}>Read a sample</a>}
{site.book.buyUrl && <a class="button primary" href={site.book.buyUrl}>Buy the book</a>}
</div>
)}
<div class="book-anatomy">
<div><span>01</span><p>Why I wrote it</p></div>
<div><span>02</span><p>Read a real chapter</p></div>
<div><span>03</span><p>Choose a purchase route</p></div>
</div>
</div>
</section>
</BaseLayout>
<style>
.book-page { display: grid; grid-template-columns: minmax(250px, .65fr) 1.35fr; gap: clamp(3rem, 9vw, 9rem); align-items: center; padding: clamp(4rem, 9vw, 8rem) 0; }
.cover { display: grid; aspect-ratio: 3 / 4; place-items: center; border: 1px solid #7690ad; background: var(--blue); box-shadow: 22px 22px 0 #d8d1c4; color: var(--paper); font-family: 'Newsreader', Georgia, serif; font-size: clamp(2.5rem, 6vw, 5rem); line-height: .85; text-align: center; }
.cover img { width: 100%; height: 100%; object-fit: cover; }
h1 { max-width: 13ch; }
.lede { max-width: 58ch; color: var(--muted); font-size: 1.1rem; }
.actions { display: flex; gap: .7rem; margin: 2rem 0; }
.book-anatomy { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 3rem; border-top: 1px solid var(--rule); }
.book-anatomy div { padding: 1rem; border-right: 1px solid var(--rule); }
.book-anatomy div:last-child { border-right: 0; }
.book-anatomy span { color: var(--amend); font-family: 'IBM Plex Mono', monospace; font-size: .68rem; }
.book-anatomy p { font-family: 'Newsreader', Georgia, serif; font-size: 1.15rem; }
@media (max-width: 750px) { .book-page { grid-template-columns: 1fr; } .cover { max-width: 320px; } .book-anatomy { grid-template-columns: 1fr; } .book-anatomy div { border-right: 0; border-bottom: 1px solid var(--rule); } }
</style>
+27
View File
@@ -0,0 +1,27 @@
---
import { getCollection, render, type CollectionEntry } from 'astro:content';
import ArticleLayout from '../../layouts/ArticleLayout.astro';
export async function getStaticPaths() {
const entries = await getCollection('briefings', ({ data }) => !data.draft);
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
}
interface Props { entry: CollectionEntry<'briefings'> }
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
kind={`This Week in Trust · Issue ${String(entry.data.issue).padStart(3, '0')}`}
author={entry.data.author}
maintainers={entry.data.maintainers}
published={entry.data.published}
reviewed={entry.data.reviewed}
status={entry.data.status}
sourcePath={`src/content/briefings/${entry.id}.md`}
>
<Content />
</ArticleLayout>
+36
View File
@@ -0,0 +1,36 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { formatDate, newestFirst } from '../../lib/content';
const entries = newestFirst(await getCollection('briefings', ({ data }) => !data.draft));
---
<BaseLayout title="This Week in Trust" description="The compact weekly briefing and historical record.">
<section class="shell archive-head">
<p class="eyebrow">The weekly ritual</p>
<h1 class="display">This Week<br />in Trust</h1>
<p>Three failures, two new systems, one government overreach, and one surprisingly good idea. Compact enough to read; consistent enough to become a record.</p>
</section>
<section class="shell issue-list">
{entries.length > 0 ? entries.map((entry) => (
<article>
<span>Issue {String(entry.data.issue).padStart(3, '0')}</span>
<h2><a href={`/briefing/${entry.id}`}>{entry.data.title}</a></h2>
<p>{entry.data.description}</p>
<time datetime={entry.data.published.toISOString()}>{formatDate(entry.data.published)}</time>
</article>
)) : <p class="empty-state">The first weekly briefing is being prepared.</p>}
</section>
</BaseLayout>
<style>
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
.archive-head > p:last-child { max-width: 58ch; color: var(--muted); }
.issue-list article { display: grid; grid-template-columns: 8rem 1fr 1fr 8rem; gap: 1.5rem; align-items: baseline; padding: 1.5rem 0; border-top: 1px solid var(--rule); }
.issue-list span, .issue-list time { color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: .68rem; }
h2 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: 2rem; line-height: 1; }
h2 a { text-decoration: none; }
.issue-list p { margin: 0; color: var(--muted); font-size: .9rem; }
@media (max-width: 760px) { .issue-list article { grid-template-columns: 1fr auto; } h2, .issue-list p { grid-column: 1 / -1; } }
</style>
+27
View File
@@ -0,0 +1,27 @@
---
import { getCollection, render, type CollectionEntry } from 'astro:content';
import ArticleLayout from '../../layouts/ArticleLayout.astro';
export async function getStaticPaths() {
const entries = await getCollection('guides', ({ data }) => !data.draft);
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
}
interface Props { entry: CollectionEntry<'guides'> }
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
kind="Field guide"
author={entry.data.author}
maintainers={entry.data.maintainers}
published={entry.data.firstPublished}
reviewed={entry.data.reviewed}
status={entry.data.status}
sourcePath={`src/content/guides/${entry.id}.md`}
>
<Content />
</ArticleLayout>
+39
View File
@@ -0,0 +1,39 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { formatDate, guidesByReview } from '../../lib/content';
const entries = guidesByReview(await getCollection('guides', ({ data }) => !data.draft));
---
<BaseLayout title="Field guides" description="Maintained explanations of systems that mediate identity, authority, and trust.">
<section class="shell archive-head">
<p class="eyebrow">Documentation written by journalists</p>
<h1 class="display">Field guides</h1>
<p>The page a smart newcomer needs after reading three contradictory accounts: what the system is, who must trust whom, what it knows, and what happens when access is lost.</p>
</section>
<section class="shell guide-list">
{entries.length > 0 ? entries.map((entry, index) => (
<article>
<span>0{index + 1}</span>
<div>
<h2><a href={`/guides/${entry.id}`}>{entry.data.title}</a></h2>
<p>{entry.data.description}</p>
</div>
<small>Last reviewed<br />{formatDate(entry.data.reviewed)}</small>
</article>
)) : <p class="empty-state">The first guides are in editorial review. Staleness will be visible once they publish.</p>}
</section>
</BaseLayout>
<style>
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
.archive-head > p:last-child { max-width: 62ch; color: var(--muted); }
.guide-list article { display: grid; grid-template-columns: 5rem 1fr 10rem; gap: 2rem; padding: 2rem 0; border-top: 1px solid var(--rule); }
.guide-list > article > span { color: var(--amend); font-family: 'IBM Plex Mono', monospace; font-size: .72rem; }
h2 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1; }
h2 a { text-decoration: none; }
p, small { color: var(--muted); }
small { font-family: 'IBM Plex Mono', monospace; font-size: .66rem; text-transform: uppercase; }
@media (max-width: 650px) { .guide-list article { grid-template-columns: 2rem 1fr; } small { grid-column: 2; } }
</style>
+407
View File
@@ -0,0 +1,407 @@
---
import { getCollection } from 'astro:content';
import RecentMaintenance from '../components/RecentMaintenance.astro';
import StoryCard from '../components/StoryCard.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { formatDate, guidesByReview, newestFirst } from '../lib/content';
import { site } from '../site.config';
const stories = newestFirst(await getCollection('stories', ({ data }) => !data.draft));
const briefings = newestFirst(await getCollection('briefings', ({ data }) => !data.draft));
const guides = guidesByReview(await getCollection('guides', ({ data }) => !data.draft));
const episodes = newestFirst(await getCollection('episodes', ({ data }) => !data.draft));
const featured = stories.find(({ data }) => data.featured) ?? stories[0];
const latestBriefing = briefings[0];
const latestEpisode = episodes[0];
---
<BaseLayout>
<section class="hero shell">
<p class="eyebrow">{site.author.note}</p>
<div class="hero-grid">
<h1 class="display">{site.proposition}</h1>
<div class="hero-note">
<p>{site.description}</p>
<p>
We follow claims of verification, safety, privacy, and autonomy to the
people who hold the keys—and to the people who live with the consequences.
</p>
<div class="hero-actions">
{featured && <a class="button primary" href={`/stories/${featured.id}`}>Read the current story</a>}
<a class="button" href="/podcast">Listen to the podcast</a>
</div>
</div>
</div>
</section>
<section class="shell current">
<div class="section-rule">
<h2 class="section-title">Current issue</h2>
<a href="/stories">All stories →</a>
</div>
<div class="current-grid">
<div>
{featured ? <StoryCard entry={featured} featured /> : <p class="empty-state">The first story is being prepared.</p>}
</div>
<aside class="briefing-card">
<p class="eyebrow">This week in trust</p>
{latestBriefing ? (
<>
<p class="issue">Issue {String(latestBriefing.data.issue).padStart(3, '0')}</p>
<h3><a href={`/briefing/${latestBriefing.id}`}>{latestBriefing.data.title}</a></h3>
<p>{latestBriefing.data.description}</p>
<div class="briefing-foot">
<time datetime={latestBriefing.data.published.toISOString()}>{formatDate(latestBriefing.data.published)}</time>
<a href={`/briefing/${latestBriefing.id}`}>Read issue →</a>
</div>
</>
) : (
<p>The weekly briefing begins here: three failures, two new systems, one overreach, and one surprisingly good idea.</p>
)}
</aside>
</div>
</section>
<section class="shell listening">
<div class="section-rule">
<h2 class="section-title">Listen</h2>
<a href="/podcast">Podcast archive →</a>
</div>
<div class="listen-grid">
<div class="sound-mark" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div>
{latestEpisode ? (
<>
<p class="eyebrow">Episode {latestEpisode.data.episode}</p>
<h3><a href={`/podcast/${latestEpisode.id}`}>{latestEpisode.data.title}</a></h3>
<p>{latestEpisode.data.description}</p>
</>
) : (
<>
<p class="eyebrow">Podcast</p>
<h3>Who gets to act in our name?</h3>
<p>Conversations about authority, recovery, delegation, and the human lives hidden inside technical systems. Episodes and full transcripts will live here.</p>
</>
)}
</div>
</div>
</section>
<section class="shell guides">
<div class="section-rule">
<h2 class="section-title">Field guides</h2>
<a href="/guides">All guides →</a>
</div>
{guides.length > 0 ? (
<div class="guide-grid">
{guides.slice(0, 3).map((guide, index) => (
<article>
<span class="guide-number">0{index + 1}</span>
<h3><a href={`/guides/${guide.id}`}>{guide.data.title}</a></h3>
<p>{guide.data.description}</p>
<small>Reviewed {formatDate(guide.data.reviewed)}</small>
</article>
))}
</div>
) : (
<div class="guide-grid planned">
{['Passkeys', 'Government wallets', 'Agent authority'].map((title, index) => (
<article>
<span class="guide-number">0{index + 1}</span>
<h3>{title}</h3>
<p>In preparation. Published only when the review date means something.</p>
</article>
))}
</div>
)}
</section>
<section class="shell book-strip">
<div class="book-object" aria-hidden="true"><span>THE<br />BOOK</span></div>
<div>
<p class="eyebrow">The book</p>
<h2>A permanent home, not a checkout widget.</h2>
<p>The book page is ready for its real title, cover, sample chapter, argument, and purchase route—without inventing details before they are supplied.</p>
<a class="button" href="/book">Visit the book page</a>
</div>
</section>
<div class="shell maintenance-wrap">
<RecentMaintenance />
</div>
</BaseLayout>
<style>
.hero {
padding-top: clamp(4rem, 9vw, 8rem);
padding-bottom: clamp(5rem, 10vw, 9rem);
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.65fr) minmax(260px, 0.65fr);
gap: clamp(2rem, 6vw, 6rem);
align-items: end;
}
.hero-note {
padding-left: 1.5rem;
border-left: 1px solid var(--ink);
}
.hero-note p:first-child {
font-family: 'Newsreader', Georgia, serif;
font-size: 1.45rem;
line-height: 1.2;
}
.hero-note p:not(:first-child) {
color: var(--muted);
font-size: 0.92rem;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
margin-top: 1.5rem;
}
.current-grid {
display: grid;
grid-template-columns: minmax(0, 1.8fr) minmax(280px, 0.7fr);
}
.briefing-card {
display: flex;
flex-direction: column;
padding: 2rem;
border-top: 1px solid var(--rule);
border-left: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
background: var(--paper-raised);
}
.briefing-card .issue {
margin: 0;
color: var(--amend);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
}
.briefing-card h3,
.listen-grid h3,
.guide-grid h3,
.book-strip h2 {
margin: 0.35rem 0;
font-family: 'Newsreader', Georgia, serif;
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1;
}
.briefing-card h3 {
font-size: 2.2rem;
}
.briefing-card h3 a,
.listen-grid h3 a,
.guide-grid h3 a {
text-decoration: none;
}
.briefing-card > p:not(.eyebrow, .issue) {
color: var(--muted);
}
.briefing-foot {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 2rem;
font-size: 0.75rem;
font-weight: 600;
}
.listening,
.guides,
.book-strip,
.maintenance-wrap {
margin-top: clamp(5rem, 10vw, 9rem);
}
.listen-grid {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: clamp(2rem, 7vw, 7rem);
padding: 3rem 0;
}
.sound-mark {
display: flex;
height: 180px;
align-items: center;
justify-content: center;
gap: 1rem;
background: var(--ink);
}
.sound-mark span {
width: 3px;
height: 28%;
background: var(--paper);
}
.sound-mark span:nth-child(2),
.sound-mark span:nth-child(4) {
height: 60%;
}
.sound-mark span:nth-child(3) {
height: 88%;
background: var(--amend);
}
.listen-grid h3 {
max-width: 18ch;
font-size: clamp(2rem, 5vw, 4.2rem);
}
.listen-grid p:not(.eyebrow) {
max-width: 55ch;
color: var(--muted);
}
.guide-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.guide-grid article {
min-height: 240px;
padding: 1.5rem;
border-right: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.guide-grid article:last-child {
border-right: 0;
}
.guide-number {
color: var(--amend);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
}
.guide-grid h3 {
margin-top: 3rem;
font-size: 2rem;
}
.guide-grid p,
.guide-grid small {
color: var(--muted);
}
.guide-grid small {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
}
.planned article {
opacity: 0.75;
}
.book-strip {
display: grid;
grid-template-columns: minmax(200px, 0.6fr) 1.4fr;
gap: clamp(2rem, 8vw, 8rem);
align-items: center;
padding: clamp(2rem, 5vw, 5rem);
background: var(--blue);
color: var(--paper);
}
.book-object {
display: grid;
max-width: 260px;
aspect-ratio: 3 / 4;
place-items: center;
border: 1px solid #7690ad;
box-shadow: 18px 18px 0 #0e2c51;
background: #102f55;
font-family: 'Newsreader', Georgia, serif;
font-size: 2.5rem;
line-height: 0.9;
text-align: center;
}
.book-strip h2 {
max-width: 15ch;
font-size: clamp(2.6rem, 6vw, 5.2rem);
}
.book-strip p:not(.eyebrow) {
max-width: 58ch;
color: #c5d1df;
}
.book-strip .eyebrow {
color: #9fb8d4;
}
.book-strip .button {
border-color: var(--paper);
color: var(--paper);
}
.book-strip .button:hover {
background: var(--paper);
color: var(--blue);
}
@media (max-width: 850px) {
.hero-grid,
.current-grid,
.listen-grid,
.book-strip {
grid-template-columns: 1fr;
}
.hero-note {
padding-top: 1.5rem;
padding-left: 0;
border-top: 1px solid var(--ink);
border-left: 0;
}
.briefing-card {
min-height: 310px;
border-left: 0;
}
}
@media (max-width: 650px) {
.guide-grid {
grid-template-columns: 1fr;
}
.guide-grid article {
min-height: auto;
border-right: 0;
}
.guide-grid h3 {
margin-top: 1.5rem;
}
.book-strip {
width: 100%;
}
}
</style>
+32
View File
@@ -0,0 +1,32 @@
---
import { getCollection, render, type CollectionEntry } from 'astro:content';
import ArticleLayout from '../../layouts/ArticleLayout.astro';
export async function getStaticPaths() {
const entries = await getCollection('episodes', ({ data }) => !data.draft);
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
}
interface Props { entry: CollectionEntry<'episodes'> }
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
kind={`Podcast · Episode ${entry.data.episode}`}
author={entry.data.author}
maintainers={entry.data.maintainers}
published={entry.data.published}
reviewed={entry.data.reviewed}
status={entry.data.status}
sourcePath={`src/content/episodes/${entry.id}.md`}
>
{entry.data.audioUrl && <audio controls preload="metadata" src={entry.data.audioUrl}>Your browser does not support embedded audio.</audio>}
<Content />
</ArticleLayout>
<style>
audio { width: 100%; margin-bottom: 2.5rem; }
</style>
+54
View File
@@ -0,0 +1,54 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { formatDate, newestFirst } from '../../lib/content';
const entries = newestFirst(await getCollection('episodes', ({ data }) => !data.draft));
---
<BaseLayout title="Podcast" description="Conversations about authority, delegation, recovery, and responsibility.">
<section class="podcast-hero">
<div class="shell podcast-grid">
<div>
<p class="eyebrow">The podcast</p>
<h1 class="display">Who gets to act in our name?</h1>
</div>
<p>Conversations pairing people who rarely share a microphone: system designers and the people who meet those systems at their most consequential edges. Full transcripts live beside every episode.</p>
</div>
<div class="wave shell" aria-hidden="true">
{Array.from({ length: 21 }).map((_, index) => <i style={`--n:${(index * 37) % 100}`} />)}
</div>
</section>
<section class="shell episode-list">
<div class="section-rule">
<h2 class="section-title">Episodes</h2>
<a href="/rss.xml">RSS →</a>
</div>
{entries.length > 0 ? entries.map((entry) => (
<article>
<span>{String(entry.data.episode).padStart(2, '0')}</span>
<div>
<h3><a href={`/podcast/${entry.id}`}>{entry.data.title}</a></h3>
<p>{entry.data.description}</p>
</div>
<time datetime={entry.data.published.toISOString()}>{formatDate(entry.data.published)}</time>
</article>
)) : <p class="empty-state">The archive is ready for real episodes and transcripts. No synthetic placeholder episode has been published.</p>}
</section>
</BaseLayout>
<style>
.podcast-hero { padding: clamp(4rem, 9vw, 8rem) 0 0; background: var(--ink); color: var(--paper); }
.podcast-grid { display: grid; grid-template-columns: 1.5fr .7fr; gap: clamp(2rem, 7vw, 7rem); align-items: end; }
.podcast-grid .eyebrow { color: #9fb8d4; }
.podcast-grid > p { color: #bcb8af; }
.wave { display: flex; height: 210px; align-items: center; justify-content: space-between; margin-top: 4rem; overflow: hidden; }
.wave i { display: block; width: 2px; height: calc(20% + var(--n) * .75%); background: color-mix(in srgb, var(--paper) calc(25% + var(--n) * .5%), var(--amend)); }
.episode-list { margin-top: 6rem; }
.episode-list article { display: grid; grid-template-columns: 4rem 1fr 8rem; gap: 2rem; padding: 2rem 0; border-bottom: 1px solid var(--rule); }
.episode-list article > span, time { color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: .7rem; }
h3 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: 2.3rem; line-height: 1; }
h3 a { text-decoration: none; }
.episode-list p { color: var(--muted); }
@media (max-width: 700px) { .podcast-grid { grid-template-columns: 1fr; } .episode-list article { grid-template-columns: 2rem 1fr; } time { grid-column: 2; } }
</style>
+40
View File
@@ -0,0 +1,40 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { site } from '../site.config';
export async function GET(context: { site?: URL }) {
const [stories, briefings, episodes] = await Promise.all([
getCollection('stories', ({ data }) => !data.draft),
getCollection('briefings', ({ data }) => !data.draft),
getCollection('episodes', ({ data }) => !data.draft),
]);
const items = [
...stories.map((entry) => ({
title: entry.data.title,
description: entry.data.description,
pubDate: entry.data.published,
link: `/stories/${entry.id}`,
})),
...briefings.map((entry) => ({
title: entry.data.title,
description: entry.data.description,
pubDate: entry.data.published,
link: `/briefing/${entry.id}`,
})),
...episodes.map((entry) => ({
title: entry.data.title,
description: entry.data.description,
pubDate: entry.data.published,
link: `/podcast/${entry.id}`,
})),
].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf());
return rss({
title: `${site.title} ${site.qualifier}`,
description: site.description,
site: context.site ?? new URL('https://example.com'),
items,
customData: '<language>en-gb</language>',
});
}
+28
View File
@@ -0,0 +1,28 @@
---
import { getCollection, render, type CollectionEntry } from 'astro:content';
import ArticleLayout from '../../layouts/ArticleLayout.astro';
export async function getStaticPaths() {
const entries = await getCollection('stories', ({ data }) => !data.draft);
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
}
interface Props { entry: CollectionEntry<'stories'> }
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
kind="Story"
author={entry.data.author}
maintainers={entry.data.maintainers}
published={entry.data.published}
reviewed={entry.data.reviewed}
status={entry.data.status}
sourcePath={`src/content/stories/${entry.id}.md`}
trustPattern={entry.data.trustPattern}
>
<Content />
</ArticleLayout>
+25
View File
@@ -0,0 +1,25 @@
---
import { getCollection } from 'astro:content';
import StoryCard from '../../components/StoryCard.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { newestFirst } from '../../lib/content';
const stories = newestFirst(await getCollection('stories', ({ data }) => !data.draft));
---
<BaseLayout title="Stories" description="Reported and argued stories about trust, identity, and authority.">
<section class="shell archive-head">
<p class="eyebrow">Original reporting and essays</p>
<h1 class="display">Stories</h1>
<p>People first. Machinery explained. Claims followed to the point where somebody must decide, recover, consent, or bear the loss.</p>
</section>
<section class="shell archive-list">
{stories.length > 0 ? stories.map((entry) => <StoryCard entry={entry} />) : <p class="empty-state">The first story is being prepared.</p>}
</section>
</BaseLayout>
<style>
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
.archive-head > p:last-child { max-width: 58ch; color: var(--muted); font-size: 1.05rem; }
.archive-list { display: grid; gap: 2rem; }
</style>