diff --git a/crates/mdbook-html/front-end/templates/toc.js.hbs b/crates/mdbook-html/front-end/templates/toc.js.hbs
index 1a9f751ccf..33205b1892 100644
--- a/crates/mdbook-html/front-end/templates/toc.js.hbs
+++ b/crates/mdbook-html/front-end/templates/toc.js.hbs
@@ -356,6 +356,16 @@ window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox)
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
+ // Skip the first heading if its text matches the active sidebar link.
+ // This avoids duplicating the chapter title in the "on this page" list
+ // when a chapter's markdown starts with a heading (e.g. `## Section Title`)
+ // that matches its SUMMARY.md entry.
+ // See https://github.com/rust-lang/mdBook/issues/2995
+ if (headers.length > 0
+ && activeSection.textContent.trim() === headers[0].textContent.trim()) {
+ headers.shift();
+ }
+
if (headers.length === 0) {
return;
}
diff --git a/tests/testsuite/toc.rs b/tests/testsuite/toc.rs
index 40bb0001e2..9d07d09488 100644
--- a/tests/testsuite/toc.rs
+++ b/tests/testsuite/toc.rs
@@ -141,6 +141,17 @@ fn check_link_target_fallback() {
);
}
+/// The "on this page" feature should skip the first heading if it matches
+/// the active sidebar link text, to avoid duplicating the chapter title
+/// when a chapter starts with an h2+ heading.
+/// See .
+#[test]
+fn toc_js_deduplicates_first_heading() {
+ BookTest::from_dir("toc/basic_toc")
+ .check_file_contains("book/toc*.js", "activeSection.textContent.trim() === headers[0].textContent.trim()")
+ .check_file_contains("book/toc*.js", "headers.shift()");
+}
+
// Checks formatting of summary names with inline elements.
#[test]
fn summary_with_markdown_formatting() {