Introduction
As a disclaimer, I'm going to gloss over a lot of things in this post. A LOT OF THINGS. I spent several weeks working on this migration, and at this point I just want to get the changes deployed and the post published 🙂.
Recently I decided to take the plunge and start learning a new frontend framework by using it to complete a substantial project. The project was to migrate this website away from Gatsby to something new. The problem was I had no idea what I would be migrating to.
The idea for this project was spurred by some issues I had been experiencing with Gatsby. I had previously wanted to figure out a way to add both captions and alt text to images inside of the markdown files I was using to write my posts. However, I found there wasn't a good, single solution to this problem in the Gatsby plugin ecosystem. While I was looking into possible solutions, I also observed things seemed to be stagnating. One new framework I had been seeing a lot about is Astro.
As of the time of publishing this post, my site has now been completely moved over to Astro. However, I would like to talk about some of the research I did before I started the migration.
Research and Discovery
During my research into frontend frameworks, I found the following:
- Gatsby seems to be dying.
- It was acquired by Netlify. Here's a video about what it seems to have done well: Why would ANYONE buy Gatsby??. There's a graph in the video showing downloads are dying. NextJS seems to have won the race.
- Google Trends show interest seems to be slowly dying down.
- The plugins, while useful, have some shortcomings. For example, adding useful information to images in markdown is difficult (figure and fig caption tags). I could probably get around this with JSON metadata, but I don't feel like doing that.
- Astro seems to be a rising star early in its lifecycle.
- There seems to be a lot of positivity around it:
- Astro just Launched.... Could it be the ultimate web framework?.
- Astro IS SO GOOD.
- Astro's New Features Have Me Rethinking Everything.
- In my opinion, the docs are really good.
- It's a static site generation framework, which makes it preferable for me over NextJS at the moment since I don't want to run a dynamic site right now.
For me, static site generation is very important. I would really like to avoid spinning up a server since that would raise costs and I don't make any money hosting this site (as of the time of writing this article). As such, NextJS wasn't really an option. There are other static site generation solutions out there, but I've tried different ones in the past and they're all fairly opinionated (in my opinion). I'm also somewhat tied to S3 right now since that's where I store the photos for my photo gallery.
Astro just seems to have a lot of good things going for it and it aligns really well with my own requirements for a new framework.
The Good
Some of the things I really like about Astro:
- Code fences.
- Importing markdown files into pages (I used this for my about page).
- Minimal amount of JavaScript delivered to the browser.
- Opportunity to break away from React (a simple blog shouldn't need it, but I needed to keep it for my gallery page).
- Rewriting dark mode for this site was super easy thanks to this tutorial in the documentation.
- Seemed easier for me to conceptualize components in Astro than Gatsby.
- Good documentation about migrating from Gatsby to Astro.
- Rewriting my popup for new posts was super easy (see below).
- There's documentation on deploying to Amplify.
- Amplify build time SIGNIFICANTLY decreased from over four minutes to less than two minutes.
- I was able to ditch the hacky
customHttp.template.yml
file approach I talked about in Migrating My Blog from DigitalOcean App Platform to AWS Amplify.
Rewriting the New Post Alert
Before starting this migration, I had implemented a new feature which is a popup announcing new posts to site visitors. I wrote about the creation of that feature in a post titled, Enhancing UI/UX in My Gatsby Blog: Introducing the New Popup Feature. What I found really cool about rewriting this in Astro was I able to completely ditch React, use vanilla JavaScript, and have all the code for the feature in one file.
Here's what the code looks like:
---
import { getCollection } from "astro:content";
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
const latestPost = posts[0];
const latestPostTitle = latestPost.data.title;
const latestPostPath = `/blog/${latestPost.slug}`
---
<div
id="new-post-alert"
class="new-post-alert message is-info m-6 hide jiggle"
data-latestPostPath={latestPostPath}
>
<div class="message-header">
<i class="fa-solid fa-bell fa-xl"></i>
New post!
<button class="delete close-new-post-alert" aria-label="delete" />
</div>
<div class="message-body">
<p>
<b>{latestPostTitle}</b>
</p>
<p>
Click <a href={latestPostPath}>here</a> to read more!
</p>
</div>
</div>
<script>
var latestPostPath = document.getElementById('new-post-alert')?.dataset.latestpostpath;
var path = window.location.pathname;
var lastViewedAlertPost = localStorage.getItem("lastViewedAlertPost");
var isCurrentPathLatestPost = latestPostPath === path;
if (lastViewedAlertPost !== latestPostPath) {
if (!isCurrentPathLatestPost) {
document.getElementById("new-post-alert")?.classList.remove("hide");
} else if (latestPostPath) {
localStorage.setItem("lastViewedAlertPost", latestPostPath);
}
} else {
document.getElementById("new-post-alert")?.classList.add("hide");
}
document.querySelectorAll(".close-new-post-alert").forEach(node => {
node.addEventListener("click", () => {
document.getElementById("new-post-alert")?.classList.add("hide");
if (latestPostPath) {
localStorage.setItem("lastViewedAlertPost", latestPostPath);
}
});
});
</script>
The interesting thing about the popup is how the server side data is passed to the client side. The latestPostPath
value is saved as a data attribute in the HTML using data-latestPostPath={latestPostPath}
. The client side code in between the <script></script>
tags is then able to access the data attribute and provide the feature's interactive functionality.
Rewriting the Dark Mode Toggle
In my previous post Creating a Blog with Bulma & Gatsby: The Power of Dark Mode, I talked about how I implemented a toggle switch that would allow users to change which mode they were viewing the site in. During the migration, I was able to ditch React for the toggle and go with a vanilla JS approach. My initial approach had some bugs, but I found this really good tutorial called Back on dry land. Take your blog from day to night, no island required! which helped me successfully migrate the toggle switch. With some slight modifications to the code in the tutorial, I ended up with the following component I called DarkModeToggle.astro
:
<div id="dark-mode-toggle-wrapper" class="field">
<input id="dark-mode-toggle" type="checkbox" class="switch is-rounded is-success is-rtl" />
<label for="dark-mode-toggle">
Dark Mode
</label>
</div>
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
var isLight = theme === 'light';
if (isLight) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
document.getElementById("dark-mode-toggle").checked = !isLight;
window.localStorage.setItem('theme', theme);
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
}
document.getElementById("dark-mode-toggle").addEventListener("click", handleToggleClick);
</script>
The main difference between my toggle switch and the one from the tutorial is the HTML. Since I'm using Bulma and it's switch extension, I have a different structure and CSS class names. For the contents of the script tag, the differences are these lines:
// ...
var isLight = theme === 'light';
if (isLight) {
// ...
}
document.getElementById("dark-mode-toggle").checked = !isLight;
// ...
document.getElementById("dark-mode-toggle").addEventListener("click", handleToggleClick);
Note that I'm using a different ID for the toggle and also have to set the toggle's input checked
value based on the theme.
While there are a lot of good things about Astro, I did encounter some speed bumps while doing the migration. These things were hard to predict. At a certain point, you can't make any more good predictions and it's best to just jump in.
The (Sort Of) Bad
Here were some of the pain points:
- Migrating the photo gallery page was complex.
- Migrating the table of contents component for blog posts required some custom code.
Migrating the Photo Gallery
The hardest part of the migration was redesigning my photo gallery's architecture. I've decided I'll go into more detail in a separate post about that, but the main issue was Astro's relatively simple approach to how it handles images.
By default, Astro supports local images or remote images. There are also community integrations, but those are more tailored to CMS solutions. Since my images are stored in an S3 bucket, I was kind of left to find my own approach to loading images on the gallery page.
After some research and experimentation, the approach I settled on was to put a CloudFront distribution in front of my image's S3 bucket, and write a bit of custom code to buid the gallery on the server side using the AWS SDK v3 for JavaScript. However, I'll just have to keep this vague for now and go into greater detail in a follow-up post.
Migrating the Table of Contents
Posts on my site feature a table of contents. The table of contents has links to the different headings on the page and features some interacitivity based on how the user scrolls. I had written about the creation of this feature in a previous post titled Designing a Dynamic Table of Contents. I was curious to see what it would be like to migrate this feature. As it turns out, it was relatively straightforward. I was able to completely ditch React during the migration. Now, the table of contents is defined by two Astro components: TableOfContents.astro
and TableOfContentsHeading.astro
.
However, the bad part of this was the migration was time consuming and I needed to do some research on how to build a nested table of contents structure from the headings. I ended up referring to this blog post titled Building a table of contents from Astro's markdown headings to help me come up with my own solution.
Due to the amount of code required to recreate the feature, I will not be going in depth on it in this post.
Conclusion
In the pursuit of migrating my website away from Gatsby, Astro emerged as a promising solution that aligned with my requirements. Its static site generation, markdown integration, and minimal JavaScript delivery were key advantages. While the transition brought benefits, such as streamlined code and the ability to rewrite features without React, challenges surfaced during the migration of complex elements like the photo gallery and table of contents. Despite these hurdles, Astro stands as a compelling choice for my new frontend framework, offering a balance of advantages and challenges. I look forward to further exploring Astro's potential as I continue to improve my website and adapt to the evolving web development landscape.