mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-18 01:12:53 +00:00
Writings detail pages now show the table of contents as a sticky sidebar once there's room (>=74rem viewport), sharing its markup with the existing inline TOC via a new TocList.astro so neither copy drifts. Below that breakpoint it falls back to the original inline list. Bumped .container--wide 64rem -> 76rem (home, bookshelf, and now writings detail) so big monitors use more of the screen. The prose column itself stays capped at 42rem for readability; the extra room is what makes space for the TOC gutter. Verified at 390/900/1180/1920px.
157 lines
4.2 KiB
Plaintext
157 lines
4.2 KiB
Plaintext
---
|
|
import { getCollection, render } from "astro:content";
|
|
import TocList from "../../components/TocList.astro";
|
|
import Base from "../../layouts/Base.astro";
|
|
|
|
export async function getStaticPaths() {
|
|
const writings = await getCollection("writings", ({ data }) => !data.draft);
|
|
return writings.map((entry) => ({
|
|
params: { slug: entry.id },
|
|
props: { entry },
|
|
}));
|
|
}
|
|
|
|
const { entry } = Astro.props;
|
|
const { Content, headings } = await render(entry);
|
|
|
|
const allWritings = (
|
|
await getCollection("writings", ({ data }) => !data.draft)
|
|
).sort((a, b) => a.data.date.valueOf() - b.data.date.valueOf());
|
|
const index = allWritings.findIndex((w) => w.id === entry.id);
|
|
const prev = index > 0 ? allWritings[index - 1] : undefined;
|
|
const next =
|
|
index < allWritings.length - 1 ? allWritings[index + 1] : undefined;
|
|
|
|
const series = entry.data.extra?.series;
|
|
const seriesEntries = series
|
|
? allWritings
|
|
.filter((w) => w.data.extra?.series === series)
|
|
.sort(
|
|
(a, b) =>
|
|
(a.data.extra?.series_index ?? 0) - (b.data.extra?.series_index ?? 0),
|
|
)
|
|
: [];
|
|
|
|
const tags = entry.data.taxonomies?.tags ?? [];
|
|
const related = tags.length
|
|
? allWritings
|
|
.filter((w) => w.id !== entry.id)
|
|
.map((w) => ({
|
|
entry: w,
|
|
overlap: (w.data.taxonomies?.tags ?? []).filter((t) => tags.includes(t))
|
|
.length,
|
|
}))
|
|
.filter((w) => w.overlap > 0)
|
|
.sort((a, b) => b.overlap - a.overlap)
|
|
.slice(0, 3)
|
|
.map((w) => w.entry)
|
|
: [];
|
|
|
|
const description =
|
|
entry.data.extra?.llms_description ?? entry.data.description;
|
|
const tocHeadings = headings.filter((h) => h.depth <= 3);
|
|
---
|
|
|
|
<Base title={entry.data.title} description={description} wide ogImage={`/images/og/writings/${entry.id}.png`}>
|
|
<div class="article-layout">
|
|
<div class="article-main">
|
|
<h1>{entry.data.title}</h1>
|
|
<p class="meta">
|
|
{entry.data.date.toDateString()}
|
|
{entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read</>}
|
|
{entry.data.updated && <> · updated {entry.data.updated.toDateString()}</>}
|
|
</p>
|
|
{tags.map((tag) => <a class="tag" href={`/tags/${tag}/`}>#{tag}</a>)}
|
|
|
|
{
|
|
series && (
|
|
<nav class="series-nav" aria-label={`Post series: ${series}`}>
|
|
<p class="meta">
|
|
Series: {series} ({(entry.data.extra?.series_index ?? 0)} / {seriesEntries.length})
|
|
</p>
|
|
<ol>
|
|
{seriesEntries.map((w) => (
|
|
<li aria-current={w.id === entry.id ? "page" : undefined}>
|
|
<a href={`/writings/${w.id}/`}>{w.data.title}</a>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
{
|
|
tocHeadings.length > 0 && (
|
|
<nav class="toc toc-inline" aria-label="Table of contents">
|
|
<TocList headings={tocHeadings} />
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
<article>
|
|
<Content />
|
|
</article>
|
|
|
|
<nav class="post-nav" aria-label="Post navigation">
|
|
{prev && <a href={`/writings/${prev.id}/`}>← {prev.data.title}</a>}
|
|
{next && <a href={`/writings/${next.id}/`}>{next.data.title} →</a>}
|
|
</nav>
|
|
|
|
{
|
|
related.length > 0 && (
|
|
<section>
|
|
<h2>Related Posts</h2>
|
|
{related.map((w) => (
|
|
<div class="card">
|
|
<h3>
|
|
<a href={`/writings/${w.id}/`}>{w.data.title}</a>
|
|
</h3>
|
|
</div>
|
|
))}
|
|
</section>
|
|
)
|
|
}
|
|
</div>
|
|
|
|
{
|
|
tocHeadings.length > 0 && (
|
|
<nav class="toc toc-sidebar" aria-label="Table of contents">
|
|
<TocList headings={tocHeadings} />
|
|
</nav>
|
|
)
|
|
}
|
|
</div>
|
|
</Base>
|
|
|
|
<style>
|
|
.article-layout {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 42rem);
|
|
justify-content: center;
|
|
align-items: start;
|
|
gap: var(--space-7);
|
|
}
|
|
|
|
.toc-sidebar {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 74rem) {
|
|
.article-layout {
|
|
grid-template-columns: minmax(0, 42rem) 16rem;
|
|
}
|
|
|
|
.toc-inline {
|
|
display: none;
|
|
}
|
|
|
|
.toc-sidebar {
|
|
display: block;
|
|
position: sticky;
|
|
top: var(--space-6);
|
|
max-height: calc(100vh - var(--space-7));
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
</style>
|