Thoughts .toString()

Astro: Customizing content routes


Perhaps we want to organize our content file structure differently, or we don’t want to have to keep track of how we name files, or we don’t want to type a post title in two different ways. Each of these situations merit considering a post file naming scheme besides the Astro default.

For example, suppose I have a blog post named “My First Blog Post”. Using the blog starter template, I would create a /src/content/blog/my-first-blog-post.md, and then enter the title in the frontmatter. The title would be used in the header of the post, but its route would be determined by the file name as stored in post.data.id. For my first several posts, I want to organize them in the file structure, maybe, in a subdirectory so that they’re grouped by project, but I don’t want this structure publicly exposed. So perhaps my file name is /src/content/blog/first-project/01.md, but I still want the route to be /2024-12-10-my-first-blog-post/.

To achieve this, we make use of a tool to convert the title into URL format: npm install slugify. This should make it easier to write a helper function to convert post metadata to a slug.

typescript
1import type { CollectionEntry } from 'astro:content';
2import slugify from 'slugify';
3
4type BlogPost = CollectionEntry<'blog'>;
5
6export const slugifyPost = (post: BlogPost): string => {
7    // Ex. toISOString() => 2024-12-28T08:00:00.000Z
8    const date = new Date(post.data.pubDate).toISOString().split('T')[0];
9    const title = slugify(post.data.title, {
10        lower: true,
11        strict: true
12    });
13    return [date, title].join('-');
14};

To generate dynamic routes in Astro, we need to use GetStaticPaths to map out all the routes according to a pattern. In this case, since we want the route to end in a slug, we first create the page /src/pages/[slug].astro. The word in between the square brackets can be used as a parameter name.

/src/pages/[slug].astrotypescript
1---
2import { type CollectionEntry, getCollection } from 'astro:content';
3import BlogPost from '../../layouts/BlogPost.astro';
4
5export async function getStaticPaths() {
6	// getCollection replaces import.meta.glob
7	const posts = await getCollection('blog');
8	return posts.map((post) => ({
 		params: { id: post.id } 
9		params: { slug: slugifyPost(post) },
10		props: post,
11	}));
12}
13
14// Render Markdown from post
15const { Content } = await render(post);
16
17// Specify types to props (e.g. to pass metadata to post header)
18type Props = CollectionEntry<'blog'>
19const { ...post } = Astro.props;
20---
21<BlogPost {...post.data}>
22	<Content />
23</BlogPost>

And then in the layout, we just use what we need.

/src/layouts/BlogPost.astroastro
1---
2import type { CollectionEntry } from 'astro:content';
3
4// The type of post.data that we passed in. In the blog starter template,
5//   it is { title, pubDate, updatedDate?, heroImage? }
6type Props = CollectionEntry<'blog'>['data'];
7const { title, pubDate, updatedDate, heroImage } = Astro.props;
8---
9// ...abbreviated example
10<h1>{ title }</h1>
11<slot />

Now this…

/src/content/blog/some-file-name.mdmarkdown
1---
2title: 'My first blog post'
3pubDate: 'Dec 10 2024'
4---

will have the route of /2024-12-10-my-first-blog-post/.