Typesafe Markdown With Astro Content Collections
Typesafe Markdown might sound like an oxymoron, but with the new content collections released in Astro 2.0, you can now specify a schema for your Markdown frontmatter using Zod and get all the delicious validation and autocomplete that comes with it.
If you want to learn how to get typesafety and autocomplete into your Markdown blog, we’ll go through the whole process of creating a brand new blog powered by Astro content collections in this article.
If you prefer video, I’ve got you covered
If you prefer learning by watching something get built, I recorded building the demo app in this post as a video tutorial. Learn about Astro content collections and typesafe Markdown on YouTube.
Skip to the end
If you’d rather jump straight to looking at the source code, you can find it here:
- Repo: https://github.com/learnwithjason/astro-content-collections
- Demo: https://astro-content-collections.netlify.app
Step 1: Set up a new Astro site
To get started, create a new Astro site:
This command kicks off a guided experience (complete with a cute robot mascot) that walks through setting up a new Astro site.
You will be asked to:
- Choose a name for the directory where the project will live
- Select “an empty project” so we can focus on content collections.
- Install npm dependencies.
- Initialize a new git repository
- This is only necessary if you want to deploy this site. If you’re just playing with content collections to learn, you can skip this step.
Once the site is created, move into the project directory:
Start the project to make sure everything is running as expected:
Open up localhost:3000
in your browser and you should see this bare-bones page:
Open the project in your code editor of choice and you’re ready to code!
Step 2: Add a layout
To give your blog a cohesive feel, create a layout for the site at src/layouts/default.astro
that will be shared across all pages:
Edit the home page to use the new layout
Update the home page to use the layout by editing src/pages/index.astro
:
Run npm run dev
to see the updated site using the layout:
Step 3: Define a content collection schema
Create a new directory at src/content/
— this is both where our content collections are defined and where the content of our blog posts will live.
Create a config file at src/content/config.ts
:
Decide what fields you want in your blog metadata
For our blog, we want the following metadata to be supplied:
- Draft status — we don’t want to show work-in-progress posts on the live site
- Publish date
- Title
- Category — each post needs to have exactly one category, and we only want to allow two categories: “food” and “wisdom”
- Tags — posts can optionally add tags for grouping posts together
- Sharing details — for social sharing cards, search results, and other external services, each post can specify custom details:
- Image
- Title
- Description
In a standard Markdown blog, this is a lot of metadata and we’d almost certainly forget things, add extra stuff, or otherwise make a mess of our frontmatter. That’s what Astro’s content collections are here to help us solve: we can now be strict about what frontmatter is allowed and required for each post and provide helpful error messages if things aren’t set up properly!
To do that, we’ll use the defineCollection
helper and Zod, a schema validation library that’s included as z
for convenience.
In src/content/config.ts
, define your blog schema:
Zod provides a clear API for defining how our frontmatter should look.
For the most part, Zod works by defining the property of the schema and using one of Zod’s types as the value. For example, title: z.string()
lets the schema know that a title must be set and it must be a string.
For more specialized use cases, we can add defaults and transforms, as well as marking things as optional. This is done using schema methods.
For example, the draft
field can default to false
so that the field can be omitted on publishable posts. We specify that by chaining .default(false)
onto the property definition.
The date
field will be used as a JavaScript Date
object, so we can use the .transform()
helper to convert the string representation of the date in frontmatter into a Date
object for use in our code.
The share
object uses .strict()
to ensure that no additional fields are added to it.
Step 4: Show a list of blog posts on the blog home page
With the definitions in place, we can start using the collection to display blog posts on our site! Update src/pages/index.astro
to pull in our blog collection and display it on the home page:
The getCollection
helper allows us to pull in everything in the blog
collection, which we then filter to show posts where draft
is true
in development mode, but not in production.
In the page body, we map over the loaded posts and add markup to display a preview of each post.
If we save this, nothing changes in the browser. This is a good thing: we haven’t added any blog posts yet! Empty collections don’t throw errors. We could add logic to check for an empty posts
array and show an empty state, but in our case we’re going to publish a couple blogs right away, so we won’t need it.
Step 5: Create your first blog posts
Create your first blog post by adding the following to a new file at src/content/blog/cheese.md
:
The home page will now show the blog preview:
Create a second post by adding the following to a new file at src/content/blog/good-advice.md
:
After saving, the homepage is now showing an error!
The error message says:
What a great error message! We forgot to add a category
field to the frontmatter, and since that one is required the schema validation fails.
This is ideal, because without the typesafety we might not notice this until our build failed, or — worse — until a user let us know that our site is broken.
Edit src/content/blog/good-advice.md
to include category: wisdom
in the frontmatter, then save and check out the home page again. Everything works as expected!
Right now, though, if we click to read one of these posts we’ll get a 404. In the next section we’ll create individual blog pages from the collection.
Step 6: Create individual blog pages from an Astro content collection
Since we don’t want to create an individual page every time we write a blog post, we’ll use a dynamic route for our blog posts.
Create a new file at src/pages/blog/[slug].astro
and add the following:
Using getCollection
again, we load all the blog posts, then use getStaticPaths
let Astro know the slug and post data for each of our blog posts.
Then, we grab the post
out of the props and define our markup in the Astro page body.
To get autocomplete, we define an interface for Props
that uses the CollectionEntry<'blog'>
type for our post. This is incredibly handy because it means we can press control
+ space
to pull up a list of all the available properties on our post
object.
Loading the Markdown content of the post is done by calling await post.render()
, which returns an object including a Content
component that we can place wherever we want the post body to be displayed.
Save this, click on one of the blog posts, and you’ve got a fully functional, fully typesafe Markdown blog running using Astro content collections!
Resources and next steps
Congrats on building your first content collection-powered Astro site!
To see this in action and review the source code, check the following links:
- Repo: https://github.com/learnwithjason/astro-content-collections
- Demo: https://astro-content-collections.netlify.app
Additional resources: