Introduction
Astro is a web framework designed specifically for building content-driven websites. It pioneered the “islands architecture” concept and ships zero JavaScript to the client by default. This comprehensive guide covers everything you need to build blazing-fast websites with Astro.
Understanding Astro
What is Astro?
Astro is a static-first web framework that renders HTML on the server and only sends JavaScript when absolutely necessary. It’s perfect for blogs, documentation, marketing sites, and e-commerce.
graph TB
subgraph "Traditional SPA"
JS1[React App]
User1[User]
Load[Initial Load: 150KB+ JS]
User1 --> Load
Load --> JS1
end
subgraph "Astro"
HTML[HTML Only]
Islands[Interactive Islands]
User2[User]
Load2[Initial Load: 0-10KB JS]
User2 --> Load2
Load2 --> HTML
HTML -.->|hydrate| Islands
end
Key Concepts
| Concept | Description |
|---|---|
| Zero JS by Default | No JavaScript sent to client unless needed |
| Islands Architecture | Interactive components isolated in islands |
| Server-First | HTML generated on server |
| Framework Agnostic | Use React, Vue, Svelte together |
| Content Collections | Type-safe content management |
When to Use Astro
| Use Case | Why Astro? |
|---|---|
| Blog | Static generation, fast loads |
| Documentation | MDX support, great DX |
| Marketing Site | Fast, SEO-friendly |
| Portfolio | Simple, beautiful output |
| E-commerce | Fast product pages |
Getting Started
Installation
# Create new Astro project
npm create astro@latest my-website
# Or with specific template
npm create astro@latest my-blog -- --template blog
npm create astro@latest my-docs -- --template docs
# Add to existing project
npx astro add react
npx astro add tailwind
npx astro add svelte
Project Structure
my-astro-project/
โโโ src/
โ โโโ components/
โ โ โโโ Header.astro
โ โ โโโ Counter.jsx # React component
โ โโโ layouts/
โ โ โโโ Layout.astro
โ โโโ pages/
โ โ โโโ index.astro
โ โ โโโ blog/
โ โ โโโ [slug].astro
โ โโโ content/
โ โ โโโ blog/
โ โ โโโ post-1.md
โ โ โโโ post-2.md
โ โโโ styles/
โ โโโ global.css
โโโ public/
โ โโโ images/
โโโ astro.config.mjs
โโโ package.json
Basic Page
---
// src/pages/index.astro
// This is the "frontmatter" - server-side JavaScript
// Import components
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
// This runs at build time
const title = "My Awesome Site";
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
---
<!-- This is the template - renders to HTML -->
<Layout title={title}>
<Header />
<main>
<h1>Welcome to {title}</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</main>
<Footer />
</Layout>
<style>
/* Scoped CSS */
main {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: blue;
}
</style>
Components
Astro Components
---
// src/components/Card.astro
interface Props {
title: string;
description?: string;
href: string;
}
const { title, description, href } = Astro.props;
---
<article class="card">
<h2>
<a href={href}>{title}</a>
</h2>
{description && <p>{description}</p>}
<a href={href}>Read more โ</a>
</article>
<style>
.card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
</style>
Using the Component
---
import Card from '../components/Card.astro';
---
<Card
title="My Post"
description="A great article"
href="/blog/my-post"
/>
React/Vue/Svelte Components
---
// Import any framework component
import Counter from '../components/Counter.jsx';
import TodoList from '../components/TodoList.svelte';
import Header from '../components/Header.vue';
---
<!-- Static HTML -->
<Header />
<!--
Interactive Island!
Only this component sends JavaScript to client
-->
<Counter client:visible />
<!-- Load when visible (lazy) -->
<TodoList client:visible />
<!-- Load immediately -->
<Counter client:load />
<!-- Only on mobile -->
<Counter client:only="mobile" />
Client Directives
| Directive | Description |
|---|---|
client:load |
Load immediately on page load |
client:visible |
Load when element enters viewport |
client:idle |
Load when browser is idle |
client:media |
Load when media query matches |
client:only |
Skip server rendering, client only |
Content Collections
Defining Collections
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // v2.0+ API
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Creating Content
---
# src/content/blog/first-post.md
title: "My First Blog Post"
description: "Hello World!"
pubDate: 2026-02-22
tags: ["getting-started"]
---
# Hello World
This is my first post in Astro!
Querying Content
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
// Get all blog posts
const posts = await getCollection('blog');
// Filter and sort
const sortedPosts = posts
.filter(post => !post.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<ul>
{sortedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
Layouts and Templates
Base Layout
---
// src/layouts/Layout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = "My awesome site" } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta name="description" content={description}>
<title>{title}</title>
</head>
<body>
<slot /> <!-- Page content goes here -->
</body>
</html>
Blog Post Layout
---
// src/layouts/BlogPost.astro
import type { CollectionEntry } from 'astro:content';
import Layout from './Layout.astro';
interface Props {
post: CollectionEntry<'blog'>;
}
const { post } = Astro.props;
const { title, description, pubDate, heroImage } = post.data;
---
<Layout title={title} description={description}>
<article>
{heroImage && <img src={heroImage} alt={title} />}
<h1>{title}</h1>
<time datetime={pubDate.toISOString()}>
{pubDate.toLocaleDateString()}
</time>
<slot /> <!-- Content goes here -->
</article>
</Layout>
Routing
Static Routes
---
// src/pages/about.astro
// Becomes /about
---
<h1>About Page</h1>
Dynamic Routes
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props();
const { Content } = await post.render();
---
<BlogPost {...post.data}>
<Content />
</BlogPost>
Route Parameters
// [category]/[page].astro
export function getStaticPaths() {
return [
{ params: { category: 'tech', page: '1' } },
{ params: { category: 'tech', page: '2' } },
{ params: { category: 'life', page: '1' } },
];
}
Data Fetching
Fetch in Frontmatter
---
// Fetch at build time
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<ul>
{data.items.map(item => (
<li>{item.name}</li>
))}
</ul>
API Routes
// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const posts = await getPosts(); // Your data fetching
return new Response(JSON.stringify(posts), {
headers: {
'Content-Type': 'application/json',
},
});
}
Styling
Scoped CSS
<style>
/* Only applies to this component */
.highlight {
background: yellow;
}
</style>
Global CSS
---
// src/pages/index.astro
---
<style is:global>
/* Applies to entire site */
html {
font-family: system-ui;
}
</style>
Tailwind CSS
npx astro add tailwind
---
// Use Tailwind classes
---
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Click Me
</button>
Integrations
React
npx astro add react
---
// src/components/ReactCounter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Markdown & MDX
npx astro add mdx
---
# src/content/blog/with-mdx.mdx
title: "MDX Blog Post"
---
import Counter from '../../components/Counter.jsx';
# Using Components in Markdown
<Counter client:visible />
Partytown (Analytics)
npx astro add partytown
<!-- Track page views without blocking -->
<script type="text/partytown" src="https://analytics.example.com/script.js" />
Performance Optimization
Image Optimization
npx astro add image
---
import { Image } from 'astro:assets';
import myImage from '../assets/hero.png';
---
<Image
src={myImage}
alt="Hero image"
width={800}
height={600}
format="webp"
/>
Prefetching
<!-- Add to layout -->
<head>
<link rel="prefetch" href="/blog/post-2" />
</head>
<!-- Or use Astro's built-in -->
<a href="/blog/post-2" data-astro-prefetch>Post 2</a>
View Transitions
---
// Enable in Astro 3.0+
import { ViewTransitions } from 'astro:transitions';
---
<head>
<ViewTransitions />
</head>
Deployment
Static (SSG)
# Build to static files
npm run build
# Output: dist/
# Deploy to any static host
# Netlify, Vercel, Cloudflare Pages, etc.
Server-Side Rendering (SSR)
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
});
# Deploy with Node
node dist/server/entry.mjs
Edge
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/edge';
export default defineConfig({
output: 'server',
adapter: vercel({
imageService: true,
}),
});
Astro vs Next.js
| Feature | Astro | Next.js |
|---|---|---|
| JavaScript | Zero by default | Always present |
| Islands | Native | Custom |
| Routing | File-based | File-based |
| Data Fetching | Build + Runtime | Server Components |
| React Support | Yes | Native |
| MDX | Built-in | Next MDX |
| Learning Curve | Easy | Moderate |
| Ecosystem | Growing | Large |
Conclusion
Astro is the perfect choice for:
- Content-focused websites that don’t need complex client-side state
- Teams that want simplicity over feature complexity
- Performance-critical applications where every byte matters
- Developers who prefer HTML over JavaScript
Start with Astro’s default settings, add interactive islands only when needed, and enjoy blazing-fast websites.
Comments