Astro
Astro cheat sheet with islands architecture, component syntax, content collections, routing, and SSG/SSR patterns with code examples.
10 min read
astrostatic-siteislandsjavascripttypescriptssrview-transitions
Loading your progress
Getting Started
Project setup and basic configuration
bash
# Interactive CLI setup
npm create astro@latest
# With specific template
npm create astro@latest -- --template blog
# DETAILED_TAB:
# Create and start project
npm create astro@latest my-site
cd my-site
npm install
npm run dev
# Available templates:
# minimal, blog, portfolio, docs✅ CLI guides you through setup with TypeScript, dependencies, and git
💡 Dev server runs on http://localhost:4321 by default
🔍 Official templates: minimal, blog, portfolio, docs
⚡ Supports React, Vue, Svelte, Solid, Preact, Alpine.js
setupcliinstall
text
my-astro-site/
├── src/
│ ├── components/ # Reusable components (.astro, .jsx, .vue)
│ ├── layouts/ # Page layouts with <slot />
│ ├── pages/ # File-based routing
│ ├── content/ # Content collections (Markdown/MDX)
│ └── assets/ # Images, fonts processed by Astro
├── public/ # Static assets (copied as-is)
└── astro.config.mjs # Astro configuration✅ src/pages/ creates routes automatically via file name
💡 public/ files served at root without processing
🔍 src/content/ requires content.config.ts for collections
⚡ All file types supported: .astro, .js, .ts, .jsx, .tsx, .vue, .svelte
structureorganizationfiles
Components & Syntax
Astro component structure, props, and slots
astro
---
// Component Script (server-side only)
import Header from './Header.astro';
const name = "World";
// Fetch data at build time
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<!-- Component Template -->
<Header />
<h1>Hello {name}!</h1>
<p>{data.message}</p>
<style>
h1 { color: blue; }
</style>✅ Components have two sections: script (---) and template (HTML)
💡 Script runs only on server at build time or on-demand
🔍 Use {expression} for JavaScript in template
⚡ Styles are scoped to component automatically
componentsyntaxstructure
astro
---
interface Props {
title: string;
count?: number;
items: string[];
}
const { title, count = 0, items } = Astro.props;
---
<div>
<h2>{title}</h2>
<p>Count: {count}</p>
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
</div>✅ Access props via Astro.props object
💡 Use interface Props for TypeScript type checking
🔍 Destructure with default values for optional props
⚡ Functions cannot be passed to hydrated framework components
propstypescripttypes
astro
---
// Layout.astro
---
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- default slot -->
</main>
<footer>
<slot name="footer">
<p>Default footer content</p>
</slot>
</footer>
</div>✅ <slot /> renders child content passed to component
💡 Named slots allow multiple content areas
🔍 Fallback content appears if slot is empty
⚡ Use Astro.slots API to check if slot has content
slotscompositionlayouts
Routing & Pages
File-based routing and dynamic routes
text
src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ ├── post-1.astro → /blog/post-1
│ └── [slug].astro → /blog/:slug
├── api/
│ └── posts.json.ts → /api/posts.json
└── _hidden.astro → Not routed (underscore)✅ Each file in src/pages/ becomes a route automatically
💡 index.astro maps to parent directory path
🔍 Prefix with _ to exclude from routing
⚡ .astro, .md, .mdx, .html, .js, .ts all supported
routingfile-basedpages
astro
---
// pages/posts/[slug].astro
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
const { slug } = Astro.params;
---
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>✅ getStaticPaths() generates all routes at build time
💡 Return array of { params, props } objects
🔍 Access params via Astro.params, props via Astro.props
⚡ Use [...rest] for catch-all routes of any depth
dynamic-routesgetStaticPathsparams
Content Collections
Type-safe content management with Markdown/MDX
typescript
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
pubDate: z.date(),
author: z.string(),
tags: z.array(z.string()),
draft: z.boolean().optional(),
}),
});
export const collections = { blog };✅ defineCollection() configures collection with loader and schema
💡 Use glob() loader for file-based content
🔍 Zod schema validates frontmatter at build time
⚡ Store content in any directory, specify with base option
contentcollectionsschemadefineCollection
astro
---
import { getCollection, getEntry } from 'astro:content';
// Get all entries
const allPosts = await getCollection('blog');
// Filter entries
const publishedPosts = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
// Get single entry
const post = await getEntry('blog', 'my-post-slug');
---✅ getCollection() fetches entire collection as array
💡 Filter function receives {id, data} for each entry
🔍 getEntry() fetches single entry by collection and id
⚡ Access frontmatter via .data, entry id via .id
getCollectiongetEntryquery
astro
---
// pages/blog/[id].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<!-- Table of contents -->
<nav>
<ul>
{headings.map(h => (
<li style={\`margin-left: \${h.depth * 10}px\`}>
<a href={\`#\${h.slug}\`}>{h.text}</a>
</li>
))}
</ul>
</nav>
<Content />
</article>✅ render() processes Markdown/MDX into renderable Content component
💡 Returns headings array for table of contents generation
🔍 Content component renders the processed HTML
⚡ Works with both Markdown and MDX files
rendercontentmarkdown
Framework Components
Using React, Vue, Svelte with client directives
astro
---
import Counter from './Counter.jsx';
---
<!-- Default: static HTML only, no JS -->
<Counter />
<!-- Load immediately on page load -->
<Counter client:load />
<!-- Load when browser is idle -->
<Counter client:idle />
<!-- Load when scrolled into view -->
<Counter client:visible />
<!-- Load on media query match -->
<Counter client:media="(max-width: 768px)" />
<!-- Only render on client, skip SSR -->
<Counter client:only="react" />✅ Framework components render as static HTML by default
💡 client:* directives add interactivity (ship JavaScript)
🔍 client:load for above-fold critical components
⚡ client:visible for lazy loading below-the-fold content
clienthydrationislandsdirectives
astro
---
import ReactCounter from './ReactCounter.jsx';
import VueButton from './VueButton.vue';
import SvelteCard from './SvelteCard.svelte';
---
<div>
<ReactCounter client:load />
<VueButton client:idle />
<SvelteCard client:visible />
</div>✅ Mix React, Vue, Svelte, Solid in same .astro file
💡 Each framework only ships its own runtime when used
🔍 Cannot mix frameworks within same framework component file
⚡ Supported props: strings, numbers, objects, arrays, dates, maps, sets
frameworksreactvuesveltemulti-framework
Images & Assets
Optimized image handling with Image component
astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/my-image.png';
---
<!-- Local image (src/) -->
<Image src={myImage} alt="Description" />
<!-- Public folder image -->
<Image
src="/images/photo.jpg"
alt="Description"
width={600}
height={400}
/>
<!-- Remote image (requires config) -->
<Image
src="https://example.com/image.jpg"
alt="Description"
width={600}
height={400}
/>✅ Image component automatically optimizes at build time
💡 Local images (src/) auto-detect dimensions to prevent CLS
🔍 Remote images need authorization in astro.config.mjs
⚡ Picture component generates multiple formats and sizes
imagesImageassetsoptimization
View Transitions
Animated page transitions and SPA routing
astro
---
// Layout.astro
import { ClientRouter } from 'astro:transitions';
---
<html>
<head>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>✅ ClientRouter enables SPA-like navigation with animations
💡 Built-in animations: fade (default), slide, none, initial
🔍 transition:name pairs elements between old/new pages
⚡ transition:persist keeps elements across navigation
transitionsClientRouteranimationsspa
astro
---
import { navigate } from 'astro:transitions/client';
---
<!-- Disable transitions for specific link -->
<a href="/about" data-astro-reload>Full page reload</a>
<!-- Control history behavior -->
<a href="/page" data-astro-history="replace">
Replace history
</a>
<script>
// Programmatic navigation
navigate('/dashboard', {
history: 'push' // or 'replace', 'auto'
});
// Listen to lifecycle events
document.addEventListener('astro:page-load', () => {
console.log('New page loaded');
// Re-initialize scripts
});
</script>✅ navigate() triggers transitions programmatically
💡 data-astro-reload forces full page navigation
🔍 astro:page-load ideal for re-initializing scripts
⚡ Respects prefers-reduced-motion automatically
navigatelifecycleevents
API Routes & Endpoints
Server endpoints and API route handling
typescript
// pages/api/posts.json.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
const posts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
];
return new Response(JSON.stringify(posts), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
return new Response(JSON.stringify({ success: true, data }), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
};✅ Export GET, POST, PUT, DELETE functions from .ts/.js files
💡 File extension determines output: .json.ts → .json
🔍 Access params, request, redirect from context object
⚡ SSR mode: live routes. Static mode: pre-rendered files
apiendpointsroutesAPIRoute
Modern Astro (Server Islands, Actions, Sessions)
Server Islands, Actions, and session middleware (Astro 4.15+ / 5+)
Static page + dynamic server-rendered components streamed in
astro
---
// src/pages/product/[id].astro
import StaticInfo from '../../components/StaticInfo.astro'
import Cart from '../../components/Cart.astro'
---
<html>
<body>
<StaticInfo id={Astro.params.id} />
{/* server:defer renders this on the server AFTER the static HTML ships */}
<Cart server:defer>
<CartFallback slot="fallback" />
</Cart>
</body>
</html>💡 Static HTML + server-streamed chunks in one response — no client JS to hydrate
📌 Requires an adapter and a deployment target that supports streaming
⚡ Always provide a <slot name="fallback"> so the static shell renders cleanly
🎯 Use for cart, "recently viewed", per-user banners on cacheable marketing pages
server-islandsstreamingmodern
Type-safe server functions callable from the client (Astro 4.15+)
typescript
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
createComment: defineAction({
accept: 'form',
input: z.object({
postId: z.string(),
body: z.string().min(1).max(2000),
}),
handler: async ({ postId, body }, { cookies }) => {
const user = cookies.get('userId')?.value
if (!user) throw new Error('Not signed in')
return db.comment.create({ data: { postId, body, userId: user } })
},
}),
}
// Use from a client component
<form method="POST" action={actions.createComment}>
<input name="postId" value={post.id} hidden />
<textarea name="body"></textarea>
<button>Comment</button>
</form>💡 Astro Actions = type-safe server RPC with zod validation built in
📌 accept: "form" makes the action work as a plain progressive HTML form
⚡ ActionError codes map cleanly to HTTP statuses — use them, don't hand-roll
🎯 Pair with islands for forms that work without JS but enhance with it
actionsrpcforms
Built-in sessions (5.1+) and the middleware request/response chain
typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
export const onRequest = defineMiddleware(async (context, next) => {
// Read or write per-request session data
const cart = await context.session?.get('cart') ?? []
context.locals.cart = cart
const res = await next()
return res
})
// astro.config.mjs: sessions are stable in Astro 6+ (no experimental flag)
import { defineConfig, sessionDrivers } from 'astro/config'
export default defineConfig({
session: {
driver: sessionDrivers.redis({ url: process.env.REDIS_URL }),
},
})💡 Middleware runs on every request — set locals there, read in pages
⚡ sequence() composes multiple middlewares cleanly in render order
📌 Sessions are stable in Astro 6+ (no experimental flag); set driver via sessionDrivers, or let the Node/Netlify/Cloudflare adapter auto-configure one
🎯 Always declare App.Locals and App.SessionData in env.d.ts for full type safety
middlewaresessionsauth