You may have noticed we write a lot of blogs. Our team at DoltHub has been shouting at the internet about version controlled databases for over 6 years. When we started our blog, Gatsby was the popular choice for a React-based static blog. It delivered fast static pages, image optimization, a unified GraphQL data layer, a rich plugin/theme ecosystem, and familiar React developer experience. We’ve written many blogs about our experiences with Gatsby.
However, our blog setup using Gatsby was becoming increasingly difficult to maintain and we were hitting some technical limitations. We decided to migrate to Astro, a modern static site generator that promised better performance, simpler architecture, and improved developer experience. This blog details our migration journey, the challenges we faced, and the results we achieved.
Motivation
Our Gatsby-based blog had served us well initially, but we hit several roadblocks that made continuing with Gatsby unsustainable.
-
GitHub Actions build limits: Our Gatsby builds were growing so large that we were hitting GitHub Actions’ resource limits. We had to implement a hacky cleanup in our workflows to avoid upgrading to GitHub Enterprise, which would have been an expensive proposition for what should be a simple static site.
-
Long build times: Without caching, our Gatsby builds had grown to around 45 minutes, making any content updates or changes painfully slow. This was starting to impact our ability to iterate quickly on blog features and content.
-
Dependency update bottlenecks: There has been an open GitHub issue about React 19 support since December 2024 that has been largely ignored until recently, and has led people to believe that Gatsby is dead. The delay in React 19 support has also blocked us from upgrading other React-related dependencies, which is an issue for security and feature reasons.
-
Performance benefits: Astro’s “islands architecture” promised better performance by shipping zero JavaScript by default and only hydrating interactive components when needed.
-
Framework independence: Unlike Gatsby’s tight coupling to React, Astro gives us the flexibility to use any framework (or none at all) without being tied to their React version support timeline.
The combination of these factors made it clear that staying with Gatsby was no longer viable for our team’s needs and budget constraints.
The migration process
1. Initial setup using Cursor Agent
If you haven’t heard, we believe Dolt is the database for agents, so we’ve been testing the limits of the most popular agents to get a better idea of how people would use agents with Dolt.
Rather than manually migrating everything, we decided to leverage Cursor’s AI agent to handle the bulk of the migration work. There were clear areas where Cursor outperformed an engineer (me) doing the same work - moving the 1,000+ blog markdown files and updating the paths of the assets, initial setup and routing, and implementing basic components.
I pointed Cursor at the Gatsby blog code and asked it to implement all the same features. When it seemed like all of the React components, markdown, and images had been migrated, this is what the blog index page looked like:

You can see that the main functionality is there, but the styling is not even close to what our current blog looks like. Even when I pointed Cursor in the direction of the Gatsby blog’s CSS files I couldn’t get it to look anything like the blog you’re reading now.
Naive a-few-weeks-ago Taylor thought the difficult part of the migration was done and getting the visual design on par with the Gatsby blog would simply require some CSS changes. But this exposed a major flaw in the decision to use AI from the start of a big project like this.
While I had laid out a migration roadmap with Cursor and had a basic understanding of Astro and the changes required, I let Cursor do all the initial migration setup with little oversight. So when I went to make changes, I was unfamiliar with the new code base and decisions that were made. This likely made the time to do this migration longer than if I had not used AI from the beginning, but instead to implement specific features after I had the groundwork laid out.
2. Getting our React component libraries to work
Getting the blog to look like our original design turned out to be more difficult than expected. While Cursor had handled the content migration well, recreating the visual design required more manual work.
The biggest bulk of time went to getting our React component libraries to work with Astro. We have two React component libraries utilized by our blog:
- An older shared component library within the monorepo that houses all our web APIs, infrastructure, front end, etc. This is used to share components between our blog and dolthub.com.
- An open source React library which is utilized by many of our web applications in different repositories.
Initially, neither of these libraries worked. At first it was issues with the CSS in the internal component library (it uses BEM) and how it was exported. And then we were getting the dreaded React hydration error, which happens when the HTML generated by React on the server is different from the HTML generated by React on the client. Unfortunately, these errors gave limited information as to the source of what was causing it.
Some of the hydration errors were solved by using a client:only directive, and others by removing some code that caused the component to change after render. The most frustrating was a whole day lost to a Chrome extension that added HTML attributes to some elements on the client.
I was able to work through most of these errors, and some of the components you see on this blog now (the footer, buttons, links) are successfully imported from one of the React libraries. However, I never figured out what was causing the top navigation component to error, and ended up recreating that component within the Astro package. I plan to go back and investigate this further.
3. The smaller stuff
Once the main blog functionality (featured blogs carousel, search, article and code block styling) was done, there was a longer list of smaller to dos that required different implementations than Gatsby. These included:
- Astro plugins for an RSS feed and sitemap
- Astro would not process larger images or GIFs. We solved this by moving them from
src/imagestopublic, where they would not be processed by Astro. - Adding scripts for Google Analytics and Hubspot
- Implementing a custom 404 page
- Getting the correct URL for the default and individual article featured images for sharing on socials
4. CI
We installed plugins to get ESLint and Prettier working with our Astro blog. Cursor Agent was helpful here to get ESLint working with our shared configuration for Typescript and React.
5. Deployment
We needed to update our blog Dockerfile to point at the new Astro blog. Gatsby has a Docker image that simplifies the Docker configuration to build and host a Gatsby site.
For Astro, we needed to include our own NGINX configuration in order to serve the static site. Astro has a recipe for this in their documentation, but we needed to add some additional configuration to get our site working with a base path (/blog) and custom 404 page. This is what that looks like:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Compression for common static types
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Cache hashed assets aggressively (Astro puts fingerprinted files under /blog/assets)
location ^~ /blog/assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location /blog/health {
return 200 'LIVE';
add_header Content-Type text/plain;
}
# Blog-specific routing - serve from /blog directory
location /blog/ {
alias /usr/share/nginx/html/blog/;
index index.html;
try_files $uri $uri/ =404;
# Handle SPA-style routing for blog pages
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Use your Astro 404 under /blog
error_page 404 /blog/404.html;
# Serve that file but keep HTTP status 404
location = /blog/404.html {
alias /usr/share/nginx/html/blog/404.html;
internal;
}
location / {
try_files $uri $uri/ =404;
}
}
}
We are now able to deploy our new Astro blog with the same look and functionality as our original Gatsby blog!
Result
Here are some key performance and developer experience highlights from the migration to Astro:
- Build time: Reduced from 43 minutes to 8 minutes (with more room for improvement!)
- Lighthouse performance: Our Lighthouse score improved from 58 to 83 on desktop. It actually decreased on mobile, but we are optimistic we can get improve both desktop and mobile scores with some minor improvements.
- SEO score: The SEO score also improved from 85 to 92
- Bundle size: The Javascript bundle was reduced from 13MB to 10MB.
- Instant search: Search performance had been severely degraded with time and is now instantaneous
- Dependencies: Reduced from 53 to 39 packages
Future Work
We’re excited about the future of Astro and its improvements for the developer experience of our blog. We were impressed with the initial performance, but there are still some areas where we want to continue to improve.
- Image caching: The image processing step of the build process takes the biggest chunk of time. We want to implement a cache so that not all the images get processed every time the blog is built.
- Lighthouse scores: Our Lighthouse scores on desktop and especially mobile could use some improvement.
- Deduping code: We didn’t end up using some components from our components libraries due to hydration errors and we are planning on fixing this.
Conclusion
Migrating from Gatsby to Astro was a great technical decision for our blog. You as the reader may not notice a difference, but the big build time improvements (45 minutes to 13 minutes) and elimination of GitHub Actions resource limits alone justify the migration.
If you’re hitting similar limitations with Gatsby builds or dependency update bottlenecks, we recommend giving Astro a try. The performance benefits and developer experience improvements make it worth the effort.
Have you migrated from Gatsby to Astro or another static site generator? Have any questions about our blog? Reach out to us on Discord.
