Using dynamic routes to organize blog posts in Astro
If you create a new Astro project with the installer, it will organize blog
posts as markdown files in a src/pages/blog/
directory. Each post will have
some frontmatter, including a layout
field to set the frame in which the
content will render.
Astro will create a new page for every markdown file in the src/pages/
directory, so adding a new post is as simple as a creating a new markdown file
in the blog/
subdirectory.
When you want to render blog content on another page, e.g. a homepage with the
last three posts, then you need to rely on two Astro APIs:
compiledContent
and/or
the Content
component.
These will give you the Astro-rendered content for a post that you can then add
to another page.
There are two issues that I encountered using these APIs:
-
MDX does not support
compiledContent
, so you can either omit.mdx
content or create your own workaround with rehype plugins. -
The
Content
component for a post includes the markup for the layout, so “embedding” it in another page results in a strange iframe-esque page within a page.
If you just want to render the content of md/mdx files, there doesn’t seem to be a workable solution with the current setup. But with a little refactor, you can have your cake and eat it too!
A new organization system
Rather than putting all of your content somewhere in the src/pages/
, instead
let’s create another directory at the same level as src/
called content/
.
This is where we will put all of our blog posts, maybe in a blog/
subdirectory:
content/
blog/
first-post.md
second-post.md
third-post.md
src/
astro.config.mjs
Now we’ll create a dynamic route for all blog posts in the src/pages/blog/
directory. This file will gather all of the posts from your new content/
directory create static paths for each:
---
// src/pages/blog/[slug].astro
export function getStaticPaths() {
const content = Astro.fetchContent("../../../content/blog/*.{md,mdx}");
return content.map((item) => ({
params: { slug: item.url.split("/").pop() },
props: {
...item,
// Any additional props
},
}));
}
---
<!-- Render your post -->
Astro will call
the getStaticPaths
function
and create a page for each item in the returned array with the given params.
We’ve added the item
as props for the page, so we can access
all of the data for each post.
Finally, we can remove the layout:
frontmatter from all of our posts in
content/blog/
and set the markup in our new dynamic route:
---
// src/pages/blog/[slug].astro
export function getStaticPaths() {
const content = Astro.fetchContent("../../../content/blog/*.{md,mdx}");
return content.map((item) => ({
params: { slug: item.url.split("/").pop() },
props: {
...item,
// Any additional props
},
}));
}
const {
Content,
frontmatter: { title },
} = Astro.props;
---
<h1>{title}</h1>
<Content />
Make sure to include post content in the <head>
element too, and add encoding
to ensure that it renders correctly.
With all of that in place, you are now able to render markdown or MDX blog post
content anywhere on your site with Astro’s Content
component; we’re doing it
right here in our [slug].astro
file!
In addition to resolving the issue above, I think I like this approach better because it keeps the content separated from the code, which feel like different concerns. As of publishing, I use it to organize notes, which are individuals bits of md/mdx content that are rendered on the notes page.
tl;dr
Astro’s default setup for organizing blog posts works well for a one-to-one relationship between content and pages. But if you want to have a many-to-one relationship with markdown and MDX, a better aproach may be layout-less posts with a dynamic route.
I hope that helps! Happy blogging 🚀