TOCflow

A free, lightweight, server-rendered Table of Contents block for the WordPress block editor. Add one block and it auto-builds an accessible, SEO-friendly linked outline from your post's headings.

Version 0.1.0 WordPress 6.4+ PHP 7.4+ GPL-2.0-or-later Gutenberg block

What TOCflow does

One focused block — Table of Contents — that turns the headings in your post into a clean, clickable outline. No shortcodes, no settings to wrangle.

The outline is rendered on the server, so it's present in the page's initial HTML. That's good for SEO and screen readers, and there's no heavy JavaScript on the front end. TOCflow also adds matching anchor IDs to your headings automatically, so every link scrolls to the right place.

Automatic

Builds itself from your H2/H3/H4 headings — and updates when they change.

Accessible

Outputs a proper <nav> landmark with in-page anchor links.

SEO-friendly

Server-rendered into the page HTML on load, not injected by JS.

Screenshots

TOCflow block settings panel in the WordPress editor showing the Heading text field and toggles for including H2, H3 and H4 headings and using a numbered list
Block settings — heading text, which levels to include, and list style.
Front-end Table of Contents output: a nested bulleted list of linked headings generated by TOCflow
Front-end output — a nested, linked outline generated from the post.

Quick start

1. Install the plugin

  1. Download tocflow.zip from the latest release.
  2. In WordPress admin, go to Plugins → Add New → Upload Plugin.
  3. Choose the ZIP, click Install Now, then Activate.

2. Add a Table of Contents

  1. Edit a post that has some Heading blocks (H2, H3, H4).
  2. Click the + inserter where you want the TOC — usually right after your intro.
  3. Search for "Table of Contents" and select it. The outline is generated automatically.
The list is built from your real headings when the page loads, so you never have to maintain it by hand.

Settings reference

Select the block and open the Settings sidebar (gear icon) to adjust:

SettingWhat it does
Heading textThe title shown above the list (default: Table of Contents). Leave it blank to hide the title.
Include H2 / H3 / H4Choose which heading levels appear in the outline. H2 and H3 are on by default; H4 is off.
Use a numbered listSwitch between a numbered (1, 2, 3) and a bulleted list.

You can also use the standard block controls for color, spacing, and typography to match your theme.

Under the hood (for developers)

TOCflow is a small, modern dynamic block. It's an early-stage beta and open to contributions — here are the pieces that make it tick.

Block metadata (block.json)

{
  "name": "tocflow/table-of-contents",
  "title": "Table of Contents",
  "category": "widgets",
  "attributes": {
    "title":   { "type": "string",  "default": "Table of Contents" },
    "showH2":  { "type": "boolean", "default": true },
    "showH3":  { "type": "boolean", "default": true },
    "showH4":  { "type": "boolean", "default": false },
    "ordered": { "type": "boolean", "default": false }
  },
  "render": "file:./render.php"
}

One heading map, built on the server

A single pass with parse_blocks() collects every heading and stamps a unique slug — the single source of truth for both the list and the anchor IDs.

function tocflow_collect_headings( $blocks, &$used, &$collected ) {
    foreach ( $blocks as $block ) {
        if ( 'core/heading' === $block['blockName'] ) {
            $text  = trim( wp_strip_all_tags( $block['innerHTML'] ) );
            $level = isset( $block['attrs']['level'] ) ? (int) $block['attrs']['level'] : 2;
            if ( '' !== $text ) {
                $collected[] = array(
                    'level' => $level,
                    'text'  => $text,
                    'slug'  => tocflow_make_slug( $text, $used ),
                );
            }
        }
        if ( ! empty( $block['innerBlocks'] ) ) {
            tocflow_collect_headings( $block['innerBlocks'], $used, $collected );
        }
    }
}

Anchors that always match

A render_block filter injects the matching id into each heading as it renders, so every TOC link has a target.

add_filter( 'render_block', 'tocflow_add_heading_ids', 10, 2 );

function tocflow_add_heading_ids( $block_content, $block ) {
    if ( 'core/heading' !== ( $block['blockName'] ?? '' ) || ! is_singular() ) {
        return $block_content;
    }
    // Pull the precomputed slug for this heading, then add id="slug".
    return preg_replace(
        '/(<h[1-6])(\s|>)/i',
        '$1 id="' . esc_attr( $slug ) . '"$2',
        $block_content,
        1
    );
}

Read the full source on GitHub, or jump straight to tocflow.php.

Troubleshooting

The TOC is empty or missing headings

Links don't scroll to the right place

The block doesn't appear in the inserter

Common mistakes to avoid

Frequently asked questions

Does it work with the Classic Editor?

No. It's a block for the WordPress block editor (Gutenberg).

Will it slow down my site?

No. The outline is rendered on the server as plain HTML — there's no heavy JavaScript on the front end.

Can I have more than one TOC on a page?

It's designed for one per post. Multiple blocks will each list the same headings.

Is it really free?

Yes — TOCflow is free and open source under the GPL-2.0-or-later license. Download it, test it, and use it on as many sites as you like.