Why Astro Is My First Choice for Content Websites
After 30+ client content sites, here's why Astro ships less JS, scores higher on Lighthouse, and beats heavier frameworks for blogs and marketing pages.
Three years into freelancing on Upwork, I’ve shipped websites for law firms in London, SaaS startups in Austin, and creative agencies in Berlin. The brief is almost always the same: fast load times, strong SEO, easy content updates, and a design that doesn’t feel like a template. Astro has become my default answer for that brief — not because it’s trendy, but because it solves the actual problem content websites have: most pages don’t need a JavaScript runtime to display text and images.
When a UK client asked me to rebuild their 120-page resource library that was running on WordPress with a bloated page builder, the goal wasn’t flashy interactions. They needed crawlable HTML, sub-second LCP, and a CMS their marketing team could use without calling me every week. We moved to Astro with a headless CMS. First Contentful Paint dropped from 3.2 seconds to 0.8 seconds. Organic traffic climbed 34% over six months — not because we hacked SEO, but because Google could finally crawl clean markup without wading through hydration overhead.
What Makes a Content Website Different
Content websites — blogs, portfolios, documentation, landing pages, case study archives — share a structural pattern. The HTML is mostly static. The data changes on a schedule measured in hours or days, not milliseconds. Interactivity is localized: a mobile menu, a contact form, maybe a pricing toggle. The rest is prose, images, and internal links.
Traditional React SPAs treat every page like an application. The browser downloads a framework, hydrates the entire component tree, and only then renders content the server could have sent as plain HTML. For a dashboard with real-time data, that trade-off makes sense. For a blog post about tax compliance, it doesn’t.
Astro inverts the assumption. By default, every .astro component compiles to static HTML with zero client-side JavaScript. You opt into interactivity only where you need it. That architectural choice isn’t a minor optimization — it’s the difference between shipping 15 KB and shipping 250 KB on a page that displays text.
The Islands Architecture in Practice
Astro’s islands model treats interactive components as isolated units that hydrate independently. The surrounding page stays static.
---
import MobileNav from "../components/MobileNav.tsx";
import NewsletterForm from "../components/NewsletterForm.tsx";
---
<header>
<nav class="desktop-nav">
<!-- Static links — no JS -->
<a href="/services">Services</a>
<a href="/about">About</a>
</nav>
<MobileNav client:media="(max-width: 768px)" />
</header>
<main>
<slot />
</main>
<footer>
<NewsletterForm client:visible />
</footer>
The client: directives are load-order controls, not afterthoughts:
client:load— hydrate immediately (use sparingly, above-the-fold only)client:idle— hydrate after the main thread is freeclient:visible— hydrate when scrolled into viewclient:media— hydrate based on a media query
On a recent portfolio for a photographer, I used client:visible for a lightbox gallery and client:media for the mobile hamburger menu. Total JavaScript on the homepage: 28 KB gzipped. The previous Next.js version shipped 187 KB before any images loaded.
Content Collections Give You Type Safety at Build Time
One reason I trust Astro for client projects is Content Collections. Markdown and MDX files get validated against a Zod schema at build time, which catches broken frontmatter before it hits production.
// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
category: z.string(),
tags: z.array(z.string()).default([]),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
When a client’s content writer forgets a publishDate or misspells a required field, the build fails locally with a clear error. Compare that to a runtime CMS query that silently returns undefined and renders a blank page in production. For agencies handing sites off to non-technical teams, build-time validation is insurance.
Framework Flexibility Without Lock-In
Astro doesn’t force you into a single UI library. I’ve used React islands for complex forms, Svelte for animated counters, and vanilla .astro components for everything else — all in the same project.
---
import PricingToggle from "../components/PricingToggle.svelte";
import ContactForm from "../components/ContactForm.tsx";
---
<section class="pricing">
<PricingToggle client:visible />
</section>
<section class="contact">
<ContactForm client:idle />
</section>
This matters when you’re inheriting a client’s component library or when your team has mixed expertise. You’re not rewriting everything in a new paradigm. You’re composing what you already know into a faster delivery model.
Performance Numbers From Real Projects
I track Core Web Vitals on every client launch. Here’s what I consistently see when migrating content sites to Astro:
| Metric | Typical WordPress/Next.js | Astro rebuild |
|---|---|---|
| LCP | 2.4–4.1s | 0.7–1.4s |
| INP | 180–350ms | 40–90ms |
| CLS | 0.08–0.25 | 0–0.02 |
| JS transferred | 150–400 KB | 5–40 KB |
A SaaS marketing site I rebuilt last quarter went from a Lighthouse performance score of 54 to 98 on mobile. The content didn’t change. The architecture did. Google’s crawler and real users on 4G connections both noticed.
SEO Advantages That Come for Free
Search engines reward fast, crawlable pages with clear structure. Astro generates static HTML by default, which means:
- No hydration delay before content is visible
- No client-side routing that confuses crawlers
- Sitemap and RSS generation via
@astrojs/sitemap - Full control over
<head>metadata per page
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<html lang="en">
<head>
<title>{post.data.title} | Omprakash Tanwar</title>
<meta name="description" content={post.data.description} />
<link rel="canonical" href={`https://example.com/blog/${post.id}`} />
<meta property="og:title" content={post.data.title} />
<meta property="og:description" content={post.data.description} />
</head>
<body>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
</body>
</html>
Structured data, canonical URLs, and Open Graph tags are first-class concerns you implement once and reuse across every page template.
Deployment and Hosting Simplicity
Astro outputs static files by default. That means Netlify, Vercel, Cloudflare Pages, AWS S3 + CloudFront — any static host works. No server runtime required unless you opt into SSR via an adapter.
For a EU-based nonprofit client, we deployed to Cloudflare Pages with their global CDN. Build time: 45 seconds. Hosting cost: free tier. Uptime: effectively 100%. When your content changes weekly, not every second, static generation with optional on-demand revalidation (via adapters) covers the freshness requirement without running Node.js 24/7.
When Astro Is Not the Right Choice
I’m honest with clients when Astro isn’t the fit. Skip it when:
- You need authenticated, per-user dashboards with heavy client state
- Real-time collaboration is core (think Figma or Google Docs)
- Your team is entirely invested in Next.js and won’t maintain Astro patterns
- You require edge middleware for complex A/B testing on every request
For those cases, I reach for Next.js or a dedicated SPA. But that’s maybe 20% of the freelance briefs I see. The other 80% are content sites that got over-engineered because React was the default hammer.
Common Mistakes I See With Astro Projects
Even with Astro’s performance defaults, teams still shoot themselves in the foot:
Hydrating everything. Slapping client:load on every component defeats the purpose. Audit your client: directives like you’d audit bundle size in a React app.
Importing heavy libraries in shared layouts. If your base layout imports a charting library, it ships on every page. Keep heavy deps inside island components with appropriate load directives.
Skipping image optimization. Astro doesn’t magically compress your 4 MB hero PNG. Use <Image> from astro:assets or an image service.
Ignoring view transitions. Astro 4+ supports View Transitions for SPA-like navigation without SPA-like JS payloads. Use them for polish, not as a reason to hydrate the entire layout.
My Workflow for Client Astro Projects
Here’s the stack I reach for repeatedly:
- Astro 5 with Content Collections for blog/docs
- Tailwind CSS for rapid, consistent styling
- React islands only for forms and interactive widgets
- Decap CMS or Sanity for client-editable content
- Cloudflare Pages or Netlify for deployment
- Lighthouse CI in GitHub Actions to catch regressions
This combination gets a content site from brief to production in two to three weeks, with performance scores that sell the next referral.
MDX and Rich Content Without the Runtime Tax
Clients often want embeds, callout boxes, and interactive code snippets in their blog posts. MDX in Astro gives you that flexibility without turning every article into a client-side application.
---
title: "Understanding GDPR for UK Businesses"
---
import Callout from '../components/Callout.astro';
<Callout type="warning">
This article is informational, not legal advice.
</Callout>
## What Changed in 2024
Standard markdown content renders as static HTML. The Callout component compiles at build time — no runtime MDX parser in the browser.
I use MDX sparingly. Most blog content stays plain markdown because it’s simpler for non-technical writers and compiles faster. MDX is reserved for components that genuinely improve comprehension — comparison tables, interactive diagrams, embedded calculators. The discipline of asking “does this need to be a component?” keeps article pages lean.
Handing Sites Off to Non-Technical Teams
A framework choice isn’t just a developer preference — it affects who can maintain the site after launch. Astro pairs well with visual CMS tools because the frontend and content layer are cleanly separated.
For a Berlin agency client, we connected Sanity Studio to Astro via a build webhook. Their content team publishes in Sanity. The webhook triggers a Netlify build. Updated content is live in 90 seconds. Nobody touches Git. Nobody breaks the layout. The marketing director edits hero copy without filing a Jira ticket.
That workflow is harder to replicate when your content lives inside React component props or requires a developer to update a JSON file. Astro’s content layer — whether Collections, a headless CMS, or markdown in a repo — maps to how marketing teams actually work.
Conclusion
Astro isn’t winning because it has the most npm downloads. It’s winning because it respects what content websites actually are: documents with occasional interactivity, not applications pretending to be documents. After building dozens of these sites for clients who measure success in search rankings and conversion rates, Astro is the first framework I recommend when the brief says “fast, SEO-friendly, and easy to maintain.”
The web doesn’t need more JavaScript on pages that display articles. It needs less — delivered smarter. That’s what Astro does, and that’s why it’s my first choice.
Key Takeaways
- Content websites are mostly static HTML — shipping a full React runtime for them is architectural overkill
- Astro’s islands architecture lets you hydrate only the components that need interactivity
- Content Collections with Zod schemas catch content errors at build time, not in production
- Real client migrations consistently show LCP under 1.5s and JavaScript payloads under 40 KB
- Use
client:visibleandclient:idleaggressively; reserveclient:loadfor truly critical UI - Astro works with React, Svelte, Vue, and more — you’re not locked into one component library
- Static deployment to any CDN keeps hosting simple and costs low for content-heavy sites
- Reach for Next.js or a SPA when you need authenticated dashboards or real-time collaboration, not for blogs and marketing pages