mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-18 01:12:53 +00:00
Add sticky sidebar TOC and widen the page for large screens
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.
This commit is contained in:
@@ -104,9 +104,13 @@ approved plan for full rationale on each decision referenced below.
|
||||
|
||||
- [x] Series-nav (multi-part post series) — `writings/[...slug].astro`,
|
||||
uses the existing `extra.series`/`extra.series_index` fields
|
||||
- [x] Table of contents — inline only, no separate sidebar layout yet (that's
|
||||
a design-system concern); heading ids + `headings` metadata now flow
|
||||
through `toml-content-loader.ts` so `render(entry)` exposes them
|
||||
- [x] Table of contents — sticky sidebar on desktop (>=74rem), falls back to
|
||||
the original inline list below the tags on narrower viewports; same
|
||||
heading data rendered via a shared `TocList.astro` so the two aren't
|
||||
duplicated markup by hand. `.container--wide` bumped 64rem → 76rem so
|
||||
big monitors use more of the screen (prose column itself stays capped
|
||||
at 42rem for readability — the extra width is the TOC gutter);
|
||||
verified at 390/900/1180/1920px with no distortion at any width
|
||||
- [x] Prev/next post navigation
|
||||
- [x] Related posts (tag-based, tag-overlap ranked, limit 3)
|
||||
- [x] Per-tag descriptions on tag archive pages — `/tags/` + `/tags/[tag]/`,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
interface Heading {
|
||||
depth: number;
|
||||
slug: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
headings: Heading[];
|
||||
}
|
||||
|
||||
const { headings } = Astro.props;
|
||||
---
|
||||
|
||||
<p class="meta">Table of Contents</p>
|
||||
<ul>
|
||||
{
|
||||
headings.map((h) => (
|
||||
<li style={{ marginLeft: `${(h.depth - 2) * 1}rem` }}>
|
||||
<a href={`#${h.slug}`}>{h.text}</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import { getCollection, render } from "astro:content";
|
||||
import TocList from "../../components/TocList.astro";
|
||||
import Base from "../../layouts/Base.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
@@ -51,68 +52,105 @@ const description =
|
||||
const tocHeadings = headings.filter((h) => h.depth <= 3);
|
||||
---
|
||||
|
||||
<Base title={entry.data.title} description={description} ogImage={`/images/og/writings/${entry.id}.png`}>
|
||||
<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>)}
|
||||
<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>
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
tocHeadings.length > 0 && (
|
||||
<nav class="toc" aria-label="Table of contents">
|
||||
<p class="meta">Table of Contents</p>
|
||||
<ul>
|
||||
{tocHeadings.map((h) => (
|
||||
<li style={{ marginLeft: `${(h.depth - 2) * 1}rem` }}>
|
||||
<a href={`#${h.slug}`}>{h.text}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
{
|
||||
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>
|
||||
|
||||
@@ -152,7 +152,41 @@ pre {
|
||||
}
|
||||
|
||||
.container--wide {
|
||||
max-width: 64rem;
|
||||
max-width: 76rem;
|
||||
}
|
||||
|
||||
.toc {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.toc p.meta {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.toc-sidebar {
|
||||
padding-top: var(--space-1);
|
||||
border-left: 1px solid var(--border);
|
||||
padding-left: var(--space-4);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
|
||||
Reference in New Issue
Block a user