zfb
GitHub repository

Type to search...

to open search from anywhere

Heading links

Core
Created Jun 24, 2026Takeshi Takatsudo

Automatically adds slug-based id attributes and anchor links to every heading.

HeadingLinksPlugin is always active. It slugifies every <h2><h6> and injects a self-referencing anchor link so readers can copy a deep-link to any section.

Behaviour

For each heading the plugin:

  1. Computes a GitHub-slugger-compatible slug from the heading text.

  2. Deduplicates repeated slugs within the same document by appending a counter (overview, overview-1, overview-2, …).

  3. Sets the id attribute on the <h*> element.

  4. Appends an empty <a href="#slug" class="hash-link" aria-label="…"> as the heading's last child. The heading text is left unwrapped. The visible # glyph is rendered via CSS ::after so the anchor body stays empty, keeping heading-text extraction (e.g. for TOC) clean.

Example

## Introduction

## Introduction

Produces:

<h2 id="introduction">Introduction<a href="#introduction" class="hash-link" aria-label="Direct link to Introduction"></a></h2>
<h2 id="introduction-1">Introduction<a href="#introduction-1" class="hash-link" aria-label="Direct link to Introduction"></a></h2>

Config

The plugin itself is always on. The ID strategy is configurable via markdown.features.headingIds:

// zfb.config.ts
export default {
  markdown: {
    features: {
      headingIds: { strategy: "hierarchical" },
    },
  },
};

strategy: "flat" (default)

The behaviour described above: GitHub-slugger slugs with one dedup counter shared across h2h6. Omitting headingIds entirely keeps this scheme.

strategy: "hierarchical"

Each heading's ID is prefixed with its ancestor chain, joined by -:

## Foo

### Moo

#### Mew

Produces id="foo", id="foo-moo", id="foo-moo-mew" (instead of the flat foo, moo, mew). The in-heading hash-link anchors, the headings export, TOC export, and link validation all follow the same IDs.

Details:

  • A duplicated full path still gets the dedup counter (a-b, a-b-1).

  • A deduplicated parent contributes its final ID to children: the second ## Foo is foo-1, so its ### Bar becomes foo-1-bar.

  • Hierarchical anchors are reconstructible from the heading outline and collide far less often than flat slugs, at the cost of longer URLs.

Warning

Switching strategies is anchor-breaking: existing deep links to nested headings (#moo) stop resolving once IDs become #foo-moo.

Ordering note

HeadingLinksPlugin runs first in the hast phase. Plugins that depend on stable heading id values — such as TocPlugin (opt-in heading-marker-toc) and TocExportPlugin (opt-in toc-export) — must run after it.

See also

  • Heading-marker TOC — opt-in TOC insertion that reads the id attributes this plugin produces.

  • TOC export — opt-in structured TOC data export.

Revision History

Takeshi TakatsudoCreated: 2026-06-25T05:17:25+09:00Updated: 2026-06-25T05:17:25+09:00

AI Assistant

Ask a question about the documentation.