Migrating my blog to Astro - Blog posts
Blog posts are the main ingredient of a blog. How do we generate the web pages for each post while preserving links so that we don't 'break the web', all while still fitting within the new Astro structure?
This is part of a series of posts on how I migrated my blog to Astro
In regards to blog posts, remember the original files look like src/posts/2025/2025-05-13-bbs.md
, but you will navigate to the corresponding webpage at /2025/05/bbs
.
By default Astro will create an output file for each file under /src/pages
. So If I had /src/pages/2025/bbs.md
then it would generate the desired path. But as I mentioned, I’m storing them in /src/posts/
. I could completely change my naming convention, but I was used to the existing one, so would prefer to stick with it if possible.
What I really wanted was for Astro to generate the YYYY/MM output directory structure dynamically.
Eventually I figured out that by leveraging Astro’s dynamic routes feature, I could do just that.
We create the curiously named src/pages/[...slug].astro
file. The ...
prefix is signifiant here. It’s a rest parameter.
---
import { getCollection, render } from "astro:content";
import { DateTime } from "luxon";
import MarkdownPostLayout from "../layouts/MarkdownPostLayout.astro";
import type {
InferGetStaticParamsType,
InferGetStaticPropsType,
GetStaticPaths,
} from "astro";
import onlyCurrent from "../scripts/filters";
export const getStaticPaths = (async () => {
const posts = (await getCollection("blog"))
.filter(onlyCurrent)
.sort((a, b) => {
return DateTime.fromISO(a.data.date).toMillis() -
DateTime.fromISO(b.data.date).toMillis();
});
return posts.map((post) => {
return ({
params: {
slug: post.id
},
props: {
post: post
}
});
});
}) satisfies GetStaticPaths;
type Params = InferGetStaticParamsType<typeof getStaticPaths>;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { post } = Astro.props as Props;
const { } = Astro.params as Params;
if (!post) {
throw new Error("Post not found");
}
const { Content } = await render(post);
---
{
post && (
<MarkdownPostLayout frontmatter={post.data}>
<Content />
</MarkdownPostLayout>
)
}
As the Astro documentation states:
a dynamic route must export a
getStaticPaths()
that returns an array of objects with a params property. Each of these objects will generate a corresponding route.
The params
property sets the slug
value to the post.id
. This will be the value defined by the generateId
function that we implemented in the blogs content collection.
The props
property is used to pass through the actual post.
Blog post layout
The blog post page layout MarkdownPostLayout
is defined in src/layouts/MarkdownPostLayout.astro
It’s not particularly complicated. The actual Markdown content from the post is rendered via the <slot />
element.
One interesting thing is how we can get access to the <head>
element to be able to set the description metadata. That’s actually set back up in the BaseLayout.astro
layout. It defined a named slot, that we can access here by including the slot="head"
attribute. That way we can opt in to setting the description to a unique value for each blog post page, but not interfere with the ability of other pages that might use the BaseLayout
component and set the description separately.
---
import DateTimeComponent from "../components/DateTimeComponent.astro";
import BaseLayout from "./BaseLayout.astro";
import { Disqus } from "astro-disqus";
interface Props {
frontmatter: {
title: string;
date: string;
description?: undefined | string;
tags: string[];
};
}
const { frontmatter } = Astro.props;
---
<BaseLayout pageTitle={frontmatter.title}>
<slot name="head" slot="head" />
{frontmatter.description && (
<meta name="description" content={frontmatter.description} slot="head" />
)}
<h1>{frontmatter.title}</h1>
<p><DateTimeComponent date={frontmatter.date} /></p>
<p><em>{frontmatter.description}</em></p>
<div class="tags">
{
frontmatter.tags.map((tag: string) => (
<p class="tag">
<a href={`/tags/${tag}`}>{tag}</a>
</p>
))
}
</div>
<slot />
<Disqus
embed="https://davidgardiner.disqus.com/embed.js"
url={Astro.url.toString()}
/>
</BaseLayout>