State of the ugly doubled TOC entries of mdbook?

I assume most readers of the official Rust tutorial book and of some other related online books suffer from the ugly duplicated TOC section entries, present due to latest mdbook updates since the end of last year?

That is, each section has a duplicated TOC entry, as in

https://doc.rust-lang.org/book/ch01-01-installation.html

with

1.1 Installation

Installation

Or does browsers or OSs exists that does not show this issue (I can' imagine that).

I have not added a screenshot of the issue here by intent, but if you can not see it in the official Rust book online version, you can find a screenshot at Fix #2995: Skip duplicate first header in sidebar header navigation by cobyfrombrooklyn-bot Ā· Pull Request #3039 Ā· rust-lang/mdBook Ā· GitHub

This issue has been reported at the end of last year already, and there was even an attempt of an PR to fix that:

But for some reason there is no progress.

I agree with Eric Huss on this one and would rather see the official Rust book headings updated than mdBook changed in this way. By some random OpenClaw bot nonetheless.

I’m on the fence with this one.

On the one hand, I find the current way illogical. Firstly, you have to repeat the title of the SUMMARY.md, and secondly you reset the hierarchy at the file level because you must have this first title as a chapter (#), rather than its actual level in the book. That means you lose the chapter level, too (I don’t think mdbook gives an error or a warning if you mistakenly put another chapter in your file—it’s simply ignored).

I had this problem when I needed to detail some sections much more than others. I didn’t want to put every file on the same level because it would have ended up with one-paragraph files vs long files with a hierarchy. But now the TOC on the left-hand side has an uneven structure and the content has inconsistent headers with regard to their global hierarchy level. I suppose I must move the content to make it look more even, but then the flow of the sections would look strange. In any book, you always have a few introductory chapters or sections that are much shorter.

On the other hand, changing it now would break every book based on it, so it can only be an option. Perhaps it’s just too late to make such a change. For the ā€œRust bookā€ itself, the easiest is to comply.

I think I just learned this pattern when writing my first mdbook of significant size, and learned to go with it. That was long before the linked issue arose apparently, so there must be other things that rely on doing this.

It makes some refactorings more annoying (moving stuff to a subpage) and others less annoying (moving between nest levels) :person_shrugging:.

I don't find it surprising that different tools have different approaches (global vs local heirarchies). The per-file approach arguably aligns with the per-doc comment approach used in code.

You can use a different title, which I do for my "sectional introduction"s.

So their advice is to make each section a (separate) chapter on its own, that is, starting the first heading with "#" instead of "##"? So a book would have a few hundred chapters -- or sub-chapters, as they call it? That is a strange system, not really logical, and different from other authoring systems like LaTeX or AsciiDoctor. While it fixes the issue introduced with mdbook v0.5, it has the effect that our former section headings use now a very large font, and for my book now the section headings extend often to more than a single line, due to the large font.

I tried it with the mdbook preprocessor below, which is just a modification of an example shipped with mdbook.

//! This is a demonstration of an mdBook preprocessor which parses markdown
//! and removes any instances of emphasis.

use mdbook_preprocessor::book::{Book, Chapter};
use mdbook_preprocessor::errors::Result;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use pulldown_cmark::HeadingLevel;
use pulldown_cmark::Tag::Heading;
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use std::io;

fn main() {
    let mut args = std::env::args().skip(1);
    match args.next().as_deref() {
        Some("supports") => {
            // Supports all renderers.
            return;
        }
        Some(arg) => {
            eprintln!("unknown argument: {arg}");
            std::process::exit(1);
        }
        None => {}
    }
    if let Err(e) = handle_preprocessing() {
        eprintln!("{e}");
        std::process::exit(1);
    }
}

struct RemoveEmphasis;

impl Preprocessor for RemoveEmphasis {
    fn name(&self) -> &str {
        "remove-emphasis"
    }
    fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
        let mut total = 0;
        book.for_each_chapter_mut(|ch| match remove_emphasis(&mut total, ch) {
            Ok(s) => ch.content = s,
            Err(e) => eprintln!("failed to process chapter: {e:?}"),
        });
        eprintln!("removed {total} emphasis");
        Ok(book)
    }
}

// ANCHOR: remove_emphasis
fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result<String> {
    let mut buf = String::with_capacity(chapter.content.len());
    let parser = Parser::new(&chapter.content);
    let new_v: Vec<_> = parser
        .into_iter()
        .map(|item| match item {
            Event::Start(Heading {
                level,
                id,
                classes,
                attrs,
            }) => {
                let l = match level {
                    HeadingLevel::H1 => HeadingLevel::H1,
                    HeadingLevel::H2 => HeadingLevel::H1,
                    HeadingLevel::H3 => HeadingLevel::H2,
                    HeadingLevel::H4 => HeadingLevel::H3,
                    HeadingLevel::H5 => HeadingLevel::H4,
                    HeadingLevel::H6 => HeadingLevel::H5,
                    //_ => level,
                };
                Event::Start(Heading {
                    level: l,
                    id,
                    classes,
                    attrs,
                })
            }
            other => other,
        })
        .collect();
    Ok(pulldown_cmark_to_cmark::cmark(new_v.into_iter(), &mut buf).map(|_| buf)?)
}
// ANCHOR_END: remove_emphasis

pub fn handle_preprocessing() -> Result<()> {
    let pre = RemoveEmphasis;
    let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?;
    let processed_book = pre.run(&ctx, book)?;
    serde_json::to_writer(io::stdout(), &processed_book)?;
    Ok(())
}
[package]
name = "mdfix"
version = "0.1.0"
edition = "2024"

[dependencies]
#mdbook = { version = "0.5.2" }
mdbook-preprocessor = "0.5.2"
pulldown-cmark = { version = "0.13.3", default-features = false }
pulldown-cmark-to-cmark = "22.0.0"
serde_json = "1.0.149"

[EDIT]

I think for my book the best option is to use

[output.html]
sidebar-header-nav = false

to obtain a plain TOC structure as before mdbook v0.5.

Yes, that is a useful feature, which I use sometimes to make the TOC entries a bit shorter -- I think LaTeX had that feature as well.

Another point of mdbook, which is a bit strange is, that there is no support for chapter and section numbers in the text, but only in the TOC. I added them manually, which makes reordering sections a bit complicated. Later I wrote a preprocessor for number insertion, but I never used it, as I had inserted the numbers already manually.

I meant it’s the corresponding title, and it may be the same, so it could have been optional. But you’re right: it allows to have a genuine title on the page and a shorter title that fits the left-hand side TOC.

Re-reading what I wrote for mdbook, I think it’s often possible to reword the text to keep a somewhat similar and linear structure, instead of trying to structure everything on a sort of absolute level of hierarchy where some parts are less detailed than others.

I suppose I’m mostly annoyed that the tool uses numbered sections for the left-hand size TOC and not the actual content, and that it unfolds that TOC structure all the time. It makes it look rather odd unless you can populate it evenly across all the chapters.

Thinking about it, and considering the fact, that with version v.0.5 the subsection headings become visible in the TOC, when we click on an entry, a new strategy might be to just join all the sections again to a full single chapter text. Then I would have just 25 chapters, for which chapter headings are shown in the TOC, and clicking on one of them would show all the sections and subsections in the TOC. I am not really sure if this is actually how it now would work, but perhaps that is how they intend it for the latest version. It would be a bit absurd, as prior to v0.5 we had to split a chapter into separate section files to make the section headings visible in the TOC, and now we would have to join them again. And no existing book seems to really follow this strategy, not even the mdbook book itself. But perhaps I should try it. A downside would be, that for that layout we could not have alternative (shorter) section headings for the TOC. Only alternative chapter headings are allowed.