In my previous post I covered how Zola validates internal links and anchors at build time. As I'm considering migrating my blog to Astro, I needed to solve the same problem: how do I ensure that fragment links (anchors) actually point to existing headings? Here's how I set up fragment link checking in Astro using lychee and rehype-slug.
The Problem with Fragments#
Unlike Zola, Astro doesn't have built-in internal link validation. Even
more problematic, Astro's default markdown processing doesn't add id
attributes to headings at all. This means:
- Fragment links like
#my-headingwon't work out of the box - There's no build-time validation that anchors exist
- Broken fragment links silently fail for readers
Because creating heading slugs is done in code, some changes compared to zola occurred and I wanted to catch and fix them.
Step 1: Adding IDs to Headings#
The first step is to ensure headings get id attributes. Astro uses remark
and rehype for markdown processing, so I installed rehype-slug:
npm install rehype-slug
Then configured it in astro.config.mjs:
import rehypeSlug from "rehype-slug"
export default defineConfig({
markdown: {
rehypePlugins: [rehypeSlug],
},
})
Now a heading like:
## My Important Section
Gets rendered as:
<h2 id="my-important-section">My Important Section</h2>
Step 2: Making Headings Linkable#
While not strictly required for validation, it's nice to let readers copy
heading links. I added rehype-autolink-headings:
npm install rehype-autolink-headings
And extended the config:
import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
export default defineConfig({
markdown: {
rehypePlugins: [
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: "append",
content: {
type: "text",
value: " #",
},
properties: {
className: ["astro-anchor"],
},
},
],
],
},
})
Now each heading has a clickable # link appended to it. The order matters
here - rehype-slug must come before rehype-autolink-headings because
the latter needs IDs to already exist.
Step 3: Validating Links with Lychee#
With headings now having IDs, I needed a way to validate that all fragment links actually point to existing anchors. Enter lychee - a fast link checker written in Rust.
brew install lychee # or cargo install lychee
The key is the --include-fragments flag, which tells lychee to verify
that anchor targets exist in the HTML:
lychee --include-fragments ./dist
But there's a catch. Lychee needs to actually fetch the pages to check fragments, which means we need a running server. I created an npm script that handles this:
{
"scripts": {
"check-links": "astro preview & sleep 2 && lychee --base-url http://localhost:4321 --include 'localhost:4321/blog' --exclude '.*' --include-fragments . ; kill $!"
}
}
Let me break down what this does:
astro preview &- starts the preview server in backgroundsleep 2- waits for the server to be readylychee --base-url http://localhost:4321- sets the base URL for checking--include 'localhost:4321/blog'- only check blog pages--exclude '.*'- exclude external links (we only care about internal)--include-fragments- the magic flag that validates anchors. ; kill $!- check current directory, then kill the background server
Comparing with Zola#
| Feature | Zola | Astro + lychee |
|---|---|---|
| Built-in validation | Yes | No |
| Fragment checking | Yes | Yes (with setup) |
| Requires server | No | Yes |
| Speed | Fast (build-time) | Slower (runtime) |
| External link checking | Yes | Yes |
| Link syntax | @/blog/post/index.md | /blog/post/ |
Zola's approach is more elegant - validation happens at build time with no extra tooling. But with Astro, the combination of rehype plugins and lychee provides comparable functionality. The trade-off is an extra build step and a slightly slower feedback loop.
Conclusion#
Fragment link validation in Astro requires a bit more setup than Zola, but
it's definitely achievable. The combination of rehype-slug for generating
heading IDs and lychee with --include-fragments for validation gives me
confidence that my internal links and anchors work correctly.
The nice thing about this approach is that lychee can also catch other link issues - like broken external links or incorrect paths. It's become part of my pre-deploy checklist for the Astro blog. Enjoy!