This article explains how socialanimal.dev moved its own marketing site from WordPress to Astro. The site now scores 100 on Lighthouse for Performance, Accessibility, Best Practices, and SEO. The approach relies on zero-JavaScript-by-default rendering, self-hosted fonts, and strict rules for third-party scripts. Any agency can use these same techniques.

Key takeaways

  • Astro's zero-JS-by-default design and island model are the main reasons socialanimal.dev hits Lighthouse 100 in every category.
  • Self-hosted, subset fonts, inlined critical CSS, and a strict limit on third-party scripts close the gap between a fast build and a perfect one.
  • The current stack pairs Astro 5 with Supabase for content, React islands for interactivity, and Vercel for hosting. It supports 1,797 English pages translated into 11 languages.
  • Moving off WordPress removes plugin-update risk and ongoing CMS work. Expect a multi-week project that covers content audit, rebuild, and redirect mapping.
  • The same pattern shows up in client work: see the bdManagedIT and SleepDr case studies for similar results.

Updated 15 August 2026: sources added, experience claims checked against our project record, summary added.

WordPress to Astro: How We Hit Lighthouse 100 on Our Rebuild

Why We Left WordPress

About 43% of all sites run on WordPress, according to W3Techs. It's not a bad platform, and we build WordPress sites for clients when it fits the project. But our own site is mostly static pages and a blog. WordPress was too much for that job.

Here's what our WordPress setup looked like:

  • Theme: Custom theme built on Sage (Roots.io)
  • Plugins: More than a dozen active plugins, including Yoast SEO, WP Rocket, Advanced Custom Fields Pro, and Gravity Forms
  • Hosting: A managed WordPress hosting plan
  • CDN: A separate CDN layer in front of the host
  • Build complexity: PHP templating, Webpack for assets, MySQL database

Even with heavy caching, our Core Web Vitals were mediocre. Largest Contentful Paint (LCP) ran over 2 seconds on mobile. Cumulative Layout Shift (CLS) was noticeable, if not terrible. Every plugin update carried a real chance of breaking something.

The real problem: hosting costs ran high for the modest traffic a brochure site with a blog actually needs.

The Breaking Point

A core update broke custom Gutenberg blocks tied to a specific Advanced Custom Fields Pro version. That break spread into theme and PHP compatibility issues. That's what pushed this migration from a someday project to a scheduled one. A routine update turned into a full day of troubleshooting, for a site that mainly serves static marketing pages and a blog.

Why We Chose Astro

We looked at four options for the rebuild:

Framework Pros Cons Our Verdict
Next.js We know it well, great ecosystem Overkill for a content site, requires server or edge runtime Too heavy
Astro Content-focused, ships zero JS by default, island architecture Smaller ecosystem, newer Perfect fit
Eleventy Simple, fast builds, mature Limited component model, less modern DX Close second
Hugo Blazing fast builds, single binary Go templating is painful, limited flexibility Not for us

We build Next.js projects for clients when a project needs dynamic features. One example is SleepDr's patient-facing platform, where Lighthouse went from 35 to 94 after leaving WordPress. Next.js is our default choice beyond a static marketing site. But for a content-heavy site like ours, Next.js ships a JavaScript runtime whether you need it or not. Even with static export, you still send React to the browser.

Astro's philosophy fit us well: ship HTML, add JavaScript only where you need it. Their island architecture lets you place a fully interactive React component next to plain static HTML. The static parts ship zero JavaScript. That's exactly what we needed.

Our team already had hands-on experience with Astro projects for clients, including bdManagedIT's rebuild. That project moved from WordPress to Astro, Sanity, and Netlify and now scores 95+ on PageSpeed. This wasn't a learning exercise. It was a tool we already trusted.

The Content Layer Question

One decision came early: how to manage content once the site grew past a handful of English blog posts. For client projects, we recommend headless CMS setups with Contentful, Sanity, or Storyblok. Our own site now spans more than a thousand pages across eleven languages, plus an AI content pipeline that scores drafts before they publish. So we built the content layer directly on Supabase instead of adding a separate CMS on top of Astro.

Supabase gives us Postgres tables for content, translations, and metadata. Astro queries these at build time through its content layer. Pages are still statically generated. There's no extra CMS subscription, no webhook plumbing between two vendors, and the content pipeline writes straight to the same database the site builds from.

The Migration Strategy

We didn't do a big-bang migration. Here's the phased approach we used.

Phase 1: Content Audit We exported the existing WordPress content with wp-cli and ran it through the same AI content pipeline we use on client projects. It scored each piece and flagged what was outdated, thin, or duplicated. Pages that weren't earning traffic got 301 redirects to relevant newer content instead of a straight migration.

Phase 2: Design System in Astro We rebuilt the component library as Astro components: buttons, cards, section layouts, navigation, all as .astro files. None of them need a JavaScript framework. They're plain HTML and CSS with scoped styles.

Phase 3: Page Build Home page, capability pages, about, contact, blog listing, individual posts, 404. We built all of these as Astro pages using the component library.

Phase 4: Performance Tuning This is where the Lighthouse 100 work happened. More on this below.

Phase 5: Launch and Redirect We set up 301 redirects for every old URL, checked them with Screaming Frog, submitted the new sitemap to Google Search Console, and flipped DNS.

The project ran alongside client work over several weeks rather than as a single sprint.

WordPress to Astro: How We Hit Lighthouse 100 on Our Rebuild - architecture

Architecture Decisions That Made the Difference

Zero JavaScript by Default

The site ships about 2KB of JavaScript in total. Most of that is a small script for the mobile navigation toggle and analytics.

Here's the mobile nav: no framework, no dependencies.

---
// MobileNav.astro
---
<button id="menu-toggle" aria-expanded="false" aria-controls="mobile-menu">
  <span class="sr-only">Toggle menu</span>
  <svg><!-- hamburger icon --></svg>
</button>
<nav id="mobile-menu" hidden>
  <slot />
</nav>

<script>
  const toggle = document.getElementById('menu-toggle');
  const menu = document.getElementById('mobile-menu');
  
  toggle?.addEventListener('click', () => {
    const expanded = toggle.getAttribute('aria-expanded') === 'true';
    toggle.setAttribute('aria-expanded', String(!expanded));
    menu?.toggleAttribute('hidden');
  });
</script>

That <script> tag in an Astro component gets bundled and deduplicated on its own. It's tiny, plain JavaScript, and it works everywhere.

CSS Strategy: Scoped Styles Plus a Minimal Global Layer

We use Astro's built-in scoped CSS for component-level styles. We add one global stylesheet, about 8KB minified, for typography, resets, custom properties, and utility classes. No Tailwind, which is unusual for us.

We like Tailwind for larger apps and client projects. For a site this small, it adds build complexity and file size we don't need. Our hand-written CSS is smaller than Tailwind's output would be, even after purging.

/* Global custom properties */
:root {
  --color-text: #1a1a2e;
  --color-bg: #ffffff;
  --color-accent: #e94560;
  --color-accent-dark: #c81e45;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-heading: 'Cal Sans', var(--font-body);
  --max-width: 72rem;
  --space-unit: 0.25rem;
}

Static Generation With Smart Preloading

Every page is statically generated at build time. Astro's prefetch integration preloads links on hover, which makes navigation feel instant:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://socialanimal.dev',
  integrations: [mdx(), sitemap()],
  prefetch: {
    prefetchAll: false,
    defaultStrategy: 'hover',
  },
  build: {
    inlineStylesheets: 'auto',
  },
});

Performance Optimizations Deep Dive

Getting to Lighthouse 100 isn't only about picking the right framework. Astro gives you a head start, but the last few points take real effort. Here's what that looked like for us.

Image Optimization

Astro's built-in <Image /> component handles responsive images. It converts formats automatically (WebP/AVIF), lazy loads images, and sets the width/height attributes that stop layout shift.

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image 
  src={heroImage} 
  alt="Social Animal development team working on headless architecture"
  widths={[400, 800, 1200]}
  sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
  format="avif"
  fallbackFormat="webp"
  quality={80}
  loading="eager"
/>

For the hero image, we use loading="eager" since it sits above the fold. Everything else gets loading="lazy" by default.

We also checked every image on the site and asked if it needed to be an image at all. Several decorative elements became CSS gradients or SVGs instead. The hero section background is a CSS gradient with a light noise texture from a small inline SVG.

Font Loading Strategy

Fonts often hurt Lighthouse scores. Here's our approach:

  1. Self-host everything. No Google Fonts CDN. We host Inter and Cal Sans from our own domain. This removes a DNS lookup, a TCP connection, and a TLS handshake to a third-party font host.

  2. Subset aggressively. We used glyphhanger to find which characters we actually use, then subset the fonts with pyftsubset. Our Inter Regular WOFF2 shrank from 96KB to 18KB.

  3. Use font-display: swap with a system font fallback chosen to match the real font's shape closely. This keeps layout shift during the swap to a minimum.

  4. Preload the critical font files:

<link rel="preload" href="/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/cal-sans-latin-600.woff2" as="font" type="font/woff2" crossorigin />

Hosting: Vercel

We moved from a paid managed WordPress host to Vercel. Vercel's free Hobby tier covers a static marketing site at our scale.

Vercel deploys from a Git push, serves from its edge network, and manages cache headers on its own. Build times run well under a minute for the whole site. With our old WordPress setup, a single cache purge could take longer than that.

Monthly hosting costs dropped once the static build moved to Vercel's free tier.

Critical CSS Inlining

Astro inlines small stylesheets automatically when build.inlineStylesheets is set to 'auto'. On our pages, every critical style sits inlined in the <head>. There are no render-blocking CSS requests. The browser starts painting right away.

Third-Party Script Discipline

This is where most sites lose their perfect scores. Every third-party script is a possible performance problem, so we kept ours to a minimum.

  • Analytics: We switched from Google Analytics, which ships a much larger script, to a privacy-focused analytics tool with a small script that loads asynchronously.
  • Forms: The contact form uses a plain HTML form with server-side handling through Vercel Functions. No JavaScript form library.
  • No chat widgets, no social embeds, no cookie consent banner (we don't use cookies that require one).

The Lighthouse 100 Scorecard

Here are the scores for socialanimal.dev, measured with Lighthouse in Chrome DevTools using the default mobile simulation:

Metric Score
Performance 100
Accessibility 100
Best Practices 100
SEO 100
First Contentful Paint Under 1s
Largest Contentful Paint Under 1s
Total Blocking Time 0ms
Cumulative Layout Shift 0
Speed Index Under 1s

A Total Blocking Time of 0ms means no JavaScript blocks the main thread on first load.

Before and After: The Numbers

Metric WordPress (Before) Astro (After)
Lighthouse Performance Low-to-mid 50s 100
LCP (mobile) Over 2 seconds Under 1 second
CLS Noticeable 0
TBT Several hundred ms 0ms
Page weight (home) Multiple MB Well under 200KB
HTTP requests Dozens Single digits
JavaScript shipped Hundreds of KB About 2KB
Monthly hosting cost A paid managed WordPress plan Vercel's free tier
Build/deploy time Several minutes Under a minute
Time to first byte Hundreds of ms Tens of ms

The page weight drop surprises people the most: multiple megabytes down to well under 200KB. That's what happens when you stop shipping jQuery, a caching plugin's script loader, an SEO plugin's schema injector, and a stack of plugin stylesheets.

What We Got Wrong Along the Way

It wasn't all smooth. Here's what we'd do differently.

Mistake 1: We Almost Added an Extra CMS Layer

Our first instinct was to put a headless CMS in front of Supabase for editorial ease. We spent time setting up a Sanity schema before asking who would actually use it: developers and an AI content pipeline, both perfectly happy writing straight to Postgres. We dropped Sanity and kept Supabase as the single source of truth. That removed an integration and a subscription we didn't need.

Mistake 2: Font Subsetting Broke Special Characters

Our first font subset was too aggressive. We stripped characters we thought we'd never use, then published a blog post with special punctuation that rendered as boxes. Lesson: test subsets against real content, not a sample alphabet.

Mistake 3: We Forgot About OpenGraph Images

We launched without dynamic OG images. When someone shared a blog post on social media, it showed a generic fallback. We went back and built an OG image pipeline using @astrojs/og, which uses Satori under the hood. That should have been part of the plan from the start.

Mistake 4: The 301 Redirect Map Had Gaps

Despite mapping old URLs with Screaming Frog, we missed a handful of image URLs that other sites were hotlinking to. We caught these in our server logs about a week after launch and added the missing redirects. Check your server logs after a migration. Google Search Console won't catch everything.

Lessons for Your Own Migration

If you're thinking about a move from WordPress to a static-first framework, here's what we'd tell you.

  1. Audit before you migrate. Retire content that isn't performing. A migration is a good chance to prune.

  2. Match the tool to the job. Astro suited us because we're mostly content. If you need heavy interactivity, Next.js or a similar framework is the better call.

  3. Don't copy your old architecture. We didn't try to rebuild the WordPress setup inside Astro. We rethought each piece from scratch. Do you actually need a form plugin? No, a <form> element with a serverless function works fine.

  4. Measure before, during, and after. We run a Lighthouse CI job in GitHub Actions on every pull request. If a PR drops any score below 95, the check fails.

  5. Budget for the last stretch. Getting from Lighthouse 85 to 95 is fairly quick. Getting from 95 to 100 takes font subsetting, critical CSS work, image format tuning, and a third-party script audit. Plan time for it.

  6. Your hosting bill should embarrass your old one. If you're serving static files and still paying big hosting fees, something's off. Static hosting is a commodity now.

We've used the same approach for clients, including bdManagedIT, whose WordPress-to-Astro rebuild now scores 95+ on PageSpeed. If a migration like this looks useful for your project, check our pricing page or get in touch.

FAQ

How long does it take to migrate a WordPress site to Astro?

Timeline depends mostly on content volume and site complexity. A small marketing site with a modest blog usually takes several weeks of part-time work. A larger site with hundreds of posts and custom post types takes longer. Content auditing and redirect mapping usually take more time than the Astro development itself.

Can you get Lighthouse 100 with Next.js instead of Astro?

It's possible but harder to reach than with Astro. Next.js ships a JavaScript runtime to the browser even on static pages, because of React hydration. You can get close with careful work, but perfect scores in every category are much easier to reach with Astro's zero-JS-by-default approach for content sites.

What about WordPress features like contact forms and search?

Contact forms work fine as plain HTML forms with a serverless function backend, such as Vercel Functions or Netlify Functions. For search, a client-side tool like Pagefind builds a search index at build time. It ships only a few kilobytes of JavaScript, so it stays fast and works offline.

Does migrating from WordPress to Astro hurt SEO?

Not if you handle it properly. Set up 301 redirects for every URL, keep the URL structure where you can, submit a new sitemap, and keep structured data. Organic traffic often improves after this kind of migration, which fits Google's guidance that Core Web Vitals affect ranking.

How do you handle dynamic content like comments on an Astro site?

We don't run comments on our own blog, since they mostly attracted spam on WordPress. For sites that need comments, embeddable services like Giscus (built on GitHub Discussions) or Hyvor Talk work well. They load as isolated Astro islands, so they don't slow down the rest of the page.

Is Astro production-ready for large sites?

Yes. Astro 5 is stable in production and used by large organizations building content-heavy sites. See Astro's showcase for examples. Build performance scales well too, and sites with thousands of pages build fast with the right setup.

What's the ongoing maintenance like compared to WordPress?

It's much lighter. There are no plugin updates, no database maintenance, and no PHP security patches to track. Dependency updates arrive through Dependabot pull requests about once a month, and each one takes a few minutes to review and merge, unlike the WordPress update treadmill.

Can non-technical team members still edit content on an Astro site?

Our own setup stores content in Supabase with an AI pipeline that helps draft posts, so daily editing doesn't need Git. For teams that want a visual interface, pairing Astro with a headless CMS like Sanity, Contentful, or Storyblok gives non-technical editors an easy UI while keeping the speed of static generation.

Key takeaway:

Astro ships zero JS by default. Use islands only where you need them.