Migrating to Astro: The Middle
Migrating to Astro (3 Part Series)
My adventures converting my old Next.js site to use Astro.
- 3 The End
Welcome back! Last time we walked through generating a new site, getting the blog post index page working, and adding the talks page. This time we’ll look at the individual blog post pages. There’s a good bit to cover here so let’s dive in.
I knew the index page and the individual blog pages were going to be the most complex. The index page actually wasn’t too bad; it was a little time consuming because I was porting so much over from React to Astro components but the complexity wasn’t high. The individual blog page was different. It wasn’t Astro’s fault. I had done a lot of custom things to my Markdown rendering pipeline that took more time to port than I realized. I started with getting the content rendering.
I had already moved all my
.md files to get the index page working, so there wasn’t anything to do there. However, I hadn’t brought over any images. I copied and pasted them into
src/assets/ as the Astro docs suggest, and updated all the paths in my Markdown files to point to those images using relative paths.
I then created a page at
[slug] part of the path acts as a placeholder. It will be populated with the post slug - a unique identifier Astro creates for each item in a collection - which I can use to fetch the page contents. Here’s the logic for generating the blog post pages and getting the data for each:
Since this one component is used to render many different pages, we need to tell Astro what pages those should be. In other words, how does Astro know which HTML files to generate given the posts content collection? We tell it exactly what to generate with
Astro runs this function when it first boots in order to know which pages it should generate.
getStaticPaths gets all the items from the posts collection and returns an array of what the params should be for each page. We’re returning the
slug param because our page path requires it. If our component had multiple placeholders, e.g.
src/pages/[year]/[month]/[slug], we would need to provide values for
slug. If we want to generate HTML for a subset of our posts, we can limit the list here as well. In fact, that’s what we’re doing with
filterPostCollection is a utility function that filters out unpublished drafts, encapsulating the filtering logic we applied on the index page. We tell Astro exactly what the slug should be for every HTML page it’s going to generate.
The rest of the logic is invoked when the pages are being rendered. The
slug variable we’re destructuring from
Astro.params is exactly what we provided in
getStaticPaths. With that
slug - again, the unique identifier for Astro - we can get all the data for a given post and render out it’s content. We can use that in the template portion of our Astro component:
At this point I was able to preview the blog pages and things looked decent. There were still a few missing features though. My images didn’t look right because I had added titles to many of them that should have been rendered in
<figcaption> tags but the titles were showing as weird text butted up against the images. The HTML structure for those wasn’t correct. I also had anchors that would display next to each header when the header was hovered. None of those were showing. Lastly, I used HTML comments in a few spots to apply inline styles to elements but those weren’t being applied. It took some Googling to figure out how best to handle this and I probably spent too much time trying to find the optimal solution when there were plenty that would work fine.
In the end, I used the exact same rehype1 plugins as I did in my old site. I installed
rehypeTitleFigure, a library to bring back the
<figcaption> tags for my images;
rehypeSlug, which got my heading anchors working; and
rehypeAttrs which re-applied the inline styles. Remark and rehype are incredibly powerful and it was smart for the Astro team to piggyback off of such a proven and widely-used ecosystem. After installing each of these libraries, I added them to
remarkExcerpt for a moment; we’ll get to that in the next section. For most of these, adding them to the
markdown.rehypePlugins array was all that needed to be done.
rehypeAutolinkHeadings needed a bit more configuration, but almost all of that was styling-related. Speaking of styling, on my old site, I used a library that allowed me to provide React components when HTML from Markdown was going to be rendered. This made it trivial, for instance, to add Tailwind classes to
<img> tags. But that was a React-specific library. To get the styles back, I added a
<style> tag in my Astro component and used Tailwind’s
@apply operator to add back the classes that were missing. It wasn’t my favorite approach. I would much rather be able to supply Astro components or HTML snippets instead of doing the
@apply hack. But it works. Now my pages looked exactly the same as they did before. Time to get the machine-readable parts of the page working.
On my old site, I had a single component that encapsulated all the SEO tags I needed for a blog post. I wanted to do the same in Astro. I copied over the
SEO component and immediately realized the way I was handling blog post excerpts was not going to work here. I needed the excerpts as strings for the
content attribute in multiple
<meta> tags. When I build the index page, I used a third-party package and passed the raw post body to a
PostExcerpt Astro component. I couldn’t use that to pass an excerpt string to a meta tag. Thankfully, I had solved this problem already in my old site. I created
src/remark-excerpt.mjs and added the following:
Let’s break down this plugin. First, I used
structuredClone to make a copy of the AST tree. Remark plugins typically mutate the tree directly and for this excerpt, I didn’t want to change what would ultimately get rendered. I wanted to operate on a copy. Next, I used
removeTags to remove any images from the tree. Alt tags and images titles should be a part of the exceprt.
stripMarkdown then removed all text formatting. I stringified the AST with
toString and trimmed it’s output, appending an ellipsis to the end2. I imported
astro.config.mjs and added it to the
Now that excerpts were working correctly, I had everything I needed to
Here’s where I ended up (with code comments removed):
post is passed in as a prop. I populated
category with the actual category collection entry if the post has an associated category. I also formated the reading time to make it more human-friendly. There’s probably a few tags I’m missing or don’t need anymore, but everything here helps my site render well, especially when I’m sharing it via Twitter or Slack. I particularly like the
twitter:data2 fields. These give the blog posts a nice two-column preview when shared via Slack.
One thing I did not do at this point was get my social images working, e.g. the
twitter:image tags. That was going to take a bit of work and I wanted to tackle other parts of the site first. I’ll dig into the social image in the next article. For now, the individual blog pages are done!
#Getting close to the end
Alright, we can all take a breath now! We covered quite a bit but we now have the individual blog pages fully working. Next time, we’ll add the remaining pages, get the RSS feed and social sharing images working, add the dark/light mode theme toggle, and put the final finishing touches on. See you then!
Remark and rehype are both parts of a larger project called
unifiedwhich works to provide structure to content and allow converting it from one format into another. Remark plugins convert Markdown into the unified AST. Rehype plugins manipulate that AST and eventually render it to HTML. ↩
I’ve since updated the excerpt extraction code. Turns out that
stripMarkdownalready had the ability to strip tags so I was able to delete the
removeTagsfunction. And I optimized the ellipsis logic to never break in the middle of a word. ↩