Skip to content

How to link documentation pages effectively

Internal and anchor links that survive a base path, redirects for reorganizations, and mechanical checks.

Links are what turn a pile of pages into documentation. They let a reader jump to the concept they are missing, follow a process across pages, and find the next step when they finish one. Bad links do the opposite: vague link text, missing cross-references and dead URLs make docs harder to use and weaker in search results.

This guide covers the kinds of links you write in a Reed project, the one rule that catches everyone, and how to keep links intact as the site changes.

Link to other pages with root-relative paths built from the page id:

mdx
- [Quickstart](/docs/quickstart)
- [Navigation](/docs/authoring/navigation)
- [Build output](/docs/ship/output)

The path after the base is the page’s id — the same string you list in navigation. The id comes from the file path, with segments slugified and a nested index collapsed to its directory. Navigation has the exact rules.

Reed does not rewrite links you write in prose, and it does not rewrite href attributes on components either. If your docs.json sets base, every internal link must include it by hand.

On a site with base: "/docs", /docs/quickstart is correct. /quickstart compiles without a single warning and 404s in production. The same applies to a <Card> or any other component with an href:

mdx
<Card title="Quickstart" href="/docs/quickstart">
From nothing to a deployable site in five minutes.
</Card>

This is the single easiest mistake to make in a Reed project. If your site has no base, links start at / with the page id and there is nothing to forget.

Links inside docs.json follow the opposite rule. Footer column links and redirects are written root-relative without the base, and Reed adds it. Writing /docs/... there produces /docs/docs/.... site and base lays both rules out side by side.

Reed does not resolve ./sibling or ../other links against the source file. They reach the browser as written, and the browser resolves them against the URL of the page it is showing — which depends on how your host serves trailing slashes, not on where the MDX file sits. Root-relative links behave the same from every page and survive moving the linking page. Use them.

An anchor link points at a section, not just a page. Every heading gets an id derived from its text, so you can link to it without doing anything.

mdx
[Jump to best practices](#best-practices)

Append the heading id to the page path: /docs/site/redirects#real-redirects opens the redirects page at its “Real redirects” section.

The id is the heading text lowercased, with spaces turned into hyphens and most punctuation dropped:

Heading Anchor
## Getting Started #getting-started
### API Authentication #api-authentication
#### Step 1: Install #step-1-install

The consequence: edit a heading and every link to its old anchor stops working. The page still loads, so nothing looks broken — the reader just lands at the top instead of the section. Reed documents no syntax for pinning a heading to a custom id, so treat the wording of headings you link to often as part of their contract, and grep for the old anchor before you change it.

An Accordion takes an id prop that gives it a stable anchor, independent of its title:

mdx
<Accordion title="Installation steps" id="install">
Run the installer, then restart your shell.
</Accordion>

A link to #install keeps working when you reword the title. See Layout and surfaces for the component’s props.

Absolute http(s) URLs pass through unchanged everywhere. Write link text that says where the reader is going:

mdx
See the [OpenAPI specification](https://spec.openapis.org/oas/latest.html) for the full schema.

Link text should tell readers where they are going before they click. “Click here” and “read more” tell them nothing, read badly out of context in a screen reader’s link list, and give search engines no signal about the destination.

mdx
See [Redirects](/docs/site/redirects) before you move a page.
Learn how to [structure your navigation](/docs/guides/navigation).

Avoid link text like [click here], [read more] or [this page], whatever it points at.

When a page depends on earlier steps, link them at the top instead of hoping readers find them:

mdx
## Prerequisites
Before you deploy, make sure you have:
- Finished the [Quickstart](/docs/quickstart)
- Set `site` and `base` as described in [site and base](/docs/site/addressing)
- Reviewed what [the build writes](/docs/ship/output)

Link related pages to each other so readers — and search engines — can see how your content fits together. A short related-pages section at the end of a page does this well, as does a CardGroup of next steps:

mdx
<CardGroup cols={2}>
<Card title="Redirects" href="/docs/site/redirects">Keep old URLs alive.</Card>
<Card title="site and base" href="/docs/site/addressing">The two link rules.</Card>
</CardGroup>

Reed does not check the links in your prose. reed check validates docs.json and confirms every page id in navigation resolves, but a broken [text](/docs/...) in a paragraph compiles cleanly. On a site with a base, a forgotten prefix is exactly that kind of link.

So check them yourself. This documentation site runs a short script, scripts/check-links.mjs, in its build script before reed runs. It:

  • scans every .mdx file for markdown link destinations ](/path) and JSX attributes href="/path",
  • skips inline code, so a page can show a wrong link as a counter-example,
  • still checks fenced code blocks, because readers copy those,
  • fails if a root-relative link is missing the /docs base or names a page id that has no file.

It fits in about eighty lines. Writing one for your own project is an afternoon’s work, and it catches the base-prefix mistake before your readers do. If you extend it to anchor links, strip the #fragment before looking up the page id.

For wider coverage — anchors, external URLs, assets — run a third-party link checker against the built output. Build the site, serve the output directory locally, and crawl it. That checks what readers actually get rather than what the source says.

When you move or rename a page:

  1. Move the file and update its entry in navigation.
  2. Add a redirect from the old path to the new one.
  3. Search the content directory for the old path.
  4. Update every internal link that uses it.
  5. Run reed check and your link checker.

A bookmark, a search result or a link from a blog post does not know you moved the page. A redirect keeps it working:

json
{
"redirects": [
{ "source": "/old-guide", "destination": "/guides/getting-started" }
]
}

Write source and destination without the base — Reed adds it. Reed writes each redirect into a _redirects file, which Cloudflare Pages and Workers serve as a real 301 or 302, and into an HTML page at the old path that meta-refreshes to the destination and points its canonical link there. Hosts that do not apply _redirects, and Netlify unless you add a forced rule, serve that page instead. A source must not be the path of a page that is still in navigation: reed check reports that as redirect-source-conflict. There are no wildcards, so moving a whole section means one entry per page. Redirects covers the details, including when the permanent flag matters.

Should internal links be root-relative or absolute URLs?

Root-relative, starting with your base. They work from any page, survive a change of domain, and are what your link checker can verify against page ids. Absolute URLs to your own site add a dependency on the origin for no benefit. Relative paths are the worst option: Reed passes them through untouched, so they resolve against the served URL rather than the source file.

How do I keep anchor links stable when I edit headings?

Treat the wording of frequently linked headings as fixed, and search for the old anchor whenever you change one. For content inside an accordion, set the accordion’s id prop, which gives it an anchor that does not depend on its title.

What happens to bookmarked links when I reorganize?

Without a redirect, they 404. Add a redirect in docs.json for every page whose URL moves. Redirects are one line each and they protect every link you do not control — blog posts, forum answers, internal wikis, search results.

How many internal links should a page have?

As many as genuinely help the reader at that moment, and no more. Too few leave readers without context or a next step. Too many turn the page into a detour. A good default is to link the first mention of a concept and not repeat the same link further down the page.