

Add Table of Contents in your blog posts.
/**
* Snippet Name: Table of Contents
* Snippet Author: coding-bunny.com
* Description: Add Table of Contents in your blog posts.
* Version: 1.0.0
*
* USAGE EXAMPLES:
* [cbtoc title="Table of content" levels="h2,h3,h4" toggle_view="yes" initial_view="show" exclude_titles="Introduction,Summary"]
*/
if (!defined('ABSPATH')) exit;
add_shortcode('cbtoc', function($atts) {
$atts = shortcode_atts([
'levels' => 'h2,h3',
'title' => 'Table of Contents',
'toggle_view' => 'no',
'initial_view' => 'show',
'exclude_titles'=> '',
], $atts, 'cbtoc');
$allowed_levels = ['h1','h2','h3','h4','h5','h6'];
$requested_levels = array_map('trim', explode(',', strtolower($atts['levels'])));
$levels = array_intersect($allowed_levels, $requested_levels);
$exclude_titles = array_filter(array_map('trim', explode(',', $atts['exclude_titles'])));
$exclude_titles_lc = array_map('mb_strtolower', $exclude_titles);
if (empty($levels)) {
return '<div class="cbtoc-error">No valid heading level specified for the Table of Contents.</div>';
}
global $post;
if (empty($post) || empty($post->post_content)) return '';
$content = $post->post_content;
$headings = [];
$pattern = '/<(' . implode('|', $levels) . ')([^>]*)>(.*?)<\/\1>/i';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
if (empty($matches)) {
return '<div class="cbtoc-empty">No headings found.</div>';
}
foreach($matches as $match) {
$tag = strtolower($match[1]);
$title = strip_tags($match[3]);
if (in_array(mb_strtolower(trim($title)), $exclude_titles_lc, true)) {
continue;
}
$slug = sanitize_title($title);
$level = intval(substr($tag, 1));
if (!preg_match('/id=["\'](.+?)["\']/', $match[2], $id_match)) {
$anchor = $slug;
$content = preg_replace(
'/<' . $tag . $match[2] . '>' . preg_quote($match[3], '/') . '<\/' . $tag . '>/i',
'<' . $tag . $match[2] . ' id="' . esc_attr($anchor) . '">' . $match[3] . '</' . $tag . '>',
$content, 1
);
} else {
$anchor = esc_attr($id_match[1]);
}
$headings[] = [
'title' => $title,
'anchor' => $anchor,
'level' => $level,
];
}
if (empty($headings)) {
return '<div class="cbtoc-empty">No headings found.</div>';
}
$min_level = min(array_column($headings, 'level'));
$unique_id = 'cbtoc-' . uniqid();
$toggle_view = strtolower($atts['toggle_view']) === 'yes';
$initial_view = strtolower($atts['initial_view']) === 'hide' ? 'hide' : 'show';
$show_style = ($toggle_view && $initial_view === 'hide') ? 'display:none;' : '';
$toc = '<nav class="cbtoc-pro" aria-label="Table of Contents" id="' . esc_attr($unique_id) . '">';
$toc .= '<div class="cbtoc-title-wrap">';
$toc .= '<span class="cbtoc-title">' . esc_html($atts['title']) . '</span>';
if ($toggle_view) {
$toggle_label = ($initial_view === 'hide') ? '+' : '–';
$toc .= '<button type="button" class="cbtoc-toggle-btn" aria-expanded="' . (($initial_view==='show')?'true':'false') . '" aria-controls="' . esc_attr($unique_id) . '-list">' . esc_html($toggle_label) . '</button>';
}
$toc .= '</div>';
$toc .= '<div class="cbtoc-contents" id="' . esc_attr($unique_id) . '-list" style="' . $show_style . '">';
$toc .= cbtoc_build_nested_ul($headings, $min_level);
$toc .= '</div>';
$toc .= '</nav>';
$toc .= '<style>
.cbtoc-pro {
background: #f9f9fc;
border: 1px solid #e0e5ee;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
max-width: 100%;
}
.cbtoc-pro .cbtoc-title-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.cbtoc-pro .cbtoc-title {
font-size: 16px;
font-weight: bold;
color: #142520;
border-left: 4px solid #177efa;
padding-left: 10px;
}
.cbtoc-pro .cbtoc-toggle-btn {
margin-left: 10px;
border: none;
background: none;
font-size: 20px;
font-weight: bold;
color: #177efa;
cursor: pointer;
line-height: 1;
padding: 0 3px 0 0;
align-self: flex-end;
}
.cbtoc-pro .cbtoc-item {
margin: 5px 0;
font-size: 14px;
}
.cbtoc-pro .cbtoc-level-2 { margin-left: 0px; font-weight: bold;}
.cbtoc-pro .cbtoc-level-3 { margin-left: 10px; font-weight: normal;}
.cbtoc-pro .cbtoc-level-4 { margin-left: 15px;}
.cbtoc-pro .cbtoc-level-5 { margin-left: 20px;}
.cbtoc-pro .cbtoc-level-6 { margin-left: 25px;}
.cbtoc-pro a {
color: #142520;
text-decoration: none;
}
.cbtoc-pro a:hover {
color: #177efa;
}
.cbtoc-list,
.cbtoc-list ul {
padding-left: 20px;
list-style-type: disc !important;
margin-left: 0;
}
</style>
';
if ($toggle_view) {
$toc .= '<script>
(function(){
var tocNav = document.getElementById("' . esc_js($unique_id) . '");
if (!tocNav) return;
var btn = tocNav.querySelector(".cbtoc-toggle-btn");
var contents = tocNav.querySelector(".cbtoc-contents");
if (!btn || !contents) return;
btn.addEventListener("click", function() {
var isOpen = contents.style.display !== "none";
contents.style.display = isOpen ? "none" : "";
btn.textContent = isOpen ? "+" : "–";
btn.setAttribute("aria-expanded", (!isOpen).toString());
});
})();
</script>';
}
return $toc;
});
function cbtoc_build_nested_ul($headings, $base_level) {
$toc = '<ul class="cbtoc-list">';
$prev_level = $base_level;
$open_uls = 0;
foreach ($headings as $heading) {
$curr_level = $heading['level'];
while ($curr_level > $prev_level) {
$toc .= '<ul class="cbtoc-list">';
$open_uls++;
$prev_level++;
}
while ($curr_level < $prev_level) {
$toc .= '</ul>';
$open_uls--;
$prev_level--;
}
$toc .= '<li class="cbtoc-item cbtoc-level-' . $curr_level . '"><a href="#' . esc_attr($heading['anchor']) . '">' . esc_html($heading['title']) . '</a></li>';
}
while ($open_uls > 0) {
$toc .= '</ul>';
$open_uls--;
}
$toc .= '</ul>';
return $toc;
}
Choose one of the following options:
Powered by CodingBunny