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-heading won'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.

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 background
  • sleep 2 - waits for the server to be ready
  • lychee --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#

FeatureZolaAstro + lychee
Built-in validationYesNo
Fragment checkingYesYes (with setup)
Requires serverNoYes
SpeedFast (build-time)Slower (runtime)
External link checkingYesYes
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!