Fixing Broken Links Reported by Docusaurus Builds
A Docusaurus build ends with a list like this, and on a large site the list is long:
[WARNING] Docusaurus found broken links!
Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
- Broken link on source page path = /docs/2.x/guides/auth/:
-> linking to ../configuration (resolved as: /docs/2.x/guides/configuration)
-> linking to /docs/api/client#retry (resolved as: /docs/api/client)
- Broken link on source page path = /docs/next/migration/:
-> linking to /docs/2.x/old-page
The report is accurate but not always intuitive: "resolved as" paths that look nothing like what the author meant, links that work in the browser but fail the check, and the same broken link reported once per version of the docs. This guide works through 214 broken links on a versioned Docusaurus 3.6 site with three doc versions and a blog, groups them by cause, fixes each group, and turns the warnings into build failures. It belongs to Docs Frameworks: Docusaurus, Starlight and VitePress.
Read the Report in Bulk
Run the build with the checks set to warn and capture the output:
npx docusaurus build 2>&1 | tee build.log
grep -c '\-> linking to' build.log
214 lines. Grouping them by the target and by the "resolved as" path shows patterns faster than reading them in order:
grep -oE 'linking to [^ ]+ \(resolved as: [^)]+\)' build.log | sort | uniq -c | sort -rn | head -30
Many links appeared three times — once in each docs version — so 214 reports came from 81 distinct broken links.
Cause 1: Relative URLs Instead of File Paths
[configuration](../configuration) is a URL, resolved relative to the current page's URL. Whether the current URL ends in a slash changes the result: from /docs/guides/auth/, ../configuration is /docs/guides/configuration; from /docs/guides/auth, it is /docs/configuration. The site had switched trailingSlash to true a year earlier, silently breaking 29 such links.
The fix is to link to the Markdown file, with its extension, relative to the current file:
See [configuration](../configuration.md).
Docusaurus resolves file links at build time to the target's final route, so they survive slug changes, trailing-slash settings and versioning. A codemod rewrote all 29: for each relative URL link, it looked for a file at the corresponding path with .md or .mdx and replaced the link when exactly one match existed.
Cause 2: Anchors That No Longer Exist
onBrokenAnchors (Docusaurus 3.1+) reported 21 links to headings that had been renamed. #retry had become #retrying-failed-requests when the heading was expanded. Two approaches fixed them: update the link to the new ID, or pin the heading's ID so it no longer depends on its text:
## Retrying failed requests {#retry}
Explicit IDs were added to the 40 most-linked headings across the site, found by counting anchor targets in the link report and in the search analytics. They now survive any rewording.
Three anchors were reported broken although they worked in the browser: they were created by a custom <ApiParam> component that rendered id attributes the link checker did not know about. Docusaurus's useBrokenLinks().collectAnchor(id) hook registers such anchors; adding one call to the component cleared them.
Cause 3: Links Across Versions
Older docs versions (1.x and 2.x) are snapshots. Pages in them linked to absolute paths such as /docs/2.x/old-page that had been renamed in later edits of the same version, or to /docs/api/client, which pointed at the current version's page and broke when that page moved. Two rules fixed these:
- Inside a versioned doc, link with relative file paths, which Docusaurus resolves within the same version.
- Never edit a removed page's URL into an old version; if a page is renamed in
current, leave old versions linking to their own copy.
Fourteen links were rewritten to file paths. Because each was repeated across versions, that cleared 42 reports.
Cause 4: Links to Pages That Are Not Built
Nine links pointed at pages that existed as files but were not built: drafts (draft: true, excluded from production builds), files under a folder prefixed with _, and API pages generated by a plugin only in the current version. Docusaurus correctly reports them in production builds and not in docusaurus start, which is why authors had not noticed. The links were removed or pointed at published equivalents; the generated API pages were linked only from the current version.
Cause 5: Absolute Links Without the Base URL
The site deploys under /sdk/ on a shared domain, with baseUrl: '/sdk/'. Eight links in React pages were written as <a href="/docs/intro">, which skips the base URL. In JSX, <Link to="/docs/intro"> adds it automatically; plain <a> does not. Replacing them with Link fixed all eight. The same applies to useBaseUrl for images and static files.
Make New Breaks Fail the Build
With the backlog cleared, the configuration changed from warning to failing:
// docusaurus.config.js
export default {
onBrokenLinks: 'throw',
onBrokenAnchors: 'throw',
markdown: { hooks: { onBrokenMarkdownLinks: 'throw' } },
};
(Before Docusaurus 3.8, onBrokenMarkdownLinks is a top-level option.) External links are not checked by the build; a scheduled lychee job checks them weekly and opens an issue, as in Checking Links in Pull Requests, so a third-party outage never blocks a docs deploy.
Making Link Fixes Part of Writing
Broken links come back if writers keep the habits that caused them. Two small changes in the authoring workflow kept the count at zero. The contributing guide's "linking" section now has one rule — link to the .md file, relative to the current file — with a before-and-after example, and the docs repository's pull request template includes a checkbox for it. More effective was editor support: the docs team uses VS Code, whose built-in Markdown language features complete file paths and flag links to files that do not exist. Enabling markdown.validate.enabled in the workspace settings shows broken file links as squiggles while typing, long before the build runs, and caught most new problems in the first weeks.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Broken link reports | 214 | 0 | Docusaurus build |
| Distinct broken links | 81 | 0 | Grouped report |
| Headings with explicit IDs | 3 | 43 | grep {# |
| Build failures from new broken links, 8 weeks | — | 11, all fixed in PR | CI history |
| 404s from internal referrers (edge logs) | 1,960 / week | 140 / week | Edge 404 log |
The last row is the reader-facing result: internal 404s fell by 93%. The remaining 140 per week came from old cached pages and external sites, tracked as in Logging 404s at the Edge.
Pitfalls & Rollback
- Linking with URLs instead of file paths. File paths survive slug, trailing-slash and version changes.
- Renaming headings that other pages link to. Pin IDs on frequently linked headings.
- Absolute links inside versioned docs. Use relative file links so each version is self-contained.
- Checking external links in the build. It makes builds flaky; schedule it instead.
- Rollback: setting the three options back to
warnrestores the old behaviour without undoing any link fixes.
Conclusion
Docusaurus's broken link report is accurate; the work is in reading it in bulk and fixing causes rather than lines. On a three-version site, 214 reports came from 81 links and five causes — relative URLs, renamed anchors, cross-version links, unbuilt pages and missing base URLs — and setting the checks to throw has kept the count at zero since.
FAQ
Why does Docusaurus report a link as broken when the page exists?
Usually because the link resolves to a different URL than expected. Relative links without a file extension are resolved against the current URL, so a link that works on a page with a trailing slash breaks on the same page without one. Linking to the Markdown file path with its extension lets Docusaurus resolve it to the correct route regardless.
What is the difference between onBrokenLinks and onBrokenMarkdownLinks?
onBrokenLinks checks links in the final rendered pages against the set of generated routes. onBrokenMarkdownLinks checks links written as file paths in Markdown that do not point to an existing file. Both can be set to throw, warn, log or ignore. A link can pass the Markdown check and still fail the route check, for example when it points to a file excluded from the build.
Does Docusaurus check links to anchors within pages?
Yes, since Docusaurus 3.1, through onBrokenAnchors. It reports links to a heading ID that does not exist on the target page. Headings with custom IDs and anchors created by components need the component to register the anchor, otherwise they are reported as broken even though they work in the browser.
Should broken links fail the production build?
Yes, for internal links. Set onBrokenLinks and onBrokenAnchors to throw so a pull request cannot introduce a broken internal link. External links should be checked separately on a schedule, because their failures are outside your control and would make builds flaky.
Related
- Parent: Docs Frameworks: Docusaurus, Starlight and VitePress — Docusaurus among the frameworks.
- Versioned Documentation with Docusaurus — how versions create cross-version links.
- Speeding Up Docusaurus Builds With Rspack — making the build that runs these checks faster.
- Crawling for Broken Links on a Schedule — external link checks after deploy.
- Keeping Redirects Working After an SSG Migration — redirects for links you cannot update.
Filed underFixesDocusaurus