Links and Internal Links

In the CMS, Mod-K opens the Add Link dialog, and its three tabs are described on Links and the Link Picker. This page is what that dialog writes into the markdown, and what the build does with it afterwards. If what you actually want is the day-to-day workflow for cross-linking a documentation set — reserve first, verify after publish — that lives on Linking Between Docs Pages; this page owns the syntax and the mechanics.

A link is [text](url). Attributes go immediately after the closing parenthesis, with no space:

[Read the changelog](https://example.com/changelog){target="_blank" rel="noopener"}

The editor models eleven attributes on a link. Anything outside that list renders correctly on your published site but has nowhere to live in the editor, so it does not come back after an edit.

AttributePurposeSet by the editor?Survives a save?
hrefthe destinationYes — whichever tab of the picker you useYes
titlenative browser tooltipFalls back to the link textNo — the serializer omits it on purpose
target_blank opens a new tabYes, set automatically on an external URLYes
downloadsave the target instead of navigating to itYes, on a Documents linkYes
classa hook for your stylesheetNoYes
relnoopener, nofollow, sponsoredNoYes
idmakes the link itself an anchor targetNoYes
roleoverrides the implied ARIA roleNoYes
aria-labelaccessible name when the text is not enoughNoNo — see below
aria-describedbypoints at an element that describes the linkNoNo — see below
data-assetidties the link to an asset in your libraryYes, on a Documents linkYes

Heading anchors

Every heading on a page from level 1 to level 5 is given an id, slugified from its text. Duplicates are disambiguated with a -1, -2 suffix, and a level-6 heading gets no id at all. That generation is described in full, with a table by level, on Basic Formatting.

To link to a heading on the same page, use the fragment on its own:

See [bare URLs are not links](#bare-urls-are-not-links) below.

Live, that is: see bare URLs are not links below. This form is plain HTML and involves none of the machinery in the rest of this page.

A link to another page on your own site is written against the page’s id, not its path:

[Attributes](pageid:96da6677-6331-4337-8b63-1f7e8732c47b)

The reason is that a path is a moving target and an id is not. Rename a page, change its slug, move it into a different folder in the docs menu — the URL changes, and every hard-coded /docs/... link pointing at it breaks. The pageId never changes, so a pageid: link keeps resolving to wherever the page now lives. How paths get built in the first place is on URL Paths and Slugs.

You can get a page’s id from its settings in the CMS, from the response to create_page when you reserve a page over MCP, or — most often — by never seeing it at all, because the link picker writes it for you.

How it resolves

Resolution happens at build time, not at request time and not while you type. Once every page in the build has been rendered, a DOM transformer called mutatePageIds walks every anchor in every emitted HTML file and rewrites the ones whose href starts with pageid:.

flowchart TD
  A["Authored: a pageid: link, optionally with a fragment"] --> B["Build step: mutatePageIds walks every anchor"]
  B --> C{"Did that page render in this build?"}
  C -->|Yes| D["Rewrite the href to the page's real path,<br/>keeping the query string and the fragment"]
  C -->|No| E["Replace the whole anchor with<br/>a span of class page-removed"]
  D --> F["Reader follows the link"]
  E --> G["Reader sees the words, with no link"]

The miss branch is the one to internalize: the words survive and the link disappears. There is no broken link, no 404 and no build error — the sentence still reads, and a reader has no way to tell that it was meant to go somewhere. A page misses because it did not render in that build, which almost always means it is not published yet.

Where the link isResult when the target did not render
In a page bodythe anchor becomes <span class="page-removed">, keeping the text
A page in the left navigation menuthe whole list item is omitted — the entry vanishes from the nav
A folder in the left navigation whose own href is deadthe folder still renders, as plain text instead of a link, so its children stay reachable
Previous / nextthe pager is built before resolution, so the dead anchor reaches the same step and becomes page-removed
A breadcrumbthe same as previous / next

Whether a page rendered is decided by publish state, which is the subject of How Publishing Works.

Linking to a section of another page

Append the target heading’s id to the link:

[how anchors are generated](pageid:47796075-56be-440d-b38e-7cc8ba2e56ae#heading-anchors)

The fragment survives resolution — the rewritten href is composed as path, then query string, then fragment — so the reader lands on that heading rather than at the top of the page. Live: how anchors are generated.

Three things govern using it well.

The fragment must be a generated heading id on the target page. That means it is the slugified heading text, it exists only for levels 1 to 5, and a duplicate heading carries its -1 or -2 suffix. A fragment can never point at a ###### heading, because no id is generated for one.

Link to the page when the page is the answer; link to the section when the section is. A fragment is a promise that the reader’s question is answered in that one part of the page. If they need the whole thing, sending them into the middle of it is worse than sending them to the top.

In practice you will rarely type a pageid: link. Paste a URL from your own site into the External URL tab of the Add Link dialog and move focus away from the field: the CMS looks the URL up against your pages, and if it resolves, the href is rewritten to pageid:<pageId> and the dialog switches itself to the Internal Page tab with that page selected.

The Add Link dialog in the page editor, with a full internal URL pasted into the External URL field

Picking a page directly from the Internal Page tab does the same thing without the round trip. Either way, the markdown that lands in the page is the id form.

The site renderer has markdown-it’s linkify option switched off, so a URL sitting in a paragraph stays text: https://example.com renders as the characters you see, not as an anchor. Write [example.com](https://example.com) when you want a link.

Do not confuse writing a link with autolinks, which are a site-wide setting rather than page content: you register a term, and the build turns every occurrence of that term across the site into a link to the page you nominated, appending UTM parameters so the traffic is attributable. Nothing in your markdown changes, and a page can opt out. That feature is covered on Autolinks.

ESC