Skip to content

fix(blog): read heading ids from the rendered html - #137

Merged
iamtatsuki05 merged 2 commits into
developfrom
work/fix-heading-slug-consistency
Sep 21, 2026
Merged

iamtatsuki05 merged 2 commits into
developfrom
work/fix-heading-slug-consistency

Conversation

@iamtatsuki05

Copy link
Copy Markdown
Owner

WHY

Follow-up noted in #135. parseMarkdownFile computed the headings slugs from mdast, separately from the ids rehype-slug puts on the rendered headings, and the two disagreed.

For the bonsai post:

before   h2 agent--に-ai-用の前提を集める  |  .agent  に AI 用の前提を集める
after    h2 agent-に-ai-用の前提を集める   |  .agent に AI 用の前提を集める
rendered <h2 id="agent-に-ai-用の前提を集める">

Three ways it drifted:

  • it joined only the direct text and inlineCode children with a space, so a heading containing inline code gained a doubled separator
  • a heading whose text sits inside a link or **strong** lost that text entirely
  • it slugged only h2/h3, while rehype-slug slugs every heading, so the dedupe counter could fall out of step (## Notes under # Notes is notes-1 in the html)

Nothing renders headings today — the table of contents reads the DOM — so there is no visible bug. But anything that starts using the field would link to anchors that do not exist, and it is adjacent to the legacyAnchors map added in #135, where copying a slug from here would produce a confusing build failure.

WHAT

  • Collect the headings from the tree right after rehype-slug with a small collectHeadings plugin, so the ids come from the same place the html does
  • Drop the separate mdast pass, along with the github-slugger and mdast type imports it needed
  • Three tests covering inline code, the dedupe counter, and heading text inside a link or **strong**

VERIFICATION

  • tsc, biome, vitest lib 103 / components 100, bun run build (73 pages), Playwright chromium E2E 59 — all pass
  • Rendered heading ids are unchanged; only the reported headings values move
  • Compared the field before and after against the same post to confirm the doubled separator is gone

`parseMarkdownFile` computed the `headings` slugs from mdast, separately
from the ids rehype-slug puts on the rendered headings, and the two
disagreed. It joined only the direct `text` and `inlineCode` children
with a space, so a heading containing inline code produced a doubled
separator (`agent--に-ai-用の前提を集める` against the rendered
`agent-に-ai-用の前提を集める`), a heading whose text sits inside a link
or strong lost that text, and slugging only h2/h3 could put the dedupe
counter out of step with rehype-slug.

Collect the headings from the tree right after rehype-slug instead, so
there is one source of truth. Nothing renders `headings` today, but
anything that starts to would have linked to anchors that do not exist.
@iamtatsuki05 iamtatsuki05 self-assigned this Sep 20, 2026
Skip headings with an empty id so the list matches what BlogToc builds
from the DOM, reset the array at the start of the transformer so hoisting
the processor cannot accumulate duplicates, and use the HeadingEntry type
in the signatures that already described it inline.

Also record two behaviours the switch to the rendered tree introduces:
headings that come from raw HTML in the body are now included, and
`title` is the text before KaTeX renders it, so a heading containing
math reports its source rather than the rendered markup. Tests cover the
raw HTML and empty id cases.
@iamtatsuki05 iamtatsuki05 changed the title [WIP] fix(blog): read heading ids from the rendered html fix(blog): read heading ids from the rendered html Sep 21, 2026
@iamtatsuki05
iamtatsuki05 marked this pull request as ready for review September 21, 2026 05:10
@iamtatsuki05
iamtatsuki05 merged commit dca9df5 into develop Sep 21, 2026
7 checks passed
@iamtatsuki05
iamtatsuki05 deleted the work/fix-heading-slug-consistency branch September 21, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant