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*
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
slugin 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
idbased on the filename (slugified) 4. - Custom Behavior: If you provide a
slugin 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-03slug: "astro-rocks" # <- This custom slug becomes the URL segmentdescription: "A quick intro to Astro"---Slug vs. ID Comparison
| Property | Source | Behavior | Best For |
|---|---|---|---|
id | Filename | Automatically generated and slugified by Astro’s glob() loader 4. | Simple blogs where file names match URLs exactly. |
slug | Frontmatter | Manually 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].astrois sufficient.
The getStaticPaths Function
Because Astro builds static sites by default, you must export getStaticPaths() to tell Astro which routes to build 3.
---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.
4. Writing Cross-Links Inside Markdown
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.
5. Advanced Patterns: Link Helpers
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
---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
| Issue | Symptom | Solution |
|---|---|---|
| Relative File Paths | Links 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 Slugs | URLs with slashes (e.g., 2025/update) return 404s. | Ensure your page filename uses the rest parameter syntax: [...slug].astro 2. |
Missing getStaticPaths | Links 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:
- Define your slug explicitly in the frontmatter if you want custom URLs 2.
- Configure your route using
[...slug].astroto handle all slug variations 2. - Write links as root-relative URLs (
/blog/my-slug/) rather than file paths 1. - Test links in the browser, as Astro does not validate Markdown links at build time.
References
Footnotes
-
Astro Docs. “Pages – File-based routing – Link between pages.” https://docs.astro.build/en/basics/astro-pages/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Astro Docs. “Content collections.” https://docs.astro.build/en/guides/content-collections/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11
-
Astro Docs. “Routing – Dynamic routes.” https://docs.astro.build/en/guides/routing/ ↩ ↩2 ↩3
-
Astro Docs. “Content Collections API Reference –
astro:contenttypes >CollectionEntry>id.” https://docs.astro.build/en/reference/modules/astro-content/ ↩ ↩2
Other Ideas