Linking Between Pages
This page explains how to write links between pages in this docs site so they resolve correctly and survive the build’s strict link checking.
Why this exists
Section titled “Why this exists”Astro and Starlight do not rewrite relative Markdown links on their own. Left alone, a relative link written in prose would render literally and 404 under the site’s base path (/groupsdrupalorg).
A build-time rehype plugin — astro-rehype-relative-markdown-links — does the rewriting for us. It is configured with the base path, so you can write simple relative links and the plugin resolves them to base-aware URLs. For example, a relative link to events.md is rewritten to /groupsdrupalorg/events/.
That means: write the simple, relative form. Never hand-write the base path or the slug yourself for a normal page.
The rules
Section titled “The rules”Linking to a .md or .mdx page
Section titled “Linking to a .md or .mdx page”Write a relative link that includes the file extension. For example, link to ./events.md. The plugin rewrites it to the correct base-aware URL at build time. Do not hand-write /groupsdrupalorg/events/ — let the plugin do it.
Linking to a .mdoc (Markdoc) page
Section titled “Linking to a .mdoc (Markdoc) page”Markdoc pages are invisible to both the rehype plugin and the link validator, so links to them cannot be auto-rewritten or validated. For these you must:
- Write a base-absolute URL including the base path and a trailing slash, for example
/groupsdrupalorg/ai-pipeline/. - Add that exact URL to
src/config/mdoc-link-exclusions.mjs— the shared listastro.config.mjsfeeds to the validator’sexclude.
Currently only two pages are .mdoc: ai-pipeline and link-report.
Sidebar navigation
Section titled “Sidebar navigation”The sidebar in src/config/sidebar.ts uses slugs, e.g. { slug: "events" }. Slugs are extension-agnostic and always work regardless of whether the page is .md, .mdx, or .mdoc. This is why a page added to the sidebar by slug needs no special handling for its format.
External links
Section titled “External links”Write external links normally, e.g. https://drupal.org. They are checked separately by lychee, which runs as a non-blocking step.
Examples
Section titled “Examples”Quick reference. Every entry in the middle column is the exact source you type.
| You want to link to… | Write this | Result |
|---|---|---|
A .md/.mdx page (e.g. team.md) | [the team](./team.md) | /groupsdrupalorg/team/ (plugin rewrites it) |
A .mdoc page (e.g. ai-pipeline.mdoc) | [AI Pipeline](/groupsdrupalorg/ai-pipeline/) + add to the exclusions list | link as written |
| Sidebar navigation | { label: "Team", slug: "team" } in src/config/sidebar.ts | /groupsdrupalorg/team/ |
| An external site | [Drupal](https://www.drupal.org) | as written (lychee-checked, non-blocking) |
Worked example: linking to a .mdoc page
Section titled “Worked example: linking to a .mdoc page”Because a .mdoc target can’t be auto-rewritten or validated, you write the full base-absolute URL in prose and register that same URL in src/config/mdoc-link-exclusions.mjs — a shared list that astro.config.mjs feeds to the validator’s exclude:
export const mdocLinkExclusions = [ "/groupsdrupalorg/ai-pipeline/", "/groupsdrupalorg/link-report/",];Add an entry here whenever you add a prose link to a new .mdoc page. A post-build guard (scripts/check-mdoc-exclusions.mjs, run by npm run build) then verifies every excluded URL still resolves to a real page — so an exclusion can’t silently rot if its target page is later renamed or removed.
Common mistakes
Section titled “Common mistakes”❌ [Team](/team/)Root-absolute without the base path — 404s under /groupsdrupalorg.
❌ [Team](team.md)Stale extension: the file is now team.mdx (or .mdoc). The strict validator fails the build.
✅ [Team](./team.md)Relative with the correct extension — the plugin rewrites it.
The safety net
Section titled “The safety net”The starlight-links-validator plugin runs with errorOnRelativeLinks: true.
The order of operations matters: the rehype plugin rewrites every valid relative link to an absolute URL before the validator runs. So by the time validation happens, any link still in relative form is one the plugin could not resolve — a broken or mistyped target. The build fails in that case.
The practical consequence: broken internal links cannot silently ship. If you mistype a target or link to a page that doesn’t exist, the build stops.
The one exception — links to .mdoc pages, which are excluded from that check — has its own guard: scripts/check-mdoc-exclusions.mjs (also run by npm run build) fails the build if any excluded URL stops resolving to a real page. So even the excluded paths can’t rot silently.
Two gotchas
Section titled “Two gotchas”Changing a page’s format breaks inbound links. Converting a page from .md to .mdx or .mdoc breaks any inbound links that still point at the old extension. The strict validator now catches these. If a page becomes .mdoc, switch its inbound links to the base-absolute form described above (and add the URL to src/config/mdoc-link-exclusions.mjs).
Base-absolute .mdoc links are the fragile ones. If the base path ever changes, the plugin-rewritten .md and .mdx links adapt automatically — but hardcoded /groupsdrupalorg/… links do not. Those must be updated by hand. Keep this in mind if the base path is ever changed.
