Logo with initials

Gatsby to Astro

──── 8 mins

Hello,

This is a nerdy post, it’s about how my website works under the hood. Last year, I moved this website from Gatsby to everyone’s favourite HTML framework: Astro.

Why Astro

I get a SSG like gatsby, with no JS by default, and none of the React issues.

Leave the React ecosystem

I wanted to leave the React ecosystem. When I first wrote my Gatsby blog, React was unstoppable. It was a good decision at the time, because that knowledge of Gatsby and Next.js helped to get jobs.

As someone who likes making demos, I prefer to be as close to writing vanilla JS as possible. There are whole books dedicated to getting React to work with D3 or WebGL. I don’t have time for this on my website.

Import all the things

With Astro, I can keep all of my existing React components. I have a full-time job and I didn’t want my rewrite to take 6 months.

With Astro, you can import everything. I’ve called it “the frameworks of all frameworks” for this reason. Right now, my interactive pages are built with Svelte, running alongside the exact same React components that I wrote for Gatsby 4 years ago.

Move to Remark

Gatsby and Astro both rely on a processor called Remark for markdown, I even made a custom plugin for it. But Gatsby relies on another gatsby-remark abstraction for its plugins. And a lot of these Gatsby-only plugins have gone stale.

I could have moved my Gatsby site to MDX, but which fixes this problem, I couldn’t get it to work. And the people behind MDX refuse to support HTML comments which means that I would have to migrate many of my existing articles. Again, I don’t have time for this.

Telemetry

More than 3 years ago, I called out Gatsby for sending telemetry data from your computer. I talked to the Astro team about this, and they listened. Astro shows it clearly and makes opting out very easy.

Telemetry

How I did it: Gatsby stuff

This a recap of everything, starting with things that are specific to Gatsby

Organise content with Markdown GraphQL request

My blog posts, book reviews and regular “markdown” pages were all organised via GraphQL requests, now I use astro.glob.

Replace gatsby-remark plugins

I used a Gatsby plugin called gatsby-remark-autolink-headers, which lets you automatically create clickable links to an html anchor on your headings, just like GitHub does on Markdown files.

{
  resolve: `gatsby-remark-autolink-headers`,
  options: {
    icon: `<svg aria-hidden="true" data-icon="anchor" width="24" height="27" viewBox="0 0 576 512"><path fill="currentColor" d="M12.971 352h32.394C67.172 454.735 181.944 512 288 512c106.229 0 220.853-57.38 242.635-160h32.394c10.691 0 16.045-12.926 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0l-67.029 67.029c-7.56 7.56-2.206 20.485 8.485 20.485h35.146c-20.29 54.317-84.963 86.588-144.117 94.015V256h52c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12h-52v-5.47c37.281-13.178 63.995-48.725 64-90.518C384.005 43.772 341.605.738 289.37.01 235.723-.739 192 42.525 192 96c0 41.798 26.716 77.35 64 90.53V192h-52c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h52v190.015c-58.936-7.399-123.82-39.679-144.117-94.015h35.146c10.691 0 16.045-12.926 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0L4.485 331.515C-3.074 339.074 2.28 352 12.971 352zM288 64c17.645 0 32 14.355 32 32s-14.355 32-32 32-32-14.355-32-32 14.355-32 32-32z"/></svg>`,
    className: `heading-anchor`,
  },
},

I solved it with:

  1. rehype-slug (to automatically add id attributes to your headings)
  2. rehype-autolink-headings (to create the links)
  3. hast-util-from-html (to insert my custom icon).
rehypePlugins: [
  rehypeSlug,
  [
    rehypeAutolinkHeadings,
    {
      behavior: 'prepend',
      content: fromHtml(
        '<svg width="24" height="27" viewBox="0 0 576 512"><path fill="currentColor" d="M12.971 352h32.394C67.172 454.735 181.944 512 288 512c106.229 0 220.853-57.38 242.635-160h32.394c10.691 0 16.045-12.926 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0l-67.029 67.029c-7.56 7.56-2.206 20.485 8.485 20.485h35.146c-20.29 54.317-84.963 86.588-144.117 94.015V256h52c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12h-52v-5.47c37.281-13.178 63.995-48.725 64-90.518C384.005 43.772 341.605.738 289.37.01 235.723-.739 192 42.525 192 96c0 41.798 26.716 77.35 64 90.53V192h-52c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h52v190.015c-58.936-7.399-123.82-39.679-144.117-94.015h35.146c10.691 0 16.045-12.926 8.485-20.485l-67.029-67.029c-4.686-4.686-12.284-4.686-16.971 0L4.485 331.515C-3.074 339.074 2.28 352 12.971 352zM288 64c17.645 0 32 14.355 32 32s-14.355 32-32 32-32-14.355-32-32 14.355-32 32-32z"/></svg>',
      ),
      properties: { ariaHidden: true, tabIndex: -1, className: 'heading-anchor' },
    },
  ],
],

Static directory

In Astro, the folder that is copied to the root of your live site for static files is the ./public/ folder. There are 2 problems with this:

  1. In Gatsby it’s the ./static/ folder.
  2. The public/ folder is actually where gatsby builds the files. So public was part of in my .gitignore .

Solution: Astro lets you change its “public” directory with the publicDir option in astro.config.mjs

// in astro.config.mjs
{
  publicDir: './static',
  // rest of your config
}

How I did it: general React stuff

Astro wants React code inside .jsx and not .js files

This was the first thing I had to do. I actually recommend doing this before creating another branch for your refactor. I will try to do this in all my React projects moving because it’s more explicit.

I renamed all my .js react files to .jsx using the find command and the rename CLI.

find ./src/components/ -name "_.js" -type f -exec rename 's/.js$/.jsx/' -- {} \;
find ./src/templates/ -name "_.js" -type f -exec rename 's/.js$/.jsx/'  -- {} \;
find ./src/pages/ -name "*.js" -type f -exec rename 's/.js$/.jsx/' -- {} \;

Inline SVGs

I used inline svgs for my icons, which was powered by ‘gatsby-plugin-react-svg’.

For my React components, I went from

const Icon = ({ url, title, icon }) => {
  const svg = React.cloneElement(icon, {
    role: 'img',
    'aria-hidden': 'true',
    width: '24',
    height: '24',
  });

   return (
     <a href={url} className="no-icon" title={title} rel={rel}>
      {icon}
     </a>
   );

to

// loading the icons as
import Feed from '../assets/icons/rss.inline.svg?raw'; // note the ?raw

const Icon = ({ url, title, icon }) => {
  const rel = url.includes('rss') ? 'me' : 'me nofollow';

  return (
    <a
      href={url}
      className="no-icon"
      title={title}
      rel={rel}
      dangerouslySetInnerHTML={{ __html: icon }}
    ></a>
  );
};

In Astro, all I had to do was this:

---
import mySvg from "../image.svg?raw";
---

<Fragment set:html={mySvg} />

React Helmet and programmatically updating <head> tags

This goes for Gatsby, Next.js, and everywhere else in the React ecosystem.

I solved it using named slots

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />

		<!-- 👇 -->
    <slot name="seo" />
  </head>
  <body>
    <div class={css.layout}>
      <Header />
      <main>
        <slot />
      </main>
      <Footer />
    </div>
  </body>
</html>

And in children component

<Layout>
  // 👇
  <Seo slot="seo" title="Welcome!" />
  <canvas id="regl-canvas"></canvas>
</Layout>

CSS modules

In Gatsby, CSS modules were turned into camel case, in Astro they are imported differently.

- <figure className={css.bookHeader}>
+ <figure class={css['book-header']}>

Dynamic HTML classes and loops

I went from

// SplitText.jsx

const SplitText = ({ text, className }) => {
  useEffect(() => {
    const elements = document.querySelectorAll(`.${className} span`);
  });

  const spans = text.split('').map((letter, i) => (
    <span aria-hidden="true" key={i}>
      {letter}
    </span>
  ));

  return (
    <h1 className={className} aria-label={text}>
      {spans}
    </h1>
  );
};

to

---
// SplitText.astro
const { text, className } = Astro.props;
---

<h1 class:list={[className, 'split']} aria-label={text}>
  {
    text.split('').map((letter, i) => {
      return <span aria-hidden="true">{letter}</span>;
    })
  }
</h1>

Both Gastby and Next.js use a <Link> component rather than the usual <a> tag. Astro and Svelte don’t.

- <Link to="/blog">Blog</Link>

* <a href="/blog">Blog</a>

How I did it: Other random stuff

Glslify

I use Glslify for all my 3D sketches. In the Gatsby site I used a babel plugin. I couldn’t find a Vite plugin but I found one for Rollup that works just as well.

import glslify from 'rollup-plugin-glslify';

Dates in frontmatter

Gatsby was lenient with my frontmatter data. Sometimes my dates were in quotes, sometimes not. Because of the type checks that Astro performs on Content Collections, my front matter data had to be consistent.

I ran a find-and-replace in VSCode, searching for this (with regular expressions enabled)

date: '(.*')

And replacing with

date: $1;

Search

The not so glorious parts

Dark Mode Flash

I remembered having to fix this exact problem when I added it to my Gatsby site. I used the same logic in the Astro site and for some reason I was still getting flashes.

Luckily I found a blog post by Axel Larsson with a one-line fix for this specific problem. I then opened a PR to the Astro docs to fix it there too. (Sorry Axel, all that SEO traffic is gone now 😅)

RSS feed

I used the official @astrojs/rss package to replace the official RSS plugin in Gatsby.

What I liked: lets you specify what goes into your RSS feed, much more explicitly than Gatsby.

Disliked: It’s up to you to a find a way to create the HTML that goes into the RSS feed. They’re essentially asking for a full markdown → HTML pipeline, and it doesn’t let you use the existing Astro config. It’s a pain but in a way it forces your code to be DRY.

Conclusion

As usual with these things, it’s been more work than I thought it would be, and I couldn’t have done it without Erika helping me. But I’m happy that I’m now free from the React ecosystem, and I can update my components bit by bit.

Thanks for reading, I hope that was useful.

1514 words

Share