---
title: How to link documentation pages effectively
description: 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.

## Internal links

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](/docs/authoring/navigation) has the exact rules.

### Write the base prefix yourself

**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](/docs/site/addressing) lays
both rules out side by side.

### Avoid relative paths

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.

## Anchor links

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.

### Same page

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

### Another page

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

### How heading ids are formed

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.

### Accordions

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](/docs/reference/components/layout) for the component's props.

## External links

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.
```

## Best practices

### Write descriptive link text

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.

### Link prerequisites up front

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)
```

### Build topic clusters

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>
```

### Check links mechanically

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.

### Update links when you reorganize

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.

### Redirect moved content

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](/docs/site/redirects) covers the details, including when the `permanent` flag
matters.

## Frequently asked questions

<AccordionGroup>
  <Accordion title="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.
  </Accordion>
  <Accordion title="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.
  </Accordion>
  <Accordion 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.
  </Accordion>
  <Accordion title="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.
  </Accordion>
</AccordionGroup>
