Skip to content

Development notes

An article’s table of contents should come from its rendered headings

How this site builds an article’s table of contents from rendered headings, sharing the source for labels, order, and anchors while handling the mobile Drawer, reading without JavaScript, and current-section indicators separately.

林予晴
Published
  • Content engineering
  • Astro
  • Markdown
  • Table of contents

If changing a section heading also means editing the table of contents beside it, you have two structures that can drift apart. The heading text may have changed or sections may have moved, while the table of contents still points to the old location.

This site does not maintain a separate list for an article’s table of contents. It uses the heading data produced when Markdown is rendered. The body determines which sections exist; the table of contents presents the ones suitable for navigation.

This reduces the work of maintaining two structures. It does not automatically make the article easier to read. Clear headings and a sensible section order still depend on writing and editing.

Start with the body, then choose which headings to include

On this site, the article title comes from frontmatter, and the layout renders it as the main h1. The Markdown body starts with h2. The article query also checks for any h1 in the body, so the layout title and the body do not each introduce a top-level heading.

After rendering the content, the article layout selects entries with depth === 2 from the heading data for the current table of contents.

Authors therefore do not need to fill in another toc field or manually write links for every section. When they change the text or order of the body’s h2 headings, the next render uses the new data for the table of contents too.

This rule is deliberately narrow. The site currently includes only main sections, rather than expanding every h3 and h4 into a multilevel list. That suits the current article structure. If future long-form tutorials need direct navigation to individual substeps, a nested table of contents can be considered then.

Use the rendered anchors instead of guessing them

Astro’s render() can return a content component and an array of headings. Each heading provides data such as depth, slug, and text.[3] This site uses text for the link label and turns slug into a #... target.

The following is a simplified view of the existing data flow, not another configuration authors need to maintain by hand:

Section headings in Markdown
        ↓
Content rendering produces heading data
        ↓
Extract the h2 text, order, and slug
        ↓
Table-of-contents links target heading IDs in the body

I would not add a separate function on the table-of-contents side to turn Chinese text into URLs. Handling punctuation, English letter case, and duplicate headings should match the identifiers used by the body renderer. Calculating them again could produce different targets.

There is a maintenance limit here too: rewriting a section heading may change its heading ID. If someone has saved an old section link, an automatic table of contents does not keep that old anchor compatible. Sections that need stable references over time still require a separate anchor strategy. An article’s stable ID and a section anchor are not the same thing.

Share the structure across desktop and mobile, not necessarily the layout

The desktop version currently places the table of contents in a sidebar beside the body. The mobile version uses a Drawer that readers can open. Both receive the same list of main headings and use the same table-of-contents component.

This avoids separately maintained content that leaves one version missing a section while the other still shows an old heading. What they share is the section structure; the two screen sizes do not need identical layouts.

When an article has no main headings, the table-of-contents blocks are not rendered. The current rule displays the list whenever it has entries. There is no separate count threshold such as hiding it when there are only two headings. Any future change should explicitly update the display condition rather than describe behavior in an article that has not been implemented.

The table-of-contents links already have real section targets in the HTML. The desktop table of contents does not wait for a script to generate it. The mobile version also provides a separate noscript list so readers without JavaScript can still use native links.

That does not mean the mobile Drawer itself works as usual without scripts. Opening and closing it is an interactive control; the noscript list is a separate way to navigate the article. Keeping these distinct avoids turning “the body remains readable” into a claim that all dynamic controls work without scripts.

With scripts enabled, the site also adds a current-section indicator, aria-current on the relevant table-of-contents item, and an action to close the Drawer after a mobile table-of-contents link is selected. Scrolling is smooth or immediate depending on the reduced-motion setting.

These enhancements use the h2[id] elements that actually exist in the body and the targets of the table-of-contents links. They do not store another set of section names.

Automatic generation still needs checking

Generating a table of contents from headings only reduces the risk of manually maintained lists falling out of sync. Authors can still write vague headings such as “Other” or “Additional notes,” or promote a small reminder to a main section and make the table of contents too long.

I first check whether the table of contents conveys the article’s reading order, then inspect the generated result:

  • Does the table-of-contents text match the main headings in the body?
  • Does every link find its corresponding heading ID?
  • Do the desktop, mobile, and no-script lists contain the same set of sections?
  • Does an article without main headings leave behind a table-of-contents container with no purpose?

After changing the layout, the actual page still needs to be used: check that clicking a link reaches the section, the mobile Drawer closes, fixed navigation does not obscure the heading, and the current-section indicator behaves sensibly. Consistent data does not mean the reading position and visual presentation have passed acceptance checks.

Give the article structure one place to change

This design brings article editing back to the body. If the table of contents is unclear, I start by changing the section headings and their order rather than giving the sidebar a more attractive set of names.

Readers still see the body when they follow a section link. A table of contents can shorten the path to the content, but it should not invent a better-looking structure that hides the differences between the two.

Scope of this record

This article describes heading filtering in this site’s article layout, the table-of-contents component, the mobile Drawer, the list for readers without JavaScript, and the responsibilities of browser scripts. It does not claim that the site currently has a multilevel table of contents, a fixed section-anchor contract, research on reading outcomes, or acceptance testing across every device. Article identity and multilingual routing are separate responsibilities, not decisions made by the table-of-contents list.

Sources

[3] https://docs.astro.build/en/reference/modules/astro-content