Another Migration: From Gatsby to Astro

logarithmicspirals - Feb 1 - - Dev Community

Astronaut sprinting from the land of Gatsby to the land of Astro

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:

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:

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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

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.

. . . . . . . . . . . . . . . . . . . . .