<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: murugu-21</title>
    <description>The latest articles on DEV Community by murugu-21 (@murugu21).</description>
    <link>https://dev.to/murugu21</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F646302%2F1ab3e616-8583-49cb-96a3-48d82c307c97.png</url>
      <title>DEV Community: murugu-21</title>
      <link>https://dev.to/murugu21</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/murugu21"/>
    <language>en</language>
    <item>
      <title>why I migrated from gatsby to astro?</title>
      <dc:creator>murugu-21</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:20:00 +0000</pubDate>
      <link>https://dev.to/murugu21/why-i-migrated-from-gatsby-to-astro-4e77</link>
      <guid>https://dev.to/murugu21/why-i-migrated-from-gatsby-to-astro-4e77</guid>
      <description>&lt;p&gt;Back in 2021, I wrote about &lt;a href="https://murugappan.dev/blog/gastby/" rel="noopener noreferrer"&gt;why I chose Gatsby for my blog&lt;/a&gt;. Five years later, this site runs on Astro. Here's why I switched, and everything that broke on the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ydtxrzkk25eyc64f5v0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ydtxrzkk25eyc64f5v0.png" alt="The old Gatsby blog as archived in 2023 — dark background, green monospace post titles, tag chips and a search bar, with " width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  State of Gatsby
&lt;/h2&gt;

&lt;p&gt;Gatsby was &lt;a href="https://www.netlify.com/press/netlify-acquires-gatsby-inc-to-accelerate-adoption-of-composable-web-architectures/" rel="noopener noreferrer"&gt;acquired by Netlify&lt;/a&gt; in February 2023, and active development on the framework stopped shortly after. &lt;a href="https://www.netlify.com/blog/gatsby-cloud-evolution/" rel="noopener noreferrer"&gt;Gatsby Cloud got folded into Netlify Cloud&lt;/a&gt; that August, the core contributors moved on, and most plugin maintainers abandoned their plugins. The framework isn't formally dead. &lt;code&gt;npm install gatsby&lt;/code&gt; still works, patches still trickle out, but the ecosystem around it is unmaintained, and the &lt;a href="https://github.com/gatsbyjs/gatsby/discussions/39062" rel="noopener noreferrer"&gt;community discussion&lt;/a&gt; has been going in circles for two years.&lt;/p&gt;

&lt;p&gt;For a nine-post blog you can ignore all that for a while. What pushed me over wasn't a dramatic break, it was a small one: a dependency bump that surfaced a plugin issue nobody was ever going to fix. I'd rather spend a weekend on a migration than a weekend on archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro
&lt;/h2&gt;

&lt;p&gt;I first heard about Astro from &lt;a href="https://www.youtube.com/watch?v=dsTXcSeAZq8" rel="noopener noreferrer"&gt;this Fireship video&lt;/a&gt;, around the same time I was picking Gatsby. What I liked then is what I liked now: &lt;a href="https://docs.astro.build/en/concepts/islands/" rel="noopener noreferrer"&gt;islands architecture&lt;/a&gt;. Zero JavaScript by default, hydrate only the parts that need it, use React (or Vue, or Svelte) for those parts when you actually need to. The old blog shipped a React bundle on every page to render a list of links and a search box. That's a lot of machinery for content.&lt;/p&gt;

&lt;p&gt;There were smaller reasons. Markdown posts as content collections instead of a data layer. Typed frontmatter, one folder per post, nothing in between. And GraphQL is on the death path. My day job is actively migrating away from it to good old REST APIs, and I wasn't going to keep a GraphQL layer alive just to render markdown files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2syrww2r8puno0q6kg1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2syrww2r8puno0q6kg1.png" alt="Diagram 1" width="800" height="1760"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why now, and why Cloudflare
&lt;/h2&gt;

&lt;p&gt;The hosting story goes back further than the framework story. I had already moved off GitHub Pages a while earlier. Back in college I did not have the money to buy a domain, and by the time I did I bought &lt;code&gt;murugappan.dev&lt;/code&gt; on Cloudflare because they sell domains at cost with no renewal markup. When I tried to point it at GitHub Pages, I found out the apex-domain setup wants &lt;a href="https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site" rel="noopener noreferrer"&gt;hardcoded IPv4 addresses&lt;/a&gt; in your DNS. After four years in B2B SaaS and an unreasonable amount of DR testing and reports, hardcoded IPs are a personal red flag, so the blog went to Cloudflare as well.&lt;/p&gt;

&lt;p&gt;Then the ecosystem made the decision for me. Cloudflare &lt;a href="https://astro.build/blog/joining-cloudflare" rel="noopener noreferrer"&gt;acquired Astro&lt;/a&gt; in January 2026, then &lt;a href="https://blog.cloudflare.com/voidzero-joins-cloudflare" rel="noopener noreferrer"&gt;VoidZero&lt;/a&gt; — Vite, Rolldown, Oxc, Vitest — that June. PartyKit, which runs the chat on this site, &lt;a href="https://blog.cloudflare.com/cloudflare-acquires-partykit" rel="noopener noreferrer"&gt;joined&lt;/a&gt; back in 2024. The VoidZero announcement landed five days before I started the migration; the framework, its build tooling and the platform it all runs on were suddenly under the same roof. At that point I was 100% onboard. My CSP was backing the project, and that was definitely going to develop deployment synergies.&lt;/p&gt;

&lt;p&gt;Two weeks later I upgraded to Astro 7 and got Vite 8 + Rolldown as a direct side effect. Synergies indeed.&lt;/p&gt;

&lt;p&gt;The other reason I was confident: this kind of migration has gotten cheap thanks to coding agents. It's the ideal problem for them. Nothing novel, a lot of grunt work, and every step verifiable against the site that already exists. Anthropic showed this at absurd scale when they &lt;a href="https://bun.com/blog/bun-in-rust" rel="noopener noreferrer"&gt;rewrote Bun from Zig to Rust&lt;/a&gt;: 535,000 lines in 11 days for an estimated $165,000 in API tokens. Sure, the code the agents write might not be top notch, but at least it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration
&lt;/h2&gt;

&lt;p&gt;I didn't start by prompting "port my blog". First I had the agent write a parity plan: everything the Gatsby site did, 24 features from image optimization to the offline service worker, each one mapped to its Astro equivalent. Then we ticked them off one at a time, a build and a browser pass after each. &lt;a href="https://github.com/murugu-21/murugu-21.github.io/blob/8f1310d/blog/docs/superpowers/plans/2026-06-10-gatsby-to-astro-migration.md" rel="noopener noreferrer"&gt;The plan is still in the git history&lt;/a&gt; if you want the full checklist. It's a decent template for this kind of port.&lt;/p&gt;

&lt;p&gt;The condensed version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gatsby did it with&lt;/th&gt;
&lt;th&gt;Astro does it with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gatsby-source-filesystem&lt;/code&gt; + &lt;code&gt;createFilePath&lt;/code&gt; for slugs&lt;/td&gt;
&lt;td&gt;Content collections with a glob loader, slug = folder name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft posts hidden in production, visible in dev&lt;/td&gt;
&lt;td&gt;A filter on the entry id in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React page + &lt;code&gt;AllPosts&lt;/code&gt; for search, tags, reading time&lt;/td&gt;
&lt;td&gt;React island at first; plain Astro components and ~30 lines of JS now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-remark-images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Astro's built-in image pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-remark-prismjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;markdown.syntaxHighlight: "prism"&lt;/code&gt;, same PrismJS theme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-remark-autolink-headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rehype-slug&lt;/code&gt; + &lt;code&gt;rehype-autolink-headings&lt;/code&gt;, same SVG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-remark-smartypants&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Astro's built-in smartypants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gatsby-browser&lt;/code&gt; + a webpack &lt;code&gt;__name&lt;/code&gt; shim for mermaid&lt;/td&gt;
&lt;td&gt;A dynamic &lt;code&gt;import("mermaid")&lt;/code&gt; on pages that need it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gatsby-ssr&lt;/code&gt; &lt;code&gt;setPreBodyComponents&lt;/code&gt; for the pre-paint theme script&lt;/td&gt;
&lt;td&gt;The same inline script, first thing in &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatsby components + &lt;code&gt;StaticImage&lt;/code&gt; for the bio and toggle&lt;/td&gt;
&lt;td&gt;React islands with plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;react-helmet&lt;/code&gt; for the SEO head&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BaseHead.astro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-plugin-feed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@astrojs/rss&lt;/code&gt; + markdown-it and sanitize-html&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-plugin-sitemap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@astrojs/sitemap&lt;/code&gt;, one sitemap for the whole site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gatsby-node&lt;/code&gt; &lt;code&gt;onPostBuild&lt;/code&gt; for &lt;code&gt;llms.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;llms.txt&lt;/code&gt; route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-plugin-manifest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A static &lt;code&gt;manifest.webmanifest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-plugin-offline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Offline caching dropped; the manifest stays, so the site is still installable, and a self-destroying &lt;code&gt;sw.js&lt;/code&gt; evicts the old worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gatsby-plugin-gtag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The gtag snippet inline in the head&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;typeface-*&lt;/code&gt; font packages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@fontsource&lt;/code&gt; imports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pages publish workflow&lt;/td&gt;
&lt;td&gt;Cloudflare Pages, later Workers + static assets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The blog moved into this repo next to the portfolio, one build command produced both sites, and 301s kept the old URLs alive. Halfway through I also folded both apps into a single Astro project, so now there's one build, one deploy, one sitemap, and &lt;code&gt;/blog&lt;/code&gt; comes from where a file sits instead of a &lt;code&gt;base&lt;/code&gt; config. That part had its own bugs, covered below.&lt;/p&gt;

&lt;p&gt;One more file before the bugs: &lt;code&gt;sw.js&lt;/code&gt;. The old site used &lt;code&gt;gatsby-plugin-offline&lt;/code&gt;, so anyone who had visited before had a service worker busy caching the Gatsby build. Skip the eviction and those visitors keep getting the old site forever, no matter what you deploy. The new &lt;code&gt;sw.js&lt;/code&gt; is ten lines. It unregisters itself on activate and reloads any open tabs. Easiest file in the migration to forget.&lt;/p&gt;

&lt;p&gt;Worth being precise about what got dropped, since I got it wrong myself at first: the offline cache, not the installability. The new site doesn't register a service worker at all, so there's no offline mode, but the manifest and icons are still there and you can still install it as an app. &lt;a href="https://developer.chrome.com/blog/update-install-criteria" rel="noopener noreferrer"&gt;Chrome stopped requiring a service worker for installation&lt;/a&gt; a while back, so the manifest is what matters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm303abg9wgeg4389qjlh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm303abg9wgeg4389qjlh.png" alt="Diagram 2" width="800" height="936"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs
&lt;/h2&gt;

&lt;p&gt;The port itself took about a day. The next few weeks went into making it not embarrassing. This is the part I actually want written down, because "the agent did it" is only half the story. It can keep a build green and pass tests, but it can't see, and a blog is a visual product. Every visual regression here was caught by me opening a page and staring at it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wk917oewmorrxyi873f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wk917oewmorrxyi873f.png" alt="Diagram 3" width="799" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bug list, straight from the commit log (links go to the fixes):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What broke&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post tables lost all their styling&lt;/td&gt;
&lt;td&gt;The rules lived in Gatsby's stylesheet and never made it into the port&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/7cca589" rel="noopener noreferrer"&gt;7cca589&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clicking an image in the react post landed on a 404&lt;/td&gt;
&lt;td&gt;Gatsby-era &lt;code&gt;[![alt](react.jpg)](react.jpg)&lt;/code&gt; self-links — Astro hashes the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; but the anchor stayed a raw relative path, and the zoom handler didn't &lt;code&gt;preventDefault&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/383afad" rel="noopener noreferrer"&gt;383afad&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoom grabbed the author avatar and the social icons&lt;/td&gt;
&lt;td&gt;The selector was page-wide instead of scoped to the article&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/006315d" rel="noopener noreferrer"&gt;006315d&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid diagrams couldn't be zoomed&lt;/td&gt;
&lt;td&gt;The SVGs live behind blob URLs, and zoom only understood &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/c070a3e" rel="noopener noreferrer"&gt;c070a3e&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every image was broken in RSS readers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;content:encoded&lt;/code&gt; kept post-relative &lt;code&gt;src="react.jpg"&lt;/code&gt;, and feed readers render the body detached from the post directory&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/cf2606b" rel="noopener noreferrer"&gt;cf2606b&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The theme toggle disappeared, console full of React error #130&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;react-toggle&lt;/code&gt; is CJS-only; moving the component under a &lt;code&gt;"type": "module"&lt;/code&gt; package flipped the default import to the whole &lt;code&gt;module.exports&lt;/code&gt; object, so React got an object where a component should be&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/4c7fdc1" rel="noopener noreferrer"&gt;4c7fdc1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your theme choice reset when you crossed between the blog and the portfolio&lt;/td&gt;
&lt;td&gt;The blog persisted a &lt;code&gt;theme&lt;/code&gt; string, the portfolio an &lt;code&gt;isDark&lt;/code&gt; JSON boolean&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/cb767e5" rel="noopener noreferrer"&gt;cb767e5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every missing &lt;code&gt;/blog/&amp;lt;url&amp;gt;&lt;/code&gt; showed the portfolio's 404&lt;/td&gt;
&lt;td&gt;Astro only treats a top-level &lt;code&gt;404.astro&lt;/code&gt; as the status page, so the blog's 404 never built to the path Cloudflare walks up to&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/1085bbe" rel="noopener noreferrer"&gt;1085bbe&lt;/a&gt;, &lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/b525b45" rel="noopener noreferrer"&gt;b525b45&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Half the redirects missed&lt;/td&gt;
&lt;td&gt;Cloudflare Pages matches paths exactly — &lt;code&gt;/slug&lt;/code&gt; and &lt;code&gt;/slug/&lt;/code&gt; are two different rules&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/0589222" rel="noopener noreferrer"&gt;0589222&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A formatter rewrote code inside six published posts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;semi: true&lt;/code&gt; in the merged config formatted the JavaScript inside markdown fences&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/135124a" rel="noopener noreferrer"&gt;135124a&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local build green, CI red with &lt;code&gt;MissingSharp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Astro's image service needs &lt;code&gt;sharp&lt;/code&gt;; my &lt;code&gt;node_modules&lt;/code&gt; had cached optimized images and the build image had none&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/3717f3e" rel="noopener noreferrer"&gt;3717f3e&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The blog index still shipped React&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AllPosts&lt;/code&gt;, &lt;code&gt;TagBar&lt;/code&gt; and &lt;code&gt;SearchBar&lt;/code&gt; were Gatsby leftovers&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/2d5bee7" rel="noopener noreferrer"&gt;2d5bee7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web fonts shifted the whole layout on load&lt;/td&gt;
&lt;td&gt;No font-metric fallbacks&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/967f0e7" rel="noopener noreferrer"&gt;967f0e7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every &lt;code&gt;git push&lt;/code&gt; died with exit 254&lt;/td&gt;
&lt;td&gt;The pre-push hook still ran &lt;code&gt;npm --prefix blog&lt;/code&gt;, and &lt;code&gt;blog/&lt;/code&gt; didn't exist anymore&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/murugu-21/murugu-21.github.io/commit/1085bbe" rel="noopener noreferrer"&gt;1085bbe&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Returning visitors would have been stuck on the old site&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gatsby-plugin-offline&lt;/code&gt; had registered a service worker at the old origin&lt;/td&gt;
&lt;td&gt;the self-destroying &lt;code&gt;sw.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of them are worth more detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 404 that wasn't.&lt;/strong&gt; Astro only special-cases a top-level &lt;code&gt;404.astro&lt;/code&gt;, so the blog's 404 page built to &lt;code&gt;dist/blog/404/index.html&lt;/code&gt;, and Cloudflare's &lt;code&gt;not_found_handling&lt;/code&gt; walks up to the nearest &lt;code&gt;404.html&lt;/code&gt;, which didn't exist under &lt;code&gt;/blog/&lt;/code&gt;. Every missing blog URL quietly served the portfolio's 404 instead. No build error, no failing test, nothing in &lt;code&gt;astro check&lt;/code&gt;. The file was there, just not where the platform looks. The fix emits &lt;code&gt;dist/blog/404.html&lt;/code&gt; as well. And then I made the build fail if the copy doesn't contain the blog's title marker. That little assertion turned out to be the most useful thing this migration produced: the same regression came back a month later during the consolidation, and this time the build stopped it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The vanishing theme toggle.&lt;/strong&gt; The toggle rendered as a blank spot and the console threw React error #130. &lt;code&gt;react-toggle&lt;/code&gt; is a CJS-only package, and its default export changes shape depending on the module type of the importing file. The component used to sit under &lt;code&gt;blog/package.json&lt;/code&gt;, which had no &lt;code&gt;type&lt;/code&gt; field, so the bundler gave us the class. Consolidating moved it under the root &lt;code&gt;package.json&lt;/code&gt; with &lt;code&gt;"type": "module"&lt;/code&gt;, which switches on Node's real ESM-from-CJS rules, and the default import became the whole &lt;code&gt;module.exports&lt;/code&gt;, an object. React got &lt;code&gt;{__esModule: true, default: Toggle}&lt;/code&gt; where a component should have been. The fix was one unwrap at the import site. Finding it was the expensive part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The images that 404'd on click.&lt;/strong&gt; Markdown from the Gatsby era wraps images in a link to the image file itself: &lt;code&gt;[![alt](react.jpg)](react.jpg)&lt;/code&gt;. Gatsby left that link alone. Astro optimizes the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; into a hashed asset under &lt;code&gt;_astro/&lt;/code&gt;, but the anchor's &lt;code&gt;href&lt;/code&gt; stayed a raw relative &lt;code&gt;react.jpg&lt;/code&gt;, which resolves to &lt;code&gt;/blog/react/react.jpg&lt;/code&gt;, a file that is never emitted. So clicking an image followed a dead link, because the zoom handler didn't &lt;code&gt;preventDefault&lt;/code&gt;. I dropped the wrapper links and made the handler bail out when an image sits inside an anchor, so the same markup in a future post zooms instead of navigating.&lt;/p&gt;

&lt;p&gt;Everything else in that table was the same story in miniature: the checks were green and the page was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Mobile before the perf pass&lt;/th&gt;
&lt;th&gt;Mobile after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blog index&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React post&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PartyKit post&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Desktop is 100 everywhere. The index ships zero React now — the list, the tag chips and the search box are plain Astro components plus maybe thirty lines of inline JS. For reference, the 2021 post's bragging right was 90+ on mobile, over a much simpler site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbep7xoeapmzk5oag9edp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbep7xoeapmzk5oag9edp.png" alt="The blog today, running on Astro — starfield background, the same SDE Journey title, filter chips for the new tag vocabulary and the chat launcher in the corner" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're just here to read, not much changed. Old post URLs 301 to &lt;code&gt;/blog/&amp;lt;slug&amp;gt;/&lt;/code&gt;, with and without the trailing slash. The feed is at &lt;code&gt;/blog/rss.xml&lt;/code&gt;. The posts themselves didn't move. That was the whole point of the parity checklist: a migration, not a redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I do it again?
&lt;/h2&gt;

&lt;p&gt;Yes, no question. This is the shape of work agents are good at: the destination is known, there's a test suite to lean on, and the middle is grunt work. What they can't do is look at the result. I found every visual bug in this post by opening a page and describing what looked off, in plain English, and the worst one, the 404, couldn't be seen at all until I checked the thing I least wanted to check.&lt;/p&gt;

&lt;p&gt;I did hit a lot of visual bugs and had to verify and re-prompt a lot, but overall I am very happy with the result, and I couldn't have done this in this much time without AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.netlify.com/press/netlify-acquires-gatsby-inc-to-accelerate-adoption-of-composable-web-architectures/" rel="noopener noreferrer"&gt;Netlify acquires Gatsby Inc.&lt;/a&gt; — February 2023&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.netlify.com/blog/gatsby-cloud-evolution/" rel="noopener noreferrer"&gt;Netlify on the Gatsby Cloud evolution&lt;/a&gt; — August 2023&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/gatsbyjs/gatsby/discussions/39062" rel="noopener noreferrer"&gt;“Is GatsbyJS officially dead?”&lt;/a&gt; — the long-running community thread&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://astro.build/blog/joining-cloudflare" rel="noopener noreferrer"&gt;The Astro Technology Company joins Cloudflare&lt;/a&gt; — January 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.cloudflare.com/voidzero-joins-cloudflare" rel="noopener noreferrer"&gt;VoidZero is joining Cloudflare&lt;/a&gt; — June 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.cloudflare.com/cloudflare-acquires-partykit" rel="noopener noreferrer"&gt;Cloudflare acquires PartyKit&lt;/a&gt; — April 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bun.com/blog/bun-in-rust" rel="noopener noreferrer"&gt;Rewriting Bun in Rust&lt;/a&gt; — the scale of what agents can port&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.astro.build/en/concepts/islands/" rel="noopener noreferrer"&gt;Astro islands architecture&lt;/a&gt; — the actual docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=dsTXcSeAZq8" rel="noopener noreferrer"&gt;Fireship on Astro&lt;/a&gt; — where I first heard about it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/murugu-21/murugu-21.github.io" rel="noopener noreferrer"&gt;This site's source&lt;/a&gt; — the migration, bugs and fixes included&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>astro</category>
      <category>gatsby</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
