Migration from Gatsby to Astro

I took a month of my free time to migrate this website from Gatsby to Astro. Why? After five years in the React bubble, I wanted a breath of fresh air in the chaotic frontend world.

The design philosophies of Gatsby and Astro diverge in notable ways:

The migration works started from individual pages. Transforming JSX to Astro pages proved fairly straightforward. The primary adjustment was to convert React-specific attribute names, like className to standard HTML attributes, class in this case. The markdown page in Astro was compiled by the remark and rehype pipelines, allowing direct use of their plugins without relying on Gatsby-specific gatsby-remark-transform-* plugins.

In the remainder of this post, let’s discuss the key tasks involved in the migration.

Define Collections

Gatsby loads ALL content and exposes the GraphQL API, giving developers significant flexibility in querying data sources and defining collections. For instance, I could store both blog and tldr entries in a single directory, then filter them using the kind attribute.

Astro, on the other hand, defines the collection though type matching, which implicitly enforces that each collection MUST have distinct types. I was unable to implement a multi-collection setup, but it was OK, as I wanted to fix the tech debt anyway.

Rework JavaScript

Given the content-driven nature of this site with minimal user interactivity, I opted to strip javascript from most pages, except for /blog/tags. Previously, the page was rendered with react-d3-cloud with tags directly passed as props. In Astro, however, data MUST be passed via web components with custom elements:

<Layout>
  <d3-cloud id="cloud" data-tags=`${ JSON.stringify(tags) }`></d3-cloud>
</Layout>
<script>
  import cloud from "d3-cloud";
  class D3Cloud extends HTMLElement {
    connectedCallback() {
      const tags: cloud.Word[] = JSON.parse(this.dataset.tags!);
      // ... ...
    }
  }
  customElements.define("d3-cloud", D3Cloud);
</script>

In the above code snippet, we define a web component <d3-cloud>, and bind it to D3Cloud element. The data-tags attribute is then passed as this.dataset.tags in the connectedCallback to pass from server-side to the client-side javascript namespace.

Since Astro builds a multi-page application, third-party stylesheets and JavaScript can be selectively included on specific pages, reducing the overall page payload and optimizing load times.

Image Optimization

The official Gatsby plugin, gatsby-transformer-sharp automatically downsizes images linked in Markdown files and generate responsive srcset attributes. Astro’s image optimization however, only applies to the .astro file when images are imported at compile time. Fortunately, Chirstian Ohanaja encountered the same issue, and developed a solution with astro-remark-eleventy-image to close the gap.

I also submitted a pull request to add relative path support, allowing Markdown files to reference images within the same folder.

However, the cover image used in the /tldr page are referenced in the astro file, but can not be imported in the build time. Since the images are referenced outside of Markdown processing, there isn’t an easy solution for this in Astro. Eventually, I followed a suggestion from the documentation, adding a check in src/content/config.ts and downsizing the images manually:

convert -resize 282px foo.jpg foo.jpg

It’s worth noting that ImageMagick’s convert command is not stable; rerunning it can resample the image inconsistently.

Syntax Highlighting

The syntax highlighting engine was upgraded to Shiki due to stalled development on Prism. A bonus point is Shiki supports TextMate syntax in JSON format, which is compatible to a popular library, iro. We can export the iro syntax highlight definition to TextMate XML format, then convert it to JSON format via TextMate Languages VSCode extensions to be consumed in Shiki.

I added minizinc, and apacheconf via this approach.

Jupyter notebook migration

The Machine Learning notes are crafted in Jupyter Notebook. In Gatsby, the notebook can be dynamically converted to Markdown format using the onCreateNode API. Astro, however, lacks a similar mechanism to inject generated content directly into Markdown collections, — or at least none that I’m aware of. The closest workaround is to generate content using the astro:config:setup hook, though the generated Markdown files may clutter the source directory.


It is worthy noting that the one-month migration is not typical: I also take advantage of the migration to pay off the tech debt accumulated over last seven years, notably: