Gatsby Astro Drupal

How I switched from Gatsby to Astro (While Keeping Drupal in the Mix)

Albert Skibinski
How I switched from Gatsby to Astro (While Keeping Drupal in the Mix)

Over five years ago, my wife embarked on a new adventure, starting her own company as a coach for introverts. Of course, she needed a website, which I agreed to build. It was the perfect excuse to try something new. Back then, I opted for Gatsby on the front end, paired with Drupal as an API and CMS. I even wrote about it.

Over the years, more than 250 blog posts were published on that site. While maintenance was minimal, I never fully warmed up to Gatsby. Over time, requests for changes trickled in, but making those adjustments grew increasingly difficult without undertaking substantial updates and extensive refactoring.

A small change...

Eventually, I decided to look into alternatives for building a new site. While I liked the concept of static site generators, I found Gatsby overly complex, and its GraphQL data layer too cumbersome. I considered Next.js, but that also fell short due to its growing complexity. Although I’d had good experiences with Remix, it didn’t support SSG (though that may be changing). I also didn’t want to be tied to React, keeping my options open for the future.

Astro

That’s when I came across the relatively new Astro, which ticked all the boxes. It’s delightfully straightforward, with excellent documentation and a JSX-like syntax that feels familiar to React developers (although you can use “real” React/Lit/Vue/Svelte components, or whatever you prefer). Below is an example of a simple .astro component. You get a lot of flexibility and a pleasant developer experience.

---
import SomeAstroComponent from '../components/SomeAstroComponent.astro';
import SomeReactComponent from '../components/SomeReactComponent.jsx';
import someData from '../data/pokemon.json';

// Access passed-in component props, like `<X title="Hello, World" />`
const { title } = Astro.props;
// Fetch external data, even from a private API or database
const data = await fetch('SOME_SECRET_API_URL/users').then(r => r.json());
---
<!-- Your template here! -->

Everything above the --- is executed server-side.

Drupal remains the CMS and API

Data fetching was straightforward. I chose to create custom endpoints in Drupal for the JSON responses rather than using JSON:API or GraphQL. Why not JSON:API? It works fine, but in my experience, it often requires adding complexity on the front end, such as filtering collections, because you can’t retrieve data directly. GraphQL could move that complexity to the backend, but the Drupal GraphQL module is a heavy-duty tool and overkill for this use case.

Instead, I quickly set up a few controllers in PHP/Drupal, which kept the frontend simple. The GraphQL module was no longer necessary, and removing it, along with other modules and patches, significantly cleaned up the backend.

Drupal remains a solid CMS. My approach here was simplicity. I haven’t always had positive experiences with Layout Builder and Paragraphs. While they can work well for larger projects, they often feel like overkill for smaller ones.

For this project, I use a single CKEditor body field with embedded content. The backend splits the body into “components” that I then map and render on the frontend.

Multilingual

The site is multilingual, just like this one. Even so, the Astro structure can remain simple.

Astro folder structure

Astro supports dynamic routing, and most of the magic happens in [...path]. This is where all the pages are created.

[...path].astro

---
import Article from "@layouts/Article.astro";
import Page from "@layouts/Page.astro";
import Config from '../../../astro.config.mjs'

export async function getStaticPaths() {

    const locales = Config?.i18n?.locales || ['en'];

    // Initialize an array to hold all paths.
    const paths: { params: { lang: string, path: string }, props: { id: number} }[] = [];

    // For each locale, fetch your Drupal data and build the paths.
    for (const locale of locales) {

      const response = await fetch(
        `${import.meta.env.DRUPAL_API}/${locale}/api/nodes`
      );
      const nodes = await response.json();

      // Create a path for each node.
      nodes.forEach((node: Node) => {
        paths.push({
          params: {
            lang: locale,
            path: node.slug,
          },
          props: {
            id: node.nid,
          },
        });
      });

    }

    return paths;
}

const { id } = Astro.props;

// Fetch the individual node data using the id.
const response = await fetch(`${import.meta.env.DRUPAL_API}/${Astro.currentLocale}/api/node/${id}`);
const node = await response.json();

---

{ node.type === 'article' && (
  <Article post={node} />
)}

{ node.type === 'page' && (
  <Page page={node} />
)}

View Transitions API

This API enables smooth transitions between different page requests. As a result, the site feels more like an app, and with a bit of customization, you can add subtle animations. For example, if you navigate from the blog overview to a blog post, the image remains visible and enlarges or shifts into position while the rest of the page builds around it. While I generally believe animations should be used sparingly, I find this one quite nice. And it only takes a few lines of code in Astro.

Demo of view transitions in Astro

Performance

The old site had a perfect 100/100/100/100 Pagespeed score, and I wanted to maintain that. For styling, I’ve been using Tailwind for a while. It’s great for performance since it only bundles the classes you actually use. Astro handles most of the performance aspects for you, as it’s hard to beat static HTML/CSS in speed. With prefetching, page loads feel instantaneous.

Pagespeed perfect score 100
PageSpeed Insights

If your site uses third-party scripts, Partytown is a great tool. It loads scripts via a web worker rather than the main thread.

Islands architecture

What I find particularly appealing about Astro is the islands architecture. By default, Astro strips all client-side JavaScript from components. However, if you need an embedded JS application, you can easily hydrate it. For each component, you can specify whether it should be a client-side or server-side island.

Builds

The quietquality.nl site builds in a few minutes on a modest Linode server. It’s a reasonably-sized site with around 300 pages (across two languages). The build runs every hour, and a successful result replaces the old build. This works well for a site like this. For larger production sites, you might consider Incremental Static Regeneration (ISR), triggered from the CMS. But you can also address many dynamic content needs with the islands architecture.

Overall, Astro makes development a joy. It seems to have hit a sweet spot for many developers. I’m curious to see where it stands in five years. 😉

 

Albert Skibinski

About the author

  • Albert Skibinski is a freelance full-stack developer en co-founder at Jafix.
  • I write about web development, long bike rides and food!
Back to overview