npm.io
1.1.2 • Published 5h ago

astro-better-toc

Licence
MIT
Version
1.1.2
Deps
1
Size
24 kB
Vulns
0
Weekly
0
Stars
1

astro-better-toc

Astro integration + component that generates a full "On This Page" table-of-contents, server-side, with active-link scroll spy at runtime.

TOC entries are built from the final rendered HTML rather than from MDX frontmatter or the AST, so headings produced by imported components, conditional blocks, or non-MDX pages are all captured correctly. No layout shift, no client-side generation.

Installation

npm install astro-better-toc

Setup

1. Register the integration
// astro.config.ts
import { defineConfig } from 'astro/config';
import astroToc from 'astro-better-toc';

export default defineConfig({
  integrations: [astroToc()],
});
2. Add the <TOC> component to your layout

Place it inside an element with data-toc-aside. The component hides that ancestor automatically when the page has no headings to list.

---
// layouts/Base.astro
import TOC from 'astro-better-toc/TOC.astro';
---

<aside data-toc-aside>
  <p>On this page</p>
  <TOC />
</aside>

The <TOC> component renders the <nav> placeholder that the integration fills, and includes the scroll spy script that highlights the active entry as the user scrolls.

3. Mark pages that need a TOC

The integration only processes pages that contain the data-server-toc placeholder rendered by <TOC>. Because <TOC> always renders the placeholder, any page that uses a layout containing <TOC> will get a TOC automatically.

To skip the TOC on a specific page, omit <TOC> from that page's layout slot, or wrap it in a condition:

{!frontmatter.disableTOC && <TOC />}

Component props

Prop Type Default Description
maxDepth number 4 Deepest heading level to include (2 = h2 only, 4 = h2–h4).
class string — CSS classes applied to the <nav> container.
<TOC maxDepth={3} class="overflow-y-auto max-h-screen" />

Integration options

astroToc({
  // CSS selector(s) for the content area to scan for headings.
  // The first selector that matches an element is used; falls back to
  // the full document if none match.
  // Default: ['article', 'main']
  articleSelector: ['article.my-content', 'main'],
})

articleSelector accepts a string or an array of strings tried in order.

Page structure requirements

The integration scans for h2–h6 elements (and [data-toc-type="api"] markers, see below) inside the element matched by articleSelector. Headings outside that element — navigation, footers, sidebars — are ignored.

A minimal layout that works with the default ['article', 'main'] selector:

<body>
  <article>
    <!-- page content with headings -->
  </article>

  <aside data-toc-aside>
    <TOC />
  </aside>
</body>

Duplicate heading IDs

When the same heading text appears more than once (e.g. "Request Headers" in multiple API sections), the integration deduplicates the IDs in the output HTML — request-headers, request-headers-1, request-headers-2 — and generates matching unique TOC links, so each entry navigates to the right place.

API endpoint markers

If your content includes [data-toc-type="api"] elements, they appear in the TOC as children of the preceding heading, rendered with a coloured HTTP method badge:

<!-- in page content -->
<div
  data-toc-type="api"
  data-toc-method="GET"
  data-toc-text="/api/users"
  id="get-api-users"
></div>
<!-- generated TOC entry -->
<a href="#get-api-users" class="block font-mono text-xs ...">
  <span class="font-bold text-blue-600">GET</span>
  <span>/api/users</span>
</a>

Controlling depth

Pass maxDepth to <TOC> or set data-max-depth directly on the placeholder:

<TOC maxDepth={3} />
<!-- equivalent raw HTML -->
<nav id="toc-container" data-server-toc data-max-depth="3"></nav>

Default is 4 (h2–h4 and API markers one level below their enclosing heading).

Generated markup

The TOC is a nested <ul> list. Active entries (managed by the scroll spy) receive an active class on their [data-widget="scroll-spy-item"] wrapper:

<ul id="toc-list" class="space-y-3 pt-4" data-widget="scroll-spy">
  <li>
    <div class="group" data-widget="scroll-spy-item">
      <a href="#overview" class="block font-medium text-slate-600 text-sm ...">Overview</a>
    </div>
    <ul class="space-y-2 ml-4 pt-2" data-widget="scroll-spy">
      <li>
        <div class="group" data-widget="scroll-spy-item">
          <a href="#details" class="...">Details</a>
        </div>
      </li>
    </ul>
  </li>
</ul>

The classes on <a> and <ul> elements are Tailwind utility classes. If you are not using Tailwind, you can target the data-widget attributes for styling instead.

Keywords