feat: working table of contents

This commit is contained in:
grassblock 2025-06-07 17:53:46 +08:00
parent 29b1fa4afb
commit 856a8f2795
5 changed files with 98 additions and 34 deletions

View file

@ -0,0 +1,92 @@
---
import type { MarkdownHeading } from 'astro';
interface Props {
headings: MarkdownHeading[];
}
const { headings } = Astro.props;
const filteredHeadings = headings.filter((heading) => heading.depth <= 3);
// github.com/rezahedi/rezahedi.dev/blob/main/src/components/TOC.astro
function buildHierarchy(headings: MarkdownHeading[]) {
const toc: any[] = [];
const parentHeadings = new Map();
if (!headings)
return toc;
headings.forEach((h: any) => {
const heading = { ...h, subheadings: [] };
parentHeadings.set(heading.depth, heading);
// Change 2 to 1 if your markdown includes your <h1>
if (heading.depth === 2) {
toc.push(heading);
} else {
parentHeadings.get(heading.depth - 1)?.subheadings.push(heading);
}
});
return toc;
}
const toc = buildHierarchy(filteredHeadings);
---
<details>
<summary>Table of Contents</summary>
<ul class="toc-list">
{
toc.map((heading) => (
<li class={`depth-${heading.depth}`} style={`margin-left: ${(heading.depth - 1)}rem;`}>
<a href={`#${heading.slug}`}>{heading.text}</a>
{
heading.subheadings.length > 0 && (
<ul>
{
heading.subheadings.map((subheading) => (
<li class={`depth-${subheading.depth}`} style={`margin-left: ${(subheading.depth - 1)}rem;`}>
<a href={`#${subheading.slug}`}>{subheading.text}</a>
{subheading.subheadings.length > 0 && (
<ul>
{
subheading.subheadings.map((subSubheading) => (
<li class={`depth-${subSubheading.depth}`} style={`margin-left: ${(subSubheading.depth - 1)}rem;`}>
<a href={`#${subSubheading.slug}`}>{subSubheading.text}</a>
</li>
))
}
</ul>
)}
</li>
))
}
</ul>
)
}
</li>
))
}
</ul>
</details>
<style>
details {
margin: 0.5rem auto;
}
.toc-list {
list-style-type: none;
padding-left: 0;
}
.depth-1 {
font-weight: bold;
margin-top: 0.5rem;
}
.depth-2 {
font-weight: 500;
}
.depth-3 {
font-size: 0.95em;
}
</style>

View file

@ -1,5 +1,6 @@
---
import Layout from '../../layouts/Layout.astro';
import { getCollection, getEntry } from 'astro:content';
import Comments from "../../components/Comments.astro";
import {getImage} from "astro:assets";
@ -7,6 +8,7 @@ import {siteConfig} from "../../config";
import ReplyViaEmail from "../../components/ReplyViaEmail.astro";
import { ExtractFirstImage } from '../../plugins/extract-images';
import AuthorInfo from "../../components/helper/authors/Info.astro";
import TableOfContents from "../../components/TableOfContents.astro";
export async function getStaticPaths() {
const blogEntries = await getCollection('posts');
@ -17,6 +19,8 @@ export async function getStaticPaths() {
const { entry } = Astro.props;
const { Content } = await entry.render();
const headings = await entry.render().then(rendered => rendered.headings);
const noscript = siteConfig.noClientJavaScript
const slug = Astro.params.slug;
const author = entry.data.author || {collection: 'authors', id: siteConfig.defaultAuthor.id};
@ -48,6 +52,7 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
<h1 class="title">{entry.data.title}</h1>
<AuthorInfo data={authorData} />
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
{headings.length !== 0 && <TableOfContents headings={headings} />}
<div class="content">
<Content />
</div>