Migrating to Astro: The Beginning
Migrating to Astro (3 Part Series)
My adventures converting my old Next.js site to use Astro.
- 3 The End
My website hasn’t gotten much attention the past few years. In 2020, I slowly migrated from Gatsby to Next.js. I even had a partially-written blog post about it.1 But I was never happy with my setup. Next.js doesn’t have great support for content sites out-of-the-box. My primary pain point was converting Markdown content to HTML. I had to manually create a conversion pipeline using remark and wire the content up to pages as opposed to it being built into the framework. Next.js also had many features I didn’t need and seemed unnecessary for my use case.
I’m wanting to get more of my thoughts down on (digital) paper in 2024. Naturally, the first thing to do is redo my website instead of writing! I heard great things about Astro over the past year or so and thought I would give it a try. Initially, this whole process was meant to be a small trial to see if Astro was even worth considering. Turns out it was! And my website is now fully powered by it. What follows is my experience migrating my site over.
Astro has a project init script one can use with
npm create2. This makes spinning up a new project effortless. After removing the old site’s Next.js files, it was time to initialize my new Astro site.
For the prompts, I went with the default of including sample files, didn’t install dependencies (cause I was going to move the project files in a moment), selected TypeScript with the “Strictest” setting (cause that’s how I roll), and declined to create a new git repository. The init script created a new Astro site in the
temp/ directory with the following directory structure:
npm create doesn’t support generating files in an existing directory so I used
temp/, copied and pasted everything but
README.md and the
.vscode directory into the repo root, and deleted
temp/. I also added an asdf
.tool-versions file to define which versions of Node.js and PNPM to use. With that, I had an empty Astro site ready to populate.
My old site used Tailwind for styling and I liked my old site’s design so I wasn’t going to change much if anything from a styling perspective. Which brings me to one of my favorite parts of Astro: integrations. Astro provides a structured way for first- or third-party code to be added to an Astro site. All I had to do was run the
astro add command:
The command installed the Tailwind dependencies, created
tailwind.config.mjs, and made the necessary changes to
astro.config.mjs. That’s it. It was that easy.
git commit and move on.
At this point, I had Tailwind installed, but I wasn’t using it at all because the site still had the default files that come with a fresh Astro installation. The first order of business was to modify
src/layouts/Layout.astro to match the HTML structure of my old site. This was shockingly easy as Astro components give you 100% control over your HTML. It was also at this point I made the executive decision to use Astro components for everything. My previous site was all in React. Thankfully, simple React components - which almost all of mine were - are easily converted to Astro components. To get everything working correctly, I converted a few SVG icons I was using as well as a global
Link component, and added a few lines of Tailwind config to get the correct fonts in place. Lastly, I copied over the HTML from my old site layout into
My site layout was in place. It was time to get the home page content working. To do so, I needed to bring over all my posts and categories from the old site. This is when I started working with Astro’s Content Collections.
Since Astro is a framework for content-heavy sites, using Markdown to generate HTML is almost trivial. There are two main ways of doing this:
.mdpages to the
src/pages/directory which get converted directly to HTML pages, or
.mdfiles to subdirectories inside of
src/content/creating “Content Collections”.
These approaches can also be used in parallel which I do on my site. For my posts and categories, I needed Content Collections because they provide programmatic access to the content. With this approach, one needs to define each collection as well as provide a Zod-based schema to check data against.
Defining the collection schema is important because it both enforces a data contract for for a given type of content and provides TypeScript type completion/checking when accessing entry data. I needed to create two collections, one for my blog posts and one for categories my blog posts are attributed to. The blog posts were
.md files and the category data was stored in
.yml files. Astro handles both of these seemlessly. I created
src/content/config.ts and defined my collections like so:
A few things to unpack here. First, each collection has a type, either
.json files, respectively. The schemas for
content collections enforce the shape of the YAML frontmatter. For
data collections, the schema is checked against the entire file. Posts required a title, allowed a publish date, and allowed a reference to a category. References work similarly to foreign keys in relational databases. Categories required just a name. Now these collections can be fetched in Astro components via the
#Index page component
src/pages/index.astro and added the following logic and HTML to get all the posts to show on the page:
The first thing I did was use the
getCollection function to get all the blog posts. The second argument for
getCollection adds a filter. During development, we want to show all posts. Otherwise, we want to filter out unpublished posts. I base the “published” state on if the post has a publish date and the publish date is on or before today. Any missing dates or dates in the future get filtered out. The posts are then sorted by their publish date in descending order so the most recent is at the top of the list.
Next, I ported over the React components I used for each
ArticleBlock on the index page as well as add a remark plugin to get the reading time for each of my posts. Astro uses Remark/Rehype under the hood for it’s Markdown-to-HTML pipeline and allows one to add additional plugins to that pipeline. I copy/pasted the reading time recipe from the Astro docs and everything worked straight away. I had also written a custom Remark pipeline on my old site to extract an excerpt from Markdown, stripping away HTML tags and things like image alt text. Before bringing that over, I did a quick Google search and found someone had already written an Astro component for this. I used that component instead of my plugin.
I now had my index page with all the bells and whistles from the old site: componentized article blocks, reading time, excerpts, etc. It was time to get some other pages working.
I store all my talks in a single
.yml file with a consistent data structure. As we’ve already seen, Astro handles this easily. I copied
talks.yml from my old site, pasted it into the
src/content/talks/ directory3, and added the following to
Each talk has a title and abstract as well as a list of events where I’ve given the talk. I created
src/pages/talks.astro and added logic to get the talks data:
I then ported over my React code from the old site and cleaned up a bit of the structure. You can view the full template if you’d like to see the full HTML structure.
#Til next time
We’ve still got a ways to go but we’ll stop here for now. I’ll be walking through generating the individual blog post pages next time. There is a lot to cover there so I don’t want to rush through it. See you then! 👋
There is still much more to share but we’ll stop here for now. If you’d like to discuss anything I shared, chat with me about it on Twitter. Next time we’ll walk through generating individual blog post pages. See you then! 👋
If you’re wondering why I created a directory
talks/for the single file
talks.yml, it’s because Astro doesn’t yet support single-file content collections. Effectively, it doesn’t make a difference. I can call
getEntry('talks', 'talks')to get the talks data which isn’t any different from having built-in support for single-file collections. ↩