Every guide gets a table of contents — and the layout bug that had been bleeding every page off the right edge since forever
The ToC was the goal; the layout fix was the surprise. `mx-auto` on a flex item silently disables stretch, so `<main>` had been sizing to its content instead of its parent on every page of the site. Found by driving headless Chrome after three wrong guesses. Plus a new orientation on which open model does which job.
Every guide gets a table of contents
Why Care?
Long-form content here has outgrown the scrollbar. A recipe with five job sections and a dozen sub-headings is skimmed by nearly everyone who opens it, and until today it offered no map — no way to see the shape of the argument, jump to the part you came for, or know where you were once you were deep in it.
Every guide now has one. And chasing it down surfaced a layout bug that had been quietly breaking every page on the site at narrow widths.
What’s New?
- Reading-position table of contents on all six guides — persistent left rail on wide screens, a floating hamburger at laptop widths, a top bar on mobile whose label names the heading you’re currently reading. Selecting a heading jumps and collapses.
@lossless-group/lfm0.3.0 → 0.4.1, which is what made the ToC possible at all.- New Recipe — Orientation: Which Open Model for Which Job, covering coding, ETL, data analysis, on-brand imagery, and speech-to-text across the open-weight models released in the last three months.
- Horizontal bleed fixed site-wide — see below.
- Wide tables now scroll inside their own frame instead of clipping.
The upgrade was the whole job
remarkHeadingIds shipped in LFM 0.4.0 and attaches an ordered outline at tree.data.headings, described in its own source as “ready to render a table of contents.” This site was pinned to 0.3.0 — three minors behind — so it never saw it, and AstroMarkdown.astro was computing its own heading slugs, which is precisely the drift that plugin was written to end.
So no LFM work was needed. Just an upgrade, deleting the local slugifier, and reading node.data.id.
Anchor churn: zero. The one real risk in adopting LFM’s slugifier is that published fragment URLs move — the anchors decision counted 646 moving across astro-knots. Before touching the renderer, every heading in every guide was parsed and compared under both algorithms: 53 headings, 6 guides, 0 moved. Nothing that was ever shared breaks.
The bug that had been there all along
The ToC looked wrong at narrow widths, which turned out to be a symptom of something older and much broader: every page on this site bled off the right edge when the window got narrow. Headings clipped mid-word, paragraphs running under the viewport.
It reads as “responsive text wrapping stopped working,” which sends you hunting in the wrong file. Three fixes were attempted and all three were wrong — a table wrapper, then min-width: 0 at two different levels — because the symptom looks content-shaped so you keep fixing content.
Driving headless Chrome and printing the ancestor chain ended it in one pass:
viewport=500 scrollWidth=637
637px main.flex-1.min-w-0.px-6 ← the only element wider than its parent
500px div.min-h-screen.flex.flex-col
500px body
500px html
html, body, and the flex wrapper were all correctly 500px, and nothing inside <main> exceeded its content box. <main> was sizing itself.
The cause: BaseThemeLayout wraps the slot in a flex column, and callers pass mx-auto through containerClass to centre the column. An auto cross-axis margin on a flex item cancels align-items: stretch — so <main> stopped filling its parent and fell back to fit-content, growing to its max-content width. The class is correct; the context silently changes what it means. min-width: 0 cannot help, because nothing was being shrunk.
One class fixed it: w-full restores a definite width, max-w-* still caps it, mx-auto still centres it once there’s genuine free space. Verified across four page types at a 500px viewport — scrollWidth == clientWidth, zero overflowing elements.
The lesson is the transferable part: measure the cascade, don’t reason about it. Written up as a blueprint with the full probe script, at astro-knots/context-v/blueprints/Guarantee-Text-Wrapping-and-No-Horizontal-Bleed-at-Any-Width.md.
Notes on the ToC itself
- Not split into
--modifier siblings, unlikeImageCarousel. The three states are viewport-driven, not author-chosen — all three render simultaneously and CSS decides.--is for variants an author picks. - Works with JavaScript disabled. It’s a list of anchors; the tracking, the collapse, and the mobile label are enhancement.
- The header offset is measured, not declared. A script reads the pinned header’s real bottom edge and publishes it as a custom property, because header heights differ per site and per breakpoint. CSS adds a per-breakpoint gap on top — mobile tucks close, the floating burger takes more air.
- Renders nothing below three entries. A two-item outline is noise.
What’s Next?
- The container-heading blocker: a
###inside a callout lands in the outline as if it were a section. Zero occurrences in this site’s content today, so it shipped — tracked package-side atlfm/context-v/issues/Heading-Outline-Cannot-Distinguish-Container-Headings.md. toc-types.tsis written to be deleted rather than migrated once LFM shipsnestHeadingsandfilterHeadings.- Roll the ToC to context-v and changelog renderings, per
astro-knots/context-v/blueprints/Standard-Table-of-Contents-for-Every-Markdown-Collection.md. - A
v0.3.0release narrative inchangelog/releases/to match this version bump.