Markdown headings and anchor links: write them, fix them
Bobby Huang ·
An ATX heading in Markdown begins with a run of # signs (up to six). Next comes a space, a tab, or the end of the line, like ## Setup. Two # signs make a level 2 heading. To link to it, write a normal link whose target is # plus the heading's id, like [Setup](#setup). The CommonMark spec gives headings no ids, so the id comes from your viewer. Many viewers lowercase the text and swap spaces for hyphens, but the rules vary. If the link goes nowhere, find the id your viewer made and match it.
Key takeaways
- An ATX heading needs a run of up to six # signs followed by a space, a tab, or the end of the line. If text comes right after the # signs, the line stays a paragraph.
- The underline style with = or - only makes level 1 and level 2 headings.
- A heading link is a plain link whose target starts with #. CommonMark never assigns a heading an id.
- Many viewers build a lowercase, hyphenated id from the heading text. Check what yours does before you rely on it.
- Headings and links are on the live render list planned for Sashmark v1. The app has not been released.
An agent's plan file may open with a table of contents. Each line in it links to a heading further down, such as Risks or Rollback steps. A working link takes you straight to that section. A broken one may do nothing or jump back to the top, depending on the viewer.
The spec tells you how to write a heading. It never gives one an id to link to. For a quick tour of every element, start with the full syntax tour.
1. Write an ATX heading
One style is called ATX. You start the line with # signs. The count sets the level, from one to six.
# Release plan
## Risks
### Rollback steps
In CommonMark this gives three headings. The first is level 1, the second is level 2, and the third is level 3.
CommonMark needs a space after the last #. A tab works too, and so does ending the line right after the signs.
#Risks
####### Too deep
Neither line here is a heading. Both lines stay as text in one paragraph, with the # signs showing. The first has no space after the sign. The second has seven signs, one more than the limit.
You may indent the line by up to three spaces, and it still counts. Extra spaces around the heading text are trimmed, so ## Risks gives a heading that reads "Risks".
2. Know when closing # signs count
You can end a heading with a run of # signs if you like. Some people find it tidier. The closing run must have a space or tab in front of it.
## Risks ##
## Notes for C#
The first line is a level 2 heading whose text is just "Risks". The closing signs drop out. The second line is also a level 2 heading, but its text is "Notes for C#". The last # touches the letter C, so the spec treats it as part of the text.
3. Use setext headings only for levels 1 and 2
There's an older style, called setext. You write the heading text, then underline it on the next line.
Release plan
============
Risks
-----
CommonMark gives a level 1 heading "Release plan" from the = underline and a level 2 heading "Risks" from the - underline. No other levels exist in this style. For level 3 and below, you need # signs.
This style has a trap for agent files. Put dashes on the line right under a plain sentence, and that sentence becomes a level 2 heading. If an agent meant the dashes as a divider, put a blank line above them. ATX headings are easier to search for and harder to trigger by accident.
4. Link to a heading on the same page
A heading link is a normal Markdown link. What sets it apart is the target. It starts with #, then the id of the heading.
See [Rollback steps](#rollback-steps) before you deploy.
CommonMark parses this as a link with the text "Rollback steps" and the destination #rollback-steps. That is all the spec does. It does not give any heading an id, so it can't promise the link lands anywhere.
The id comes from whatever shows the page. Many viewers make one from the heading text. One pattern you may see:
- The text is turned to lowercase.
- Spaces become hyphens.
- Most punctuation is dropped.
- A repeat heading may get a number added, like
-1.
That list is only a guess. Viewers differ on punctuation, accented letters, emoji and repeats. Some viewers don't make ids at all. Check yours before you build a long table of contents on top of it.
In CommonMark, a bare target with a space in it is not read as a link. Angle brackets around the target allow spaces, but whether a heading id holds a space depends on the viewer. Write
#rollback-steps, not#Rollback steps.
5. Link to a heading in another file
The same idea works across files in many viewers. Put the file path first, then # and the id.
The setup is in [Install steps](setup-notes.md#install-steps).
CommonMark reads this as one link with the destination setup-notes.md#install-steps. The viewer decides whether it opens the file and scrolls to the heading. Test it once in the tool your team reads files in.
6. Fix a heading link that jumps nowhere
When a click does nothing, work through these checks in order.
- Read the raw link. Open the source text. Make sure every bracket and parenthesis is closed, and that the target starts with
#. - Read the raw heading. Confirm it's really a heading. Look for heading text that comes right after the
#signs with no space or tab between. That line is not a heading. So there is no heading id for the link to land on. - Find the real id. Copy it from a link icon if your viewer shows one, or inspect the heading in a browser. Compare it letter by letter with your target.
- Look for changed text. If the heading was renamed, the id probably changed with it. Either restore the old heading text or update every link that points at it.
- Check for repeats. Two headings with the same text may get different ids. A link may land on the first one when you meant the second.
- Watch for punctuation. Colons, slashes, ampersands and question marks are handled differently from viewer to viewer. Shorter, plainer heading text gives ids that are easier to predict.
Some viewers accept a raw HTML tag such as <a id="rollback"></a> above a heading, as a fixed target that doesn't depend on the heading text. Other viewers strip raw HTML, and some ignore it on purpose, so test it in each tool first.
7. Keep heading text stable when an agent edits
Agent files add a risk of their own. An agent revises a document and tidies a heading from "Rollback steps" to "How to roll back". The text reads better. Many viewers then build a new id as well, and every link to the old id now goes nowhere. Your viewer may not flag the broken links.
The same thing breaks your own place in the file, which the post on holding your spot during an agent rewrite covers. A short standing rule in the agent's project notes helps:
Treat heading text as fixed once a file has links to it. Do not reword, merge or split a heading unless I ask. If a heading must change, find every link in the project that points to it and update them in the same edit. Use plain heading text with no punctuation where you can.
After any agent pass, scan the diff for changed heading lines. A renamed heading is one line in a long diff and easy to skip. A checklist for reviewing what agents write shows a fast way to read just the lines that moved.
Where Sashmark fits
The Sashmark app aims to turn your agents' Markdown files into clean pages you can check and fix. It is in development and has not been released. Headings and links are both on the live render list planned for v1. The v1 plan adds editing on the rendered page, where lines you leave alone keep their exact bytes. The app itself has no AI features. See the planned features, the help pages and the roadmap for where each part stands.
To follow Sashmark as it takes shape, visit sashmark.com.
Questions
- Why does my heading show its # signs as plain text?
- In CommonMark, the opening # signs must be followed by a space, a tab, or the end of the line. A line like #Setup is not a heading, so its # sign shows as text. Seven or more # signs also stay as plain text, since six is the deepest level.
- How do I find the id my viewer gave a heading?
- Each viewer has its own way. Some put a small link icon next to each heading that you can copy. In a browser, you can inspect the heading and read its id. You can also test a guess, such as the heading text in lowercase with hyphens for spaces.
- Can I link to a heading in a different Markdown file?
- Many viewers accept a link like [Steps](setup-notes.md#steps), with the file path first and the heading id after the #. It is still an ordinary link, so whether it lands on the heading depends on the viewer.