Chapterize JSv0.1.0
On this page Every framework

Frameworks · one recipe, everywhere

Every framework.
No adapters.

Chapterize JS is plain DOM, so there is no React package or Vue plugin to keep in sync. Render a nav, call init() once on the client, and the DOM watching does the rest.

How it fits #

Recipes #

Chapterize JS is plain DOM, so the recipe is the same everywhere: render a nav with data-chapter, and call init() once on the client after the first render. init() is idempotent and keeps watching the DOM, so route changes, re-rendered headings and navs that mount later are picked up without more code. When a nav leaves the DOM its instance is destroyed for you.

Plain HTML #

HTML
<nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker></nav>
<article>…</article>
<script src="https://unpkg.com/chapterize-js" defer></script>

React #

Call init() in an effect, after hydration, so the built list never causes a hydration mismatch. Strict Mode's double effect is harmless.

JSX
import { useEffect } from 'react';
import { init } from 'chapterize-js';

export function Toc() {
  useEffect(() => { init(); }, []);
  return <nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker />;
}

Next.js #

App Router: put the nav in any Server Component and render one small Client Component that calls init(), once, in a layout. Pages stay static, the nav ships no JavaScript of its own, and on client-side navigation (next/link) the DOM watching rebuilds the outline from the new page.

app/chapter-init.tsx
'use client';
import { useEffect } from 'react';
import { init } from 'chapterize-js';

export default function ChapterInit() {
  useEffect(() => { init(); }, []);
  return null;
}
app/layout.tsx
import ChapterInit from './chapter-init';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <ChapterInit />
      </body>
    </html>
  );
}
app/docs/page.tsx
export default function Page() {
  return (
    <>
      <nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker />
      <article>{/* headings and content */}</article>
    </>
  );
}

Vue #

Vue
<script setup>
import { onMounted } from 'vue';
import { init } from 'chapterize-js';
onMounted(() => init());
</script>

<template>
  <nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker></nav>
</template>

Nuxt #

A client plugin that waits for the app to mount (so hydration sees the server markup first).

JavaScript
// plugins/toc.client.js
import { init } from 'chapterize-js';
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:mounted', () => init());
});

Svelte / SvelteKit #

Svelte
<script>
  import { onMount } from 'svelte';
  import { init } from 'chapterize-js';
  onMount(() => init());
</script>

<nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker></nav>

Angular #

afterNextRender runs only in the browser, after the view is in the DOM. Links work with <base href>.

TypeScript
import { Component, afterNextRender } from '@angular/core';
import { init } from 'chapterize-js';

@Component({
  selector: 'app-toc',
  template: `<nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker></nav>`,
})
export class TocComponent {
  constructor() { afterNextRender(() => init()); }
}

Astro #

A bundled script runs once per page; with <ClientRouter /> the DOM watching follows each swap.

Astro
<nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker></nav>
<script>
  import 'chapterize-js/auto';
</script>

Solid #

JSX
import { onMount } from 'solid-js';
import { init } from 'chapterize-js';

export function Toc() {
  onMount(() => init());
  return <nav data-chapter data-chapter-build="article h2, article h3" data-chapter-marker />;
}

WordPress #

Enqueue the global build in the theme, and put the nav in a template part or a Custom HTML block.

PHP
add_action('wp_enqueue_scripts', function () {
  wp_enqueue_script('chapterize-js', 'https://unpkg.com/chapterize-js', [], null, ['strategy' => 'defer']);
});
HTML
<nav data-chapter data-chapter-build=".entry-content h2, .wp-block-post-content h2" data-chapter-marker></nav>

Hugo #

Hugo's headings already have ids, which are kept. Build the outline, or spy Hugo's own .TableOfContents.

HTML
<nav data-chapter data-chapter-build=".content h2, .content h3" data-chapter-marker></nav>
<!-- or: <div data-chapter data-chapter-marker>{{ .TableOfContents }}</div> -->
<script src="https://unpkg.com/chapterize-js" defer></script>

Eleventy #

In the layout that wraps your Markdown:

HTML
<nav data-chapter data-chapter-build="main h2, main h3" data-chapter-marker></nav>
<main>{{ content | safe }}</main>
<script src="https://unpkg.com/chapterize-js" defer></script>

Drupal #

Declare a library in the theme, attach it, and add the nav to the node template.

yaml
# mytheme.libraries.yml
toc:
  js:
    https://unpkg.com/chapterize-js: { type: external, minified: true, attributes: { defer: true } }
HTML
{{ attach_library('mytheme/toc') }}
<nav data-chapter data-chapter-build=".node__content h2, .node__content h3" data-chapter-marker></nav>