A small, accessible WordPress plugin that automatically generates a table of contents (TOC) for posts and pages based on heading tags. It builds a nested, semantic TOC, provides a collapsible panel with ARIA attributes, and supports per-page enable/disable and server-side caching.
- Plugin slug:
dynamic-toc - Version: 1.6.4
- Requires: WordPress 5.2+
- Requires PHP: 8.1+
- License: GPL-2.0-or-later
- Generates a hierarchical TOC from headings (configurable levels).
- Produces accessible markup (button with
aria-expanded/aria-controls,role="region", etc.). - Collapsible panel toggle (progressive enhancement with JS).
- Per-page enable/disable meta box in the editor.
- Gutenberg block for easy insertion in the block editor.
- Shortcode support for manual TOC placement.
- Server-side per-post transient caching to avoid repeated work.
- Filters to adjust behavior and markup for advanced integrations.
- Upload the
dynamic-tocfolder to your/wp-content/plugins/directory. - Activate the plugin from the WordPress Plugins screen.
- Optionally enable the TOC per page using the Dynamic TOC meta box on the post/page edit screen, or set the default via filter.
- The TOC will be inserted into the post content automatically when enabled.
- Enable per-post via the Show Table of Contents on this page meta box on the post/page edit screen.
- The plugin detects headings and builds a nested ordered list reflecting the document structure.
- Assets (JS/CSS) are enqueued only when a TOC will be output.
- Use the Dynamic Table of Contents block in the block editor to insert a TOC.
- Search for "toc" or "table of contents" in the block inserter.
- The block displays a preview in the editor and renders the [dynamic_toc] shortcode on the frontend.
- Manually insert
[dynamic_toc]in post content or text widgets to place the TOC inline. - The shortcode respects the same per-page enable/disable settings as the automatic filter.
Below are the available filters and short usage examples showing how to modify plugin behavior from a theme or custom plugin.
ttm_dynamic_toc_enabled(bool $enabled, int $post_id)- Filter whether the TOC is enabled for a post.
// Disable TOC for a specific post ID.
add_filter( 'ttm_dynamic_toc_enabled', function( $enabled, $post_id ) {
if ( 42 === (int) $post_id ) {
return false;
}
return $enabled;
}, 10, 2 );ttm_dynamic_toc_heading_levels(int[] $levels)- Filter the heading levels the TOC should consider. Defaults to
[2,3,4].
- Filter the heading levels the TOC should consider. Defaults to
// Include h2-h5 in the TOC.
add_filter( 'ttm_dynamic_toc_heading_levels', function() {
return array( 2, 3, 4, 5 );
} );ttm_dynamic_toc_meta_key(string $meta_key)- Filter the post meta key used for the per-page enable checkbox. Default:
ttm_dynamic_toc_enabled.
- Filter the post meta key used for the per-page enable checkbox. Default:
// Use a custom meta key if your site already uses a different one.
add_filter( 'ttm_dynamic_toc_meta_key', function( $key ) {
return 'my_custom_toc_flag';
} );ttm_dynamic_toc_meta_key_{post_id}(string $meta_key, int $post_id)- Dynamic filter that allows overriding the meta key used on a per-post basis. Replace
{post_id}with the numeric post ID. This hook is fired only when a valid post ID is available and allows themes/plugins to change which meta key the plugin reads for that specific post.
- Dynamic filter that allows overriding the meta key used on a per-post basis. Replace
// For post ID 123, change the meta key used by the plugin.
add_filter( 'ttm_dynamic_toc_meta_key_123', function( $meta_key, $post_id ) {
// You can ignore $meta_key and return a custom key for this post.
return 'my_custom_toc_flag_for_post_123';
}, 10, 2 );ttm_dynamic_toc_cache_ttl(int $seconds)- Filter the transient TTL (default 12 hours).
// Reduce cache TTL to 1 hour.
add_filter( 'ttm_dynamic_toc_cache_ttl', function( $ttl ) {
return HOUR_IN_SECONDS;
} );ttm_dynamic_toc_html(string $html, array $toc_list, int $post_id)- Filter the rendered TOC HTML before it is prepended to content.
// Wrap the TOC in a custom container or modify markup.
add_filter( 'ttm_dynamic_toc_html', function( $html, $toc_list, $post_id ) {
return '<div class="my-toc-wrap">' . $html . '</div>';
}, 10, 3 );ttm_dynamic_toc_meta_post_types(string[] $post_types)- Filter the post types that show the per-page TOC meta box in the editor.
// Enable the meta box for a custom post type 'book'.
add_filter( 'ttm_dynamic_toc_meta_post_types', function( $types ) {
$types[] = 'book';
return $types;
} );Notes:
- The plugin registers assets via
ttm_dynamic_toc_register_assets(); you can dequeue or override the handlesttm-dynamic-tocin your theme if you need custom styling or behavior. - Use the
ttm_dynamic_toc_htmlfilter to fully replace the output if you need a completely different markup structure.
- The plugin ships with PHP_CodeSniffer (WPCS) configured for local development. Use the composer-supplied tools (if present) to lint and autofix:
# from plugin root (if composer/dev dependencies are installed)
./vendor/bin/phpcs --standard=WordPress --extensions=php -n --ignore=vendor -p .
./vendor/bin/phpcbf --standard=WordPress --extensions=php -n --ignore=vendor -p .- JavaScript is lightweight and responsible for accessibility toggles and reduced-motion respects.
Contributions are welcome. Please open issues or pull requests on the GitHub repository: https://github.com/NathanDozen3/dynamic-toc
When contributing:
- Follow WordPress PHP coding standards.
- Keep accessibility (keyboard interaction and ARIA) intact.
- Added nested TOC rendering by heading level.
- Improved accessible collapse toggles and ARIA attributes.
- Reworked DOM handling to avoid deprecated mbstring usage.
- Initial refactor: removed ACF, added per-page meta box and caching.
This plugin is licensed under the GPL-2.0-or-later. See the LICENSE file for details.