The framework that decided HTML was good enough — and then made it a superpower. Here's what Astro is, why it exists, and why the .astro file format is unlike anything else in the web ecosystem.
Every JavaScript framework of the last decade shared a quiet assumption: the browser needs to run code to build your UI. React, Vue, Angular — they all ship a runtime to the browser, execute it on load, and construct the page in JavaScript. For interactive apps, this makes sense. For a website that's mostly text, images, and links? It's like renting a bulldozer to plant a flower.
Astro was built on a different premise. Your HTML already is the UI. Generate it once at build time — or on the server when needed — and send it over the wire. No framework runtime. No hydration step. Just fast, static HTML that loads instantly.
Think of traditional JS frameworks as a restaurant that sends you raw ingredients and a chef's instructions — you cook at your table. Astro is the restaurant that sends you the finished meal. Less work for the browser, faster on your plate.
Astro is a web framework for content-driven websites — marketing sites, blogs, documentation, portfolios. It:
At build time, Astro renders every page to plain HTML. No client-side JavaScript required to display the page.
Any component that needs interactivity becomes an "island" — only that part hydrates. The rest stays static HTML.
Use React, Vue, Svelte, Solid, Preact — or none. Mix and match in the same project.
Type-safe content from Markdown files. Astro validates your frontmatter schema with Zod at build time.
A fresh Astro page ships 0 bytes of JavaScript to the browser. Every KB must be explicitly opted into.
Switch any page — or the whole site — to server-side rendering when you need dynamic content or auth.
Google's Core Web Vitals directly affect search ranking. Shipping less JavaScript means faster LCP (Largest Contentful Paint) and better FID (First Input Delay). A default Astro page routinely scores 100/100 on Lighthouse. That's not luck — it's the architecture.
The web framework space is crowded. Before you can appreciate what makes Astro different, you need to know who else is in the room.
| Framework | Base Language | Zero JS? | Best For | Trade-off |
|---|---|---|---|---|
| Astro | Any (agnostic) | Yes | Content sites, marketing, docs, blogs | Less suited for heavy full-stack apps |
| Next.js | React | No | Full-stack apps, e-commerce, dashboards | React bundle always ships; opinionated toward Vercel |
| Nuxt | Vue | No | Full-stack Vue apps, hybrid rendering | Tied to Vue; complex config |
| SvelteKit | Svelte | No | Fast, elegant full-stack apps | Tied to Svelte; smaller ecosystem |
| Gatsby | React + GraphQL | No | Static sites with rich data sources | Slow builds; full React + GraphQL always ships |
| Eleventy (11ty) | Templates (Nunjucks, Liquid...) | Yes | Pure static HTML, minimal config | No component model; templating only |
| Hugo | Go templates | Yes | Massive content sites, fastest build times | Go template syntax is its own world; no JS integration |
Astro and Next.js aren't really competitors — they optimize for different problems. Astro is a publishing tool. Next.js is an application tool. If your site has more pages than user states, Astro. If it has more user states than pages, Next.js.
This is Astro's signature idea. In a traditional React app, the entire page is a React tree — everything hydrates, everything re-renders. In Astro, the default is static HTML. You opt specific components into interactivity using client directives.
--- import Header from './Header.astro'; // static — zero JS import Counter from './Counter.jsx'; // a React component import Map from './Map.svelte'; // a Svelte component --- <Header /> <!-- static HTML, 0 bytes JS --> <Counter client:load /> <!-- hydrates immediately --> <Map client:visible /> <!-- hydrates when scrolled into view --> <Chart client:idle /> <!-- hydrates when browser is idle -->
| Directive | When It Hydrates | Best For |
|---|---|---|
client:load | Immediately on page load | Critical interactive UI (nav, forms) |
client:idle | When browser is idle (requestIdleCallback) | Lower-priority widgets |
client:visible | When component enters the viewport | Below-the-fold interactive elements |
client:media | When a CSS media query matches | Mobile-only or desktop-only components |
client:only | Client-side only — never server-rendered | Components that break on the server |
If you're building a blog or docs site, content collections are a game-changer. You define a schema for your Markdown files, and Astro validates every file against it at build time. Missing a required field? Build error. Wrong type? Build error.
import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), publishDate: z.date(), author: z.string().default('Anonymous'), tags: z.array(z.string()).optional(), draft: z.boolean().default(false), }), }); export const collections = { blog };
Add export const prerender = false to any page to opt it into SSR while keeping everything else static. This "hybrid" mode is Astro's secret weapon — 95% of your site ships as static HTML, while your /dashboard renders on demand.
Astro 3+ ships built-in View Transitions — smooth, SPA-like page animations with a single import. Add <ViewTransitions /> to your layout and Astro uses the browser's native View Transitions API, with a polyfill fallback. You get the feel of a single-page app without the architecture of one.
Once you've built your Astro site, you need somewhere to deploy it. The two most popular choices are Vercel and Cloudflare Pages — and the decision matters more than you'd think, especially if you're using SSR.
Both are CDN-first deployment platforms. The key difference is in their serverless runtime: Vercel runs Node.js in AWS Lambda functions at the edge. Cloudflare runs V8 isolates (Workers) across 300+ locations. Same concept — very different environments.
If your Astro site is fully static, the choice is nearly academic. Both give you:
When you add server-side rendering, you need an adapter. Astro ships official adapters for both:
// For Cloudflare Pages import cloudflare from '@astrojs/cloudflare'; export default { adapter: cloudflare() }; // For Vercel import vercel from '@astrojs/vercel/serverless'; export default { adapter: vercel() };
Cloudflare Workers do not run Node.js. They run the V8 JavaScript engine directly, without Node's standard library. Packages using fs, path, or Node's crypto will break on Cloudflare. This is the #1 gotcha for developers moving from Vercel to Cloudflare.
| Feature | Vercel | Cloudflare Pages |
|---|---|---|
| Runtime | Node.js (AWS Lambda) | V8 isolates (Workers) |
| Node.js compat | Full | Partial (node: prefix required) |
| Cold start | ~200–500ms | <5ms (no cold starts) |
| Edge locations | ~50 | 300+ |
| Free tier bandwidth | 100 GB/month | Unlimited |
| Preview deploys | Excellent | Good |
| Built-in database | None (use external) | D1 (SQLite at edge) |
| Object storage | None (use external) | R2 (S3-compatible, free egress) |
| KV store | None (use external) | Workers KV (globally replicated) |
Every component and page in an Astro project lives in a .astro file. It looks like HTML. It mostly is HTML. But it has three superpowers that plain HTML never had: a build-time script, a component model, and automatic CSS scoping.
An .astro file has up to four distinct zones, each with a different purpose and a different runtime:
Wrapped in --- fences. Full TypeScript/JavaScript that runs at build time or on the server — never in the browser. This is where you import components, fetch data, define variables, and validate props.
Standard HTML with JSX-style {expressions} for dynamic values. Supports conditional rendering and .map() for lists. The output is always plain HTML — no virtual DOM, no runtime.
CSS that Astro automatically scopes to this component by injecting a unique attribute. Styles don't bleed into children or siblings. Use :global() to intentionally break out of scoping.
JavaScript that runs in the browser. Astro bundles and deduplicates these automatically. Add is:inline to skip bundling. This is entirely separate from Zone 1 — it ships to the client, Zone 1 does not.
--- // Zone 1: runs at build time — never shipped to the browser interface Props { title: string; date: Date; href: string; } const { title, date, href } = Astro.props; const formatted = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); --- <!-- Zone 2: HTML template with expressions --> <article class="card"> <time>{formatted}</time> <h2><a href={href}>{title}</a></h2> <slot /> <!-- renders children passed from the parent --> </article> <!-- Zone 3: scoped CSS — .card only applies to this component --> <style> .card { background: #1a1a2e; border-radius: 8px; padding: 1.5rem; } </style> <!-- Zone 4: client script — this DOES ship to the browser --> <script> document.querySelector('.card') ?.addEventListener('click', () => console.log('clicked')); </script>
{/* Expressions — any JS expression in curly braces */} <p>{2 + 2}</p> <!-- renders: 4 --> <p>{name.toUpperCase()}</p> <!-- renders: NAME --> {/* Conditional rendering */} {isLoggedIn && <Dashboard />} <!-- short-circuit --> {isLoggedIn ? <Dashboard /> : <Login />} <!-- ternary --> {/* Rendering lists */} {posts.map(post => ( <PostCard title={post.title} href={post.slug} /> ))} {/* Spread props */} <Button {...props} /> {/* Raw HTML — use carefully, XSS risk */} <div set:html={markdownContent} />
Astro templates use class= (like HTML), not className= (like React/JSX). This trips up React developers constantly. The rule: Astro templates are closer to HTML than to JSX. When in doubt, write it like HTML.
Understanding what makes .astro unique requires knowing what problem each format was designed to solve — and where their boundaries lie.
The output of an .astro file is plain HTML. But the authoring experience is radically better — you get components, logic, and type safety without shipping a runtime to your users.
JSX runs in the browser. .astro runs at build time. React ships a runtime to construct your UI in the browser; Astro constructs the UI during the build and ships the finished HTML. This is why an Astro page has zero framework dependency at runtime — the work was already done before the user arrived.
| Feature | .astro | .jsx / .tsx (React) |
|---|---|---|
| Renders where | Build time / server | Client (browser) + optionally server |
| Ships a runtime | No | Yes — React (~45 KB gzipped) |
| State management | No (build-time only) | Yes — useState, useReducer... |
| CSS scoping | Automatic | Manual (CSS Modules, styled-components...) |
| Attribute syntax | class= (HTML-style) | className= (JSX-style) |
| Lifecycle hooks | None | useEffect, useMemo, etc. |
| Can import .astro | Yes | No |
The rule of thumb: use .astro for structure, layout, and content. Use .jsx wrapped in a client:* directive when you need interactivity or state.
MDX is Markdown with JSX embedded — designed for prose that occasionally needs interactive components. .astro is for the structure that wraps that prose.
In an Astro project, both formats work together. Your blog layout is an .astro file. Your blog posts are .mdx files. The layout wraps the post. Structure and content stay cleanly separated — each format doing what it's best at.
Every format before .astro forced a tradeoff:
.astro is the first format that gives you a full component model, TypeScript, logic, scoped CSS, and zero runtime cost — because all that work happens before the HTML ever reaches a browser.
The web has drifted toward shipping megabytes of JavaScript to render content that could have been plain HTML. The .astro format is a direct response: a component format that takes complexity seriously at authoring time, so your users don't pay for it at load time.
.pathname, .searchParams, etc. Natural T3 continuations of this topic — not live yet, but they're coming.
Typed front matter, Zod schemas, getCollection() — how to build a fully validated content pipeline with Astro's native system.
Partial hydration explained: what client:load, client:visible, and client:idle actually do and when to reach for each one.
How SSG, SSR, and ISR differ, how bundlers fit in, and what Astro's build output actually looks like under the hood.