Astro: Building Fast Content-Focused Sites
Astro has quickly emerged as one of the most exciting frameworks in the JavaScript ecosystem. Unlike traditional meta-frameworks that ship large JavaScript bundles to the browser by default, Astro takes a fundamentally different approach — it renders pages to HTML at build time and ships zero JavaScript by default. The result is websites that load nearly instantly, consistently achieving Lighthouse scores above 95 out of the box. Whether you're building a blog, documentation site, marketing page, or e-commerce storefront, Astro gives you the tools to ship blazing-fast, SEO-friendly sites while still using your favourite UI components from React, Vue, Svelte, or any other framework you love.
Astro has surpassed 45,000 GitHub stars and powers sites at companies including Microsoft, Google, and The Guardian. Its Islands Architecture was named one of the most innovative web patterns in 2023, and Astro sites consistently dominate benchmarks for Time to First Byte (TTFB) and Largest Contentful Paint (LCP).
What Makes Astro Different?
Most JavaScript frameworks assume you need interactivity everywhere. Next.js, Remix, and SvelteKit all send a full JavaScript runtime to the browser that hydrates your entire page. For content-heavy sites — blogs, documentation, marketing pages — most of that JavaScript is unnecessary overhead. Astro was built from the ground up around a different assumption: content sites primarily need HTML and CSS, not JavaScript.
Zero JavaScript by Default
When you build an Astro page, it generates pure HTML with no runtime JavaScript. Your Astro components execute during the build step (or on the server for SSR mode), produce HTML, and that HTML is what gets sent to the browser. A typical Astro blog page loads in under 500ms even on slow 3G because the browser receives a pre-rendered HTML document rather than a JavaScript shell that bootstraps the application in the browser.
Islands Architecture
The concept of Islands Architecture describes a page as an "ocean of static HTML" with interactive "islands" of JavaScript. In Astro, you can embed a React counter, Vue accordion, or Svelte chart as an island, and Astro will only hydrate that specific component — leaving the rest of the page as plain, static HTML. This gives you the best of both worlds: static-site performance with component-level interactivity exactly where it's needed.
Multi-Framework Component Support
Astro is framework-agnostic at the component level. Within the same project you can use React, Vue, Svelte, Solid, Preact, and vanilla HTML components side by side. This makes Astro ideal for teams migrating between frameworks, or for projects that need specialised components from different ecosystems without being locked into a single framework.
Astro vs Traditional Frameworks
| Feature | Astro | Next.js | Gatsby | SvelteKit |
|---|---|---|---|---|
| Default JS Shipped | 0 KB | ~70–200 KB | ~70–150 KB | ~20–60 KB |
| Rendering Modes | SSG, SSR, Hybrid | SSG, SSR, ISR | SSG, DSG | SSG, SSR |
| UI Framework | Any (React, Vue, Svelte…) | React only | React only | Svelte only |
| Islands / Partial Hydration | Native built-in | Not built-in | Not built-in | Not built-in |
| Content Collections | Built-in + type-safe | Manual setup | GraphQL layer | Manual setup |
| Best For | Content sites, blogs, docs | React apps, SaaS | Content / e-commerce | Svelte full-stack apps |
Installation and Setup
Getting started with Astro takes less than two minutes. The official CLI walks you through project creation with a suite of starter templates for blogs, portfolios, and minimal setups.
Create a New Astro Project
Run the official Astro CLI. The interactive wizard lets you pick a template, TypeScript settings, and whether to install dependencies automatically.
# Create a new Astro project (interactive wizard)
npm create astro@latest my-astro-site
# Or scaffold directly with the blog template
npm create astro@latest my-blog -- --template blog
# Navigate into the project
cd my-astro-site
# Start the development server (http://localhost:4321)
npm run dev
Add Integrations
Astro's integration system extends the framework with React, Tailwind, MDX, sitemaps, and more — all with a single command that handles config automatically.
# Add React support (auto-configures astro.config.mjs)
npx astro add react
# Add Tailwind CSS
npx astro add tailwind
# Add MDX for interactive Markdown
npx astro add mdx
# Add automatic sitemap generation
npx astro add sitemap
Understand the Project Structure
Astro uses a clear, convention-based directory layout that keeps routing, components, content, and static assets cleanly separated.
my-astro-site/
├── public/ # Static assets served as-is (images, fonts, favicon)
│ └── favicon.svg
├── src/
│ ├── components/ # Reusable .astro and framework components
│ │ └── Card.astro
│ ├── content/ # Content Collections (Markdown, MDX, JSON, YAML)
│ │ ├── config.ts # Collection schemas defined here
│ │ └── blog/
│ │ └── first-post.md
│ ├── layouts/ # Page layout wrappers
│ │ └── BaseLayout.astro
│ └── pages/ # File-based routing — every file = a route
│ ├── index.astro # → /
│ ├── about.astro # → /about
│ └── blog/
│ └── [slug].astro # → /blog/:slug (dynamic route)
├── astro.config.mjs # Framework configuration
├── package.json
└── tsconfig.json
Astro has first-class TypeScript support out of the box. Your component frontmatter, content collection schemas, and config files all benefit from full type inference. The astro check command runs a type-check across your entire project without emitting files.
Astro Component Syntax
Astro components use a .astro file format with a unique two-section structure: a JavaScript/TypeScript frontmatter block (between triple dashes) and an HTML-like template section. This design intentionally separates server-side logic from the presentation layer, making components easy to reason about.
Anatomy of an Astro Component
---
// Frontmatter: runs on the server at build time (or per-request in SSR)
// Imports, TypeScript interfaces, and data-fetching logic live here
interface Props {
title: string;
description: string;
pubDate: Date;
slug: string;
tags?: string[];
}
const { title, description, pubDate, slug, tags = [] } = Astro.props;
const formattedDate = pubDate.toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
});
---
<!-- Template: HTML with Astro expressions in curly braces -->
<article class="blog-card">
<a href={`/blog/${slug}`}>
<h2>{title}</h2>
</a>
<time datetime={pubDate.toISOString()}>{formattedDate}</time>
<p>{description}</p>
{tags.length > 0 && (
<ul class="tags">
{tags.map(tag => <li>{tag}</li>)}
</ul>
)}
</article>
<!-- Scoped styles: only apply to THIS component's output -->
<style>
.blog-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 24px;
transition: box-shadow 0.2s;
}
.blog-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
h2 { color: #7c3aed; margin-bottom: 8px; }
.tags { display: flex; gap: 8px; list-style: none; padding: 0; }
.tags li { background: #f3f0ff; color: #7c3aed; padding: 2px 10px; border-radius: 12px; font-size: 0.8rem; }
</style>
Using Framework Components as Islands
After adding a framework integration, you can import React, Vue, or Svelte components directly into .astro files. By default they render to static HTML — you explicitly opt into client-side hydration with a client: directive.
---
import BaseLayout from '../layouts/BaseLayout.astro';
import BlogCard from '../components/BlogCard.astro';
import Counter from '../components/Counter.jsx'; // React component
import Newsletter from '../components/Newsletter.vue'; // Vue component
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<BaseLayout title="My Astro Blog">
<h1>Welcome to My Blog</h1>
<!-- Static Astro components — pure HTML output, zero JS shipped -->
<section class="posts-grid">
{posts.map(post => (
<BlogCard
title={post.data.title}
description={post.data.description}
pubDate={post.data.pubDate}
slug={post.slug}
tags={post.data.tags}
/>
))}
</section>
<!-- React island: hydrates immediately on page load -->
<Counter client:load />
<!-- Vue island: only hydrates when scrolled into the viewport -->
<Newsletter client:visible />
</BaseLayout>
Content Collections
Content Collections are Astro's built-in, type-safe content management system. Instead of writing custom glob logic or querying an external CMS, you define your content schema using Zod in code, and Astro generates TypeScript types automatically. Your IDE will autocomplete frontmatter fields and the build will fail with a clear error if required properties are missing — catching content bugs before they reach production.
Defining a Collection Schema
import { z, defineCollection } from 'astro:content';
const blogCollection = defineCollection({
type: 'content', // 'content' for Markdown/MDX, 'data' for JSON/YAML
schema: z.object({
title: z.string().max(100),
description: z.string().max(200),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
author: z.string().default('Mayur Dabhi'),
}),
});
const authorsCollection = defineCollection({
type: 'data', // JSON or YAML files
schema: z.object({
name: z.string(),
bio: z.string(),
avatar: z.string().url(),
twitter: z.string().optional(),
github: z.string().optional(),
}),
});
// Export named collections — Astro generates types from this
export const collections = {
blog: blogCollection,
authors: authorsCollection,
};
Querying and Rendering Collections
---
import { getCollection, type CollectionEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
// getStaticPaths tells Astro which slugs to pre-render at build time
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
type Props = { post: CollectionEntry<'blog'> };
const { post } = Astro.props;
// render() converts Markdown/MDX to an HTML component
const { Content, headings, remarkPluginFrontmatter } = await post.render();
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
{post.data.heroImage && <img src={post.data.heroImage} alt="" />}
<Content /> {/* Renders the Markdown body as HTML */}
</article>
</BaseLayout>
If any Markdown file's frontmatter doesn't match your Zod schema — wrong type, missing required field, value out of range — Astro throws a descriptive build error identifying the exact file and field. This means content bugs are caught before they can ever reach production.
Islands Architecture in Depth
The islands pattern is Astro's most powerful concept for balancing performance with interactivity. A "static island" is a server-rendered component with no browser JavaScript. An "interactive island" hydrates client-side, but only that specific component — the rest of the page remains untouched static HTML. Astro controls hydration with client: directives attached to framework components.
Astro Islands Architecture — only the interactive React island ships JavaScript; everything else is pure HTML
Client Directives Reference
| Directive | When It Hydrates | Best Use Case |
|---|---|---|
client:load |
Immediately on page load | Critical UI: nav dropdowns, search, auth forms |
client:idle |
When browser is idle (requestIdleCallback) | Non-critical widgets: chat bubbles, analytics banners |
client:visible |
When element enters the viewport | Below-fold content: comment sections, demos, carousels |
client:media="(max-width: 768px)" |
When the CSS media query matches | Mobile-only interactive menus |
client:only="react" |
Client-side only (skips SSR entirely) | Components using browser-only APIs (localStorage, canvas) |
---
import SearchBar from '../components/SearchBar.jsx'; // React
import LiveChat from '../components/LiveChat.jsx'; // React
import VideoPlayer from '../components/VideoPlayer.svelte';
import MobileMenu from '../components/MobileMenu.vue';
---
<!-- Hydrates immediately — search must work on first interaction -->
<SearchBar client:load />
<!-- Hydrates when the browser is idle after initial load -->
<LiveChat client:idle />
<!-- Hydrates only when the user scrolls to the video section -->
<VideoPlayer client:visible />
<!-- Hydrates only on mobile screens — saves JS on desktop -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- No directive = renders to HTML only, zero JavaScript shipped -->
<footer><p>© 2026 My Astro Site</p></footer>
Layouts, Routing, and API Endpoints
Astro's file-based router maps every .astro file inside src/pages/ to a URL automatically. Nested directories create nested routes, and square-bracket filenames create dynamic segments. You can also create JSON API endpoints alongside pages.
Layouts are Astro components that wrap pages and provide shared structure. Use the <slot /> element to render child content.
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = 'My Astro Site' } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content={description}>
<title>{title}</title>
<link rel="sitemap" href="/sitemap-index.xml">
</head>
<body>
<header>
<nav><a href="/">Home</a> <a href="/blog">Blog</a></nav>
</header>
<main>
<slot /> {/* Page content renders here */}
</main>
<footer><p>Built with Astro</p></footer>
</body>
</html>
getStaticPaths() tells Astro which URL segments to generate at build time for dynamic routes.
---
// src/pages/tags/[tag].astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
// Collect all unique tags across all posts
const tags = [...new Set(posts.flatMap(p => p.data.tags))];
return tags.map(tag => ({
params: { tag },
props: {
posts: posts.filter(p => p.data.tags.includes(tag)),
},
}));
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<BaseLayout title={`Posts tagged: ${tag}`}>
<h1>Posts tagged: {tag}</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
))}
</ul>
</BaseLayout>
Create .ts files in src/pages/api/ to expose JSON endpoints alongside your HTML pages.
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async () => {
const posts = await getCollection('blog', ({ data }) => !data.draft);
const payload = posts.map(post => ({
slug: post.slug,
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDate.toISOString(),
tags: post.data.tags,
}));
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
// POST endpoint for form submissions (requires SSR or hybrid mode)
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
// Handle the submission...
return new Response(JSON.stringify({ ok: true }), { status: 200 });
};
Building and Deploying
Astro supports three output modes. Choosing the right one depends on how dynamic your content needs to be at request time.
- static (default): Generates plain HTML/CSS/JS at build time. Deploy to any CDN or static host — Netlify, Vercel, Cloudflare Pages, GitHub Pages. Best for blogs, docs, and marketing sites where content changes infrequently.
- server: Renders every page on-demand per request. Enables user authentication, real-time data, and personalised content. Requires a Node.js server or serverless adapter.
- hybrid: Mixes static pre-rendering with on-demand SSR. Individual pages opt into SSR by exporting
export const prerender = false. Great for mostly-static sites with a few dynamic routes.
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import vercel from '@astrojs/vercel/serverless'; // SSR on Vercel
export default defineConfig({
site: 'https://mysite.com', // Required for sitemap and canonical URLs
output: 'hybrid', // 'static' | 'server' | 'hybrid'
adapter: vercel(), // Required for 'server' or 'hybrid' modes
integrations: [
react(),
tailwind(),
sitemap(), // Auto-generates /sitemap-index.xml
],
image: {
// Optimise images with Sharp (auto-converts to WebP/AVIF)
service: { entrypoint: 'astro/assets/services/sharp' },
},
});
# Build for production (outputs to ./dist/)
npm run build
# Preview the production build locally before deploying
npm run preview
# Deploy static output to Netlify
npx netlify deploy --prod --dir=dist
# Deploy to Vercel (auto-detects Astro, applies correct settings)
npx vercel deploy
# Deploy to Cloudflare Pages
npx wrangler pages deploy dist
Built-in Image Optimisation
Astro 3.0+ ships a built-in <Image> component that automatically resizes images, converts them to WebP or AVIF, infers dimensions to prevent layout shift, and adds lazy loading. This makes achieving excellent Core Web Vitals scores the default rather than something you have to chase.
---
import { Image, Picture } from 'astro:assets';
import heroImage from '../assets/hero.jpg'; // Local image import
---
<!-- Astro automatically:
✓ Converts to WebP/AVIF for smaller file sizes
✓ Adds width/height attributes to prevent CLS
✓ Adds loading="lazy" and decoding="async"
✓ Generates responsive srcset for different screen sizes -->
<Image
src={heroImage}
width={800}
height={400}
alt="Hero image — describe the content meaningfully"
format="webp"
quality={85}
/>
<!-- Picture for art direction with multiple formats -->
<Picture
src={heroImage}
formats={['avif', 'webp']}
width={800}
height={400}
alt="Hero image"
/>
Astro sites routinely score 95–100 on Google Lighthouse performance audits without any manual tuning. The zero-JS default eliminates render-blocking scripts, the Image component prevents Cumulative Layout Shift (CLS), and static generation delivers sub-100ms Time to First Byte from a CDN edge. You get a perfect score as a starting point, not a goal.
When to Choose Astro
Astro isn't the right tool for every project — it shines brightest in specific scenarios. If your application is highly interactive (a complex dashboard, a real-time collaboration tool, a single-page app with deep client-side state), a dedicated SPA framework remains the better choice. But for the vast majority of websites — content-heavy sites that need speed, SEO, and low maintenance overhead — Astro's "ship less JavaScript" philosophy is genuinely transformative.
Astro is the ideal choice when:
- Your site is primarily content (blogs, documentation, portfolios, marketing)
- SEO performance and fast initial page loads are critical requirements
- Your team wants to reuse existing React, Vue, or Svelte components
- You're migrating away from Gatsby and want type-safe content management
- You need a fast static site with a few interactive widgets — not a full SPA
Key Takeaways
- Zero JS by default — pages load nearly instantly because the browser gets pre-rendered HTML, not a JavaScript bundle
- Islands Architecture — add interactivity precisely where you need it with
client:directives, not everywhere - Multi-framework support — use React, Vue, Svelte, and Solid components in the same project
- Content Collections — type-safe, schema-validated content management with Zod, no CMS required
- Three output modes — static, server, and hybrid cover everything from a personal blog to a personalised SaaS app
- Built-in image optimisation — WebP conversion, lazy loading, and CLS prevention are automatic, not an afterthought
"Astro represents a fundamental rethinking of how we build the web. By making JavaScript opt-in rather than opt-out, it puts performance first without sacrificing developer happiness."
— Fred Schott, Co-creator of Astro
Astro's approach to web performance feels obvious in hindsight: most of the internet is content, and content doesn't need a JavaScript runtime. Start with npm create astro@latest, pick the blog template, and experience what it feels like to build a site that's fast by design rather than by optimisation effort.