Adds table of contents. Removes now from the top level of the sidebar.

This commit is contained in:
2026-05-26 17:10:01 -07:00
parent 5c90096a10
commit bcc2d85560
7 changed files with 105 additions and 6 deletions

View File

@@ -12,7 +12,6 @@ import SocialLinks from './SocialLinks.astro';
<SidebarLink href="/">Home</SidebarLink> <SidebarLink href="/">Home</SidebarLink>
<SidebarLink href="/blog">Blog</SidebarLink> <SidebarLink href="/blog">Blog</SidebarLink>
<SidebarLink href="/about">About</SidebarLink> <SidebarLink href="/about">About</SidebarLink>
<SidebarLink href="/now">Now</SidebarLink>
<NotesLinks /> <NotesLinks />
</div> </div>
<div class="social-links"> <div class="social-links">

View File

@@ -0,0 +1,31 @@
---
import TableOfContentsTree from "./TableOfContentsTree.astro";
import type { MarkdownHeading } from "astro";
import { makeHeadingsTree, removeEmptyLevels } from "../lib/headings";
type Props = {
headings: Array<MarkdownHeading>;
};
const {headings} = Astro.props;
const headingsTree = removeEmptyLevels(makeHeadingsTree(headings));
---
<div class="table-of-contents">
<TableOfContentsTree headingsTree={headingsTree}></TableOfContentsTree>
</div>
<style>
.table-of-contents {
border: 1px solid var(--color-space-blue-light);
ul {
list-style: none;
}
& > ul {
padding: 0;
margin: 8px 16px;
}
}
</style>

View File

@@ -0,0 +1,15 @@
---
type Props = {
headingsTree: HeadingsTree;
};
const {headingsTree} = Astro.props;
---
<ul>
{headingsTree.map(node =>
Array.isArray(node)
? <Astro.self headingsTree={node} />
: <li><a href={`#${node.slug}`}>{node.text}</a></li>
)}
</ul>

View File

@@ -1,13 +1,15 @@
--- ---
import type { MarkdownHeading } from "astro";
import type { CollectionEntry } from 'astro:content'; import type { CollectionEntry } from 'astro:content';
import RootLayout from '../layouts/RootLayout.astro'; import RootLayout from '../layouts/RootLayout.astro';
import FormattedDate from '../components/FormattedDate.astro'; import FormattedDate from '../components/FormattedDate.astro';
import NotchedBox from '../components/NotchedBox.astro'; import NotchedBox from '../components/NotchedBox.astro';
import TableOfContents from '../components/TableOfContents.astro';
import { Image } from 'astro:assets'; import { Image } from 'astro:assets';
type Props = CollectionEntry<'blog'>['data'] & { slug: string; }; type Props = CollectionEntry<'blog'>['data'] & { slug: string; headings: MarkdownHeading[] };
const { slug, title, description, pubDate, updatedDate, heroImage, tags } = Astro.props; const { slug, title, description, pubDate, updatedDate, heroImage, tags, headings } = Astro.props;
--- ---
<style> <style>
@@ -77,6 +79,7 @@ const { slug, title, description, pubDate, updatedDate, heroImage, tags } = Astr
</div> </div>
} }
<hr /> <hr />
<TableOfContents headings={headings} />
<div class="e-content"> <div class="e-content">
<slot /> <slot />
</div> </div>

49
src/lib/headings.ts Normal file
View File

@@ -0,0 +1,49 @@
import type { MarkdownHeading } from "astro";
export type HeadingsTree = Array<MarkdownHeading | HeadingsTree>;
// could take a list like [5, 1, 2, 2, 3, 5, 1] and should return
// [ [ [ [ [ 5 ] ] ] ], 1, [ 2, 2, [3, [[5]]]], 1]
export function makeHeadingsTree(headings: MarkdownHeading[]): HeadingsTree {
let index = 0;
let currDepth = 1;
const treeStack = [[]] as Array<HeadingsTree>;
while (index < headings.length) {
const currHeading = headings[index];
// If the next heading is at the current depth, push it into the tree
if (currHeading.depth == currDepth) {
treeStack[treeStack.length - 1].push(currHeading);
index += 1;
}
// If the next heading is deeper, go deeper
if (currHeading.depth > currDepth) {
const nextLevel = [] as HeadingsTree;
treeStack[treeStack.length - 1].push(nextLevel);
treeStack.push(nextLevel);
currDepth += 1;
}
// If the next heading is shallower, back out
if (currHeading.depth < currDepth) {
treeStack.pop();
currDepth -= 1;
}
}
return treeStack[0];
}
export function removeEmptyLevels(tree: HeadingsTree): HeadingsTree {
const newLevel = tree.map((node) =>
Array.isArray(node) ? removeEmptyLevels(node) : node,
);
if (tree.every(Array.isArray)) {
return tree.flatMap((node) => node);
}
return tree;
}

View File

@@ -14,9 +14,9 @@ type Props = CollectionEntry<'blog'>;
const post = Astro.props; const post = Astro.props;
const { slug } = Astro.params; const { slug } = Astro.params;
const { Content } = await render(post); const { Content, headings } = await render(post);
--- ---
<BlogPost slug={slug} {...post.data} > <BlogPost slug={slug} headings={headings} {...post.data} >
<Content /> <Content />
</BlogPost> </BlogPost>

View File

@@ -2,6 +2,7 @@
import RootLayout from '../../layouts/RootLayout.astro'; import RootLayout from '../../layouts/RootLayout.astro';
import NotchedBox from '../../components/NotchedBox.astro'; import NotchedBox from '../../components/NotchedBox.astro';
import FormattedDate from '../../components/FormattedDate.astro'; import FormattedDate from '../../components/FormattedDate.astro';
import TableOfContents from '../../components/TableOfContents.astro';
import { type CollectionEntry, getCollection } from 'astro:content'; import { type CollectionEntry, getCollection } from 'astro:content';
import { render } from 'astro:content'; import { render } from 'astro:content';
@@ -15,7 +16,7 @@ export async function getStaticPaths() {
type Props = CollectionEntry<'notes'>; type Props = CollectionEntry<'notes'>;
const note = Astro.props; const note = Astro.props;
const { Content } = await render(note); const { Content, headings } = await render(note);
--- ---
<RootLayout> <RootLayout>
@@ -24,6 +25,7 @@ const { Content } = await render(note);
<div class="last-updated"> <div class="last-updated">
Last updated <FormattedDate date={note.data.updated} /> Last updated <FormattedDate date={note.data.updated} />
</div> </div>
<TableOfContents headings={headings}/>
<Content /> <Content />
</NotchedBox> </NotchedBox>
</RootLayout> </RootLayout>