243 lines
6.5 KiB
Text
243 lines
6.5 KiB
Text
---
|
||
import { getCollection, getEntry } from 'astro:content';
|
||
import AppLayout from '../../layouts/AppLayout.astro';
|
||
import { fmtDate } from '../../lib/markdown';
|
||
|
||
const user = Astro.locals.user;
|
||
const { slug } = Astro.params;
|
||
|
||
const update = await getEntry('updates', slug as string);
|
||
if (!update) return Astro.redirect('/updates');
|
||
|
||
const { Content } = await update.render();
|
||
|
||
const allUpdates = await getCollection('updates');
|
||
const sorted = allUpdates.sort(
|
||
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
|
||
);
|
||
const currentIndex = sorted.findIndex((u) => u.slug === update.slug);
|
||
const prev = sorted[currentIndex + 1] ?? null;
|
||
const next = sorted[currentIndex - 1] ?? null;
|
||
---
|
||
<AppLayout title={update.data.title} user={user}>
|
||
<div class="page">
|
||
|
||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||
<a href="/updates" class="crumb label-sm">Updates</a>
|
||
<span class="crumb-sep" aria-hidden="true">›</span>
|
||
<span class="crumb label-sm crumb-current">{update.data.title}</span>
|
||
</nav>
|
||
|
||
<article class="article">
|
||
<header class="article-header">
|
||
<time class="label-sm post-date" datetime={String(update.data.date)}>
|
||
{fmtDate(String(update.data.date))}
|
||
</time>
|
||
<h1 class="display-md article-title">{update.data.title}</h1>
|
||
<p class="lead article-summary">{update.data.summary}</p>
|
||
<p class="label-sm article-author">
|
||
<span class="author-name">{update.data.author}</span>
|
||
</p>
|
||
</header>
|
||
|
||
<div class="prose">
|
||
<Content />
|
||
</div>
|
||
</article>
|
||
|
||
<nav class="post-nav" aria-label="Post navigation">
|
||
{prev && (
|
||
<a href={`/updates/${prev.slug}`} class="post-nav-link post-nav-prev">
|
||
<span class="label-sm nav-dir">Earlier</span>
|
||
<span class="nav-title body-md">{prev.data.title}</span>
|
||
</a>
|
||
)}
|
||
{next && (
|
||
<a href={`/updates/${next.slug}`} class="post-nav-link post-nav-next">
|
||
<span class="label-sm nav-dir">Later</span>
|
||
<span class="nav-title body-md">{next.data.title}</span>
|
||
</a>
|
||
)}
|
||
</nav>
|
||
|
||
</div>
|
||
</AppLayout>
|
||
|
||
<style>
|
||
.page {
|
||
padding: var(--space-10) var(--space-20) var(--space-16);
|
||
max-width: var(--content-max);
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* ── Breadcrumb ──────────────────────────────────────────────────── */
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
margin-bottom: var(--space-8);
|
||
}
|
||
|
||
.crumb {
|
||
color: var(--on-surface-muted);
|
||
letter-spacing: var(--tracking-wide);
|
||
text-transform: uppercase;
|
||
text-decoration: none;
|
||
border-bottom: none;
|
||
transition: color var(--duration-fast) var(--ease-standard);
|
||
}
|
||
a.crumb:hover {
|
||
color: var(--on-surface-variant);
|
||
border-bottom: none;
|
||
}
|
||
.crumb-sep {
|
||
color: var(--on-surface-muted);
|
||
}
|
||
.crumb-current {
|
||
color: var(--on-surface-variant);
|
||
}
|
||
|
||
/* ── Article ─────────────────────────────────────────────────────── */
|
||
.article {
|
||
max-width: var(--reading-max);
|
||
}
|
||
|
||
.article-header {
|
||
margin-bottom: var(--space-10);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-4);
|
||
}
|
||
|
||
.post-date {
|
||
letter-spacing: var(--tracking-wide);
|
||
text-transform: uppercase;
|
||
color: var(--on-surface-muted);
|
||
}
|
||
|
||
.article-title {
|
||
margin: 0;
|
||
}
|
||
|
||
.article-summary {
|
||
color: var(--on-surface-variant);
|
||
margin: 0;
|
||
}
|
||
|
||
.article-author {
|
||
color: var(--on-surface-muted);
|
||
letter-spacing: var(--tracking-wide);
|
||
text-transform: uppercase;
|
||
margin: 0;
|
||
}
|
||
|
||
.author-name {
|
||
color: var(--secondary);
|
||
}
|
||
|
||
/* ── Prose content ───────────────────────────────────────────────── */
|
||
.prose :global(p) {
|
||
margin: 0 0 var(--space-5);
|
||
color: var(--on-surface-variant);
|
||
line-height: var(--leading-relaxed);
|
||
font-size: var(--text-body-lg);
|
||
}
|
||
|
||
.prose :global(h2) {
|
||
font-family: var(--font-serif);
|
||
font-size: var(--text-headline-md);
|
||
font-weight: 400;
|
||
letter-spacing: var(--tracking-snug);
|
||
color: var(--on-surface);
|
||
margin: var(--space-10) 0 var(--space-4);
|
||
line-height: var(--leading-snug);
|
||
}
|
||
|
||
.prose :global(h3) {
|
||
font-family: var(--font-serif);
|
||
font-size: var(--text-headline-sm);
|
||
font-weight: 400;
|
||
letter-spacing: var(--tracking-snug);
|
||
color: var(--on-surface);
|
||
margin: var(--space-8) 0 var(--space-3);
|
||
line-height: var(--leading-snug);
|
||
}
|
||
|
||
.prose :global(ul),
|
||
.prose :global(ol) {
|
||
padding-left: var(--space-6);
|
||
margin: 0 0 var(--space-5);
|
||
color: var(--on-surface-variant);
|
||
line-height: var(--leading-relaxed);
|
||
font-size: var(--text-body-lg);
|
||
}
|
||
|
||
.prose :global(li) {
|
||
margin-bottom: var(--space-2);
|
||
}
|
||
|
||
.prose :global(strong) {
|
||
font-weight: 600;
|
||
color: var(--on-surface);
|
||
}
|
||
|
||
.prose :global(em) {
|
||
font-style: italic;
|
||
}
|
||
|
||
.prose :global(code) {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.875em;
|
||
background: var(--surface-container);
|
||
padding: 0.15em 0.4em;
|
||
border-radius: var(--radius-sm);
|
||
color: var(--on-surface);
|
||
}
|
||
|
||
.prose :global(hr) {
|
||
border: none;
|
||
border-top: var(--ghost-border);
|
||
margin: var(--space-8) 0;
|
||
}
|
||
|
||
/* ── Post nav ────────────────────────────────────────────────────── */
|
||
.post-nav {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: var(--space-6);
|
||
margin-top: var(--space-12);
|
||
padding-top: var(--space-8);
|
||
border-top: var(--ghost-border);
|
||
max-width: var(--reading-max);
|
||
}
|
||
|
||
.post-nav-link {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
text-decoration: none;
|
||
border-bottom: none;
|
||
max-width: 48%;
|
||
}
|
||
.post-nav-link:hover {
|
||
border-bottom: none;
|
||
}
|
||
.post-nav-next {
|
||
margin-left: auto;
|
||
text-align: right;
|
||
}
|
||
|
||
.nav-dir {
|
||
letter-spacing: var(--tracking-wide);
|
||
text-transform: uppercase;
|
||
color: var(--on-surface-muted);
|
||
}
|
||
|
||
.nav-title {
|
||
color: var(--on-surface-variant);
|
||
transition: color var(--duration-fast) var(--ease-standard);
|
||
}
|
||
.post-nav-link:hover .nav-title {
|
||
color: var(--on-surface);
|
||
}
|
||
</style>
|