Skip to content
A Astro Rocket
astro-rocket features blog navigation

Table of Contents — Reading Anchors for Long Posts

Astro Rocket renders an optional TOC on blog posts in three layouts — inline card, sticky sidebar, or both — with the sidebar on the left or right. Pick what fits your audience.

H

Hans Martens

3 min read

Long-form articles benefit from a table of contents. It tells readers what’s coming, lets them jump straight to the section they want, and acts as a quiet progress indicator when scroll-spy highlights the active section.

Astro Rocket ships a built-in TOC component for blog posts with three layout options — pick the one that fits your audience. Every option is opt-in: when the TOC is disabled, no extra HTML and no extra JavaScript reach the page.

The three layouts

LayoutMobile / tablet (< xl)Desktop (xl+, ≥1280 px)
'inline'Card at the top of the articleCard at the top of the article
'sidebar'TOC hiddenSticky sidebar next to the article
'auto'Card at the top of the articleSticky sidebar next to the article

Inline keeps your full reading width on every viewport — best when you want articles to read like a book and don’t need the TOC to follow the reader. Mine started this way.

Sidebar is the classic docs-style layout — a sticky list that follows the reader on desktop and stays out of the way on phones. Hidden below the xl breakpoint so it doesn’t compete with mobile reading.

Auto combines both: phone and tablet readers get the inline card at the top, desktop readers get the sticky sidebar. This is what this site uses — try resizing the browser on any blog post.

In all three, the article column stays at max-w-4xl so reading width never changes when the sidebar appears or disappears.

Left or right?

Whenever the sidebar is active ('sidebar' or 'auto'), you can pick which side it sits on with the sidebarPosition setting. 'left' puts the TOC before the article — handy when you want the navigation to feel like a docs site or to mirror your main nav. 'right' keeps it after the article, which is the more common blog convention. The article column stays the same width either way; only the column order changes.

How to enable

Open src/config/site.config.ts and find the articleFeatures.toc block:

articleFeatures: {
  toc: {
    enabled: true,             // turn the TOC on site-wide
    layout: 'auto',            // 'inline' | 'sidebar' | 'auto'
    sidebarPosition: 'left',   // 'left' | 'right' (sidebar layouts only)
    minHeadings: 3,            // hide TOC on posts with fewer than 3 headings
    maxDepth: 3,               // include H2 + H3 (set to 2 for H2 only)
  },
},

Save, build, and every blog post with three or more headings now shows a TOC in the layout — and on the side — you chose. sidebarPosition defaults to 'right' if you omit it, and is ignored when layout is 'inline'.

How to disable

If you’ve decided you’d rather not have a TOC at all, set enabled to false:

articleFeatures: {
  toc: {
    enabled: false,            // ← TOC fully off
    layout: 'inline',
    sidebarPosition: 'right',
    minHeadings: 3,
    maxDepth: 3,
  },
},

When disabled, the TableOfContents component renders nothing, no scroll-spy script reaches the page, and the article column stays at its original max-w-4xl layout. Performance is identical to a theme without the feature.

Hide on a single post

Sometimes a post is too short for a TOC, or it’s an announcement that doesn’t need section anchors. Override on a per-post basis with frontmatter:

---
title: My short post
toc: false
---

This hides the TOC on just that post — your site-wide setting stays untouched.

You can also force-hide in the other direction: if you’ve enabled the TOC site-wide but a specific evergreen post has its own custom navigation, toc: false lets you opt that one post out without changing your config.

Switching layouts later

Switching is a one-line change. If you start with 'inline' and decide later you want a sidebar:

layout: 'sidebar',           // or 'auto'
sidebarPosition: 'left',     // or 'right'

Save and rebuild — every existing blog post picks up the new layout automatically. No frontmatter migration, no per-post tweaks. Flipping the sidebar from one side to the other is the same one-line change to sidebarPosition.

How it works under the hood

The TOC is generated from the headings Astro extracts from your MDX file. There’s no extra plugin or library — it uses the headings array that Astro already returns from render(post).

Each heading already has an id (Astro’s MDX integration auto-generates them via slugified text), so the TOC just turns those into anchor links. No external dependencies.

For auto, both an inline card and a sidebar nav are rendered on every post, with CSS xl:hidden and hidden xl:block swapping which one is visible. Scroll-spy runs independently on whichever is currently in view, using IntersectionObserver with a tuned rootMargin so a heading becomes “active” when it scrolls into the upper portion of the viewport, not when it’s already past the bottom.

Left vs. right is a pure layout swap: the sidebar <aside> is rendered before or after the article in the DOM, and the grid template flips between [15rem_minmax(0,1fr)] and [minmax(0,1fr)_15rem]. No extra CSS, no extra JS — and the same scroll-spy script runs in either direction.

What it costs (close to nothing)

ScenarioCost
TOC disabled (enabled: false)0 KB, 0 JS — exactly as if the feature didn’t exist
TOC enabled, post has fewer than minHeadings0 KB — component renders nothing
TOC enabled ('inline'), post qualifies~1 KB HTML + ~1 KB inline JS for scroll-spy
TOC enabled ('sidebar' or 'auto'), post qualifiesSame ~2 KB total — auto reuses the same script for both layouts

There’s no third-party script, no external request, and no cumulative layout shift. The component is server-rendered HTML with one small inline script.

Customising the look

The TOC uses the standard theme tokens — --color-foreground, --color-foreground-muted, --color-brand-500, --color-border. Switch your colour theme and the TOC adapts. No hard-coded colours.

If you want a different title above the list, the component accepts a title prop. The default is “On this page” but you can override it inside BlogLayout.astro if your audience speaks something other than English.

Where it lives

  • Component: src/components/blog/TableOfContents.astro
  • Wiring: src/layouts/BlogLayout.astro
  • Config: src/config/site.config.tsarticleFeatures.toc
  • Per-post override: toc: false in MDX frontmatter

That’s the whole feature. Three layouts, a left-or-right sidebar, one config block, an optional per-post override — and a scroll-spy that just works.

Back to Blog
Share:

Related Posts

Comments on Blog Posts — Giscus, Lazy-Loaded

Astro Rocket now has optional comments on blog posts, powered by Giscus and GitHub Discussions. The script is lazy-loaded so readers who don't scroll to the bottom pay zero cost.

H Hans Martens
2 min read
astro-rocket features blog comments giscus

Independent Footer Menu — Different Links in Header and Footer

Astro Rocket now lets you configure the footer menu independently of the header navigation. Add a Privacy link, an Imprint, or a Cookie Policy without cluttering your main nav.

H Hans Martens
2 min read
astro-rocket features footer navigation

Hero Scroll Indicator — Desktop-Only, Hides on Scroll

Astro Rocket's hero has an animated scroll indicator: two bouncing chevrons that fade in after the hero animation and disappear the moment you start scrolling. Here's how every part of it works.

H Hans Martens
2 min read
astro-rocket features ux animation

Follow along

Stay in the loop — new articles, thoughts, and updates.