-
-
Save maciejpedzich/000da5c6b3a91290d49a91c9fe940ca3 to your computer and use it in GitHub Desktop.
| --- | |
| import type { CollectionEntry } from 'astro:content'; | |
| import { getCollection, render } from 'astro:content'; | |
| // @ - alias for "src" directory | |
| import TableOfContents from '@/components/TableOfContents.astro'; | |
| export async function getStaticPaths() { | |
| const posts = await getCollection('YOUR_COLLECTION_NAME_HERE'); | |
| return posts.map((post) => ({ | |
| params: { slug: post.id }, | |
| props: post, | |
| })); | |
| } | |
| type Props = CollectionEntry<'YOUR_COLLECTION_NAME_HERE'>; | |
| const post = Astro.props; | |
| const { Content, headings } = await render(post); | |
| --- | |
| <h1>{post.data.title}</h1> | |
| <TableOfContents headings={headings} /> | |
| <article> | |
| <Content /> | |
| </article> |
| --- | |
| import type { MarkdownHeading } from 'astro'; | |
| type Props = { | |
| headings: MarkdownHeading[]; | |
| }; | |
| type HeadingWithSubheadings = MarkdownHeading & { | |
| subheadings: MarkdownHeading[]; | |
| }; | |
| const { headings } = Astro.props; | |
| const grouppedHeadings = headings.reduce((array, heading) => { | |
| if (heading.depth === 2) { | |
| array.push({ ...heading, subheadings: [] }); | |
| } else if (heading.depth === 3) { | |
| array.at(-1)?.subheadings.push(heading); | |
| } | |
| return array; | |
| }, [] as HeadingWithSubheadings[]); | |
| --- | |
| <nav id="table-of-contents" aria-label="Table Of Contents"> | |
| <ol> | |
| { | |
| grouppedHeadings.map((h) => ( | |
| <li> | |
| <a href={`#${h.slug}`}>{h.text}</a> | |
| {h.subheadings.length > 0 && ( | |
| <ol> | |
| {h.subheadings.map((sub) => ( | |
| <li> | |
| <a href={`#${sub.slug}`}>{sub.text}</a> | |
| </li> | |
| ))} | |
| </ol> | |
| )} | |
| </li> | |
| )) | |
| } | |
| </ol> | |
| </nav> | |
| <script is:inline> | |
| // This script tag is useful only if you want to display the TOC alongside the blog post... | |
| // ... and highlight the section that the user is currently reading through. | |
| // Feel free to remove this tag if you don't need this type of functionality. | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| for (const entry of entries) { | |
| const headingFragment = `#${entry.target.id}`; | |
| const tocItem = document.querySelector(`a[href="${headingFragment}"]`); | |
| if (entry.isIntersecting) { | |
| const previouslyActivatedItem = | |
| document.querySelector('.active-toc-item'); | |
| previouslyActivatedItem?.classList.remove('active-toc-item'); | |
| tocItem.classList.add('active-toc-item'); | |
| } else { | |
| const isAnyOtherEntryIntersecting = entries.some( | |
| (e) => e.target.id !== entry.target.id && e.isIntersecting | |
| ); | |
| if (isAnyOtherEntryIntersecting) { | |
| tocItem.classList.remove('active-toc-item'); | |
| } | |
| } | |
| } | |
| }, | |
| { root: null, rootMargin: '0px', threshold: [1] } | |
| ); | |
| const sectionHeadings = document.querySelectorAll( | |
| 'article > h2, article > h3' | |
| ); | |
| for (const heading of sectionHeadings) { | |
| observer.observe(heading); | |
| } | |
| </script> | |
| <style> | |
| .active-toc-item { | |
| font-weight: bold; | |
| } | |
| </style> |
Thanks, it works :)
Tip
You can also add this code at the end of the <script> for smooth scrolling to the section:
document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", function (e) { e.preventDefault(); document.querySelector(this.getAttribute("href")).scrollIntoView({ behavior: "smooth", }); }); });
It's a cool solution but when you click it doesn't add the hash at the end of the URL.
You can add this line to fix it history.pushState(null, null, this.getAttribute("href"));
The final code looks like that :
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
history.pushState(null, null, this.getAttribute("href"));
document.querySelector(this.getAttribute("href")).scrollIntoView({
behavior: "smooth",
});
});
});@Delmotte-Vincent thanks for your reminds! It looks greater
Emmm... null is not allowed in the latest version of Astro. better code like:
connectedCallback() {
// Smooth scroll
this.tocLinks.forEach((link) => {
link.element.addEventListener('click', (e) => {
e.preventDefault()
// Push the history to add the hash at the end of the URL
const directHeading = this.headings.find((heading) => heading.id === link.slug)
if (directHeading) {
// Push the history to add the hash at the end of the URL
history.pushState(null, directHeading.textContent || "", this.getAttribute("href"));
directHeading.scrollIntoView({ behavior: 'smooth' });
} else {
console.warn(`No heading found for slug: ${link.slug}`);
}
})
})
// Initial first and listen to scroll event
setInterval(this.updatePositionAndStyle, 100)
window.addEventListener('scroll', this.updatePositionAndStyle)
}which use textContent for param title.
This needs a small update for Astro 5.0 which has changed the way that render works.
For [...slug].astro these are the changes. This is how I have it on my website.
import {
render,
CollectionEntry,
getCollection
} from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection('YOUR_COLLECTION_NAME_HERE');
return posts.map((post) => ({
params: { slug: post.id},
props: entry,
}));
}
interface Props {
entry: CollectionEntry<"YOUR_COLLECTION_NAME_HERE">;
}
const { entry } = Astro.props;
const { Content, headings } = await render(entry);
This needs a small update for Astro 5.0 which has changed the way that render works.
For [...slug].astro these are the changes. This is how I have it on my website.
import { render, CollectionEntry, getCollection } from "astro:content"; export async function getStaticPaths() { const posts = await getCollection('YOUR_COLLECTION_NAME_HERE'); return posts.map((post) => ({ params: { slug: post.id}, props: entry, })); } interface Props { entry: CollectionEntry<"YOUR_COLLECTION_NAME_HERE">; } const { entry } = Astro.props; const { Content, headings } = await render(entry);
Thanks for the heads up! It's been a while since I've last updated this gist, so I'll try and do it tomorrow.
Edit: I've just updated the gist. Thanks once again for pointing out the breaking change.
I update the code as:
It might improve performance in repeatedly obtaining items. And make it into a web component can help to make code structures better. More ts & es features is support.
The full example is 1 , 2 and 3.