Linking Between Markdown Documents in Astro: A Practical Guide for Blog Authors

*How to reliably cross-link Markdown files when you’re using content collections and custom `slug` front-matter*

Deep Research AI

Author’s note: I’m trying to link between two pages in the raw markdown documents. How do I do that when the documents have a slug property?


Linking Between Markdown Documents in Astro: A Practical Guide for Blog Authors

How to reliably cross-link Markdown files when you’re using content collections and custom slug front-matter


Executive Summary

If you are building a blog with Astro content collections, you might expect standard relative file links (like [Next Post](../post-2.md)) to work automatically. However, Astro’s routing logic requires a different approach, especially when custom slugs are involved.

  • Root-Relative URLs are Required: Astro treats links in Markdown as standard HTML anchors. You must link to the final generated URL path (e.g., /blog/my-slug/), not the file system path 1.
  • Custom Slugs Override IDs: If you define a slug in your frontmatter, Astro uses that exact string for the URL, bypassing the default filename-based ID 2.
  • Dynamic Routes Must Match: To make these links work, your dynamic route file (e.g., [...slug].astro) must be configured to handle the specific shape of your slugs 2.

The following guide details how to configure your collections, build the necessary routes, and write resilient cross-links.


1. Overview: Astro’s Routing Fundamentals

Astro leverages a strategy called file-based routing. In the standard src/pages/ directory, every file automatically becomes an endpoint on your site 1. However, most blogs today use Content Collections (located in src/content/), which live outside the pages directory and do not generate routes by default 2.

To turn collection entries into pages, you must create a dynamic route file. This file acts as a template that generates a unique page for every entry in your collection 3.

Crucially, the URL for these pages is determined by the slug.

  • Default Behavior: If no slug is provided, Astro generates an id based on the filename (slugified) 4.
  • Custom Behavior: If you provide a slug in the frontmatter, Astro respects that value, allowing you to decouple your URLs from your filenames 2.

2. Defining a Content Collection with Custom Slugs

When using content collections, you can override the automatically generated id by adding a slug property to your Markdown frontmatter. This is similar to the “permalink” feature found in other static site generators 2.

Example Frontmatter

---
title: "Why Astro is Awesome"
date: 2025-11-03
slug: "astro-rocks" # <- This custom slug becomes the URL segment
description: "A quick intro to Astro"
---

Slug vs. ID Comparison

PropertySourceBehaviorBest For
idFilenameAutomatically generated and slugified by Astro’s glob() loader 4.Simple blogs where file names match URLs exactly.
slugFrontmatterManually defined string. Can contain slashes (e.g., 2025/post) 2.SEO optimization, migrating old URLs, or organizing content hierarchically.

3. Building the Dynamic Route

To make your cross-links work, you must ensure your dynamic route file is correctly capturing the slug.

The Route File

Create a file at src/pages/blog/[...slug].astro.

  • Note: Using the rest parameter [...slug] is recommended if your custom slugs might contain slashes (e.g., category/post-title) 2. If you only use simple strings, [slug].astro is sufficient.

The getStaticPaths Function

Because Astro builds static sites by default, you must export getStaticPaths() to tell Astro which routes to build 3.

src/pages/blog/[...slug].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
// Map every entry to a route using its slug
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />

In this setup, an entry with slug: "astro-rocks" will be generated at yoursite.com/blog/astro-rocks/ 2.


Once your routing logic is set up, linking between documents is straightforward. You do not link to the file path (e.g., ../post.md). Instead, you write standard HTML anchor links that point to the final URL path relative to your domain root 1.

The Correct Syntax

If your dynamic route is src/pages/blog/[...slug].astro, your link should look like this:

<!-- CORRECT: Links to the generated URL -->
Check out my [previous post](/blog/astro-rocks/).

The Incorrect Syntax

<!-- INCORRECT: Links to the file system path -->
Check out my [previous post](../content/blog/astro-rocks.md).

Why? Astro compiles Markdown to HTML at build time. It does not resolve relative file paths inside Markdown content to their final build destinations automatically. It simply passes the href attribute through to the browser. Therefore, the link must be a valid URL path 1.


If you want to avoid hard-coding /blog/ into every link (which makes refactoring difficult later), you can create a custom Astro component to handle the linking logic.

Create a LinkTo Component

src/components/LinkTo.astro
---
import { getEntry } from 'astro:content';
interface Props {
slug: string;
collection?: 'blog'; // extendable to other collections
}
const { slug, collection = 'blog' } = Astro.props;
const entry = await getEntry(collection, slug);
if (!entry) {
throw new Error(`Could not find entry with slug: ${slug}`);
}
---
<a href={`/${collection}/${entry.slug}/`}><slot /></a>

Usage in MDX

If you are using MDX, you can import this component directly into your content:

import LinkTo from '../../components/LinkTo.astro';
Read more about <LinkTo slug="astro-rocks">Astro features</LinkTo>.

This ensures that if you ever move your blog from /blog/ to /posts/, you only need to update the logic in one component.


6. Common Pitfalls & Troubleshooting

IssueSymptomSolution
Relative File PathsLinks result in 404s or point to non-existent file paths like /src/content/...Always use root-relative URL paths (e.g., /blog/slug/) 1.
Multi-segment SlugsURLs with slashes (e.g., 2025/update) return 404s.Ensure your page filename uses the rest parameter syntax: [...slug].astro 2.
Missing getStaticPathsLinks work in dev but pages don’t exist in the build.Verify that getStaticPaths returns an entry for every item in the collection 3.

Bottom Line

To successfully cross-link Markdown documents in Astro:

  1. Define your slug explicitly in the frontmatter if you want custom URLs 2.
  2. Configure your route using [...slug].astro to handle all slug variations 2.
  3. Write links as root-relative URLs (/blog/my-slug/) rather than file paths 1.
  4. Test links in the browser, as Astro does not validate Markdown links at build time.

References

Footnotes

  1. Astro Docs. “Pages – File-based routing – Link between pages.” https://docs.astro.build/en/basics/astro-pages/ 2 3 4 5 6

  2. Astro Docs. “Content collections.” https://docs.astro.build/en/guides/content-collections/ 2 3 4 5 6 7 8 9 10 11

  3. Astro Docs. “Routing – Dynamic routes.” https://docs.astro.build/en/guides/routing/ 2 3

  4. Astro Docs. “Content Collections API Reference – astro:content types > CollectionEntry > id.” https://docs.astro.build/en/reference/modules/astro-content/ 2