
Astro
- 67 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
astro is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- astro
- AI & Agent Building
- AI-coding skill
Astro by the numbers
- 67 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,935 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill astroAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Astro
Overview
Astro is an all-in-one web framework for building fast, content-focused websites. It uses an islands architecture that ships zero JavaScript by default, hydrating only interactive components on demand. Components from React, Svelte, Vue, Solid, and Preact can coexist in a single project.
When to use: Content-driven sites (blogs, docs, marketing), portfolios, e-commerce storefronts, any site where most pages are primarily static with isolated interactive regions.
When NOT to use: Highly interactive single-page applications (dashboards, real-time collaboration tools), apps requiring full client-side routing with shared global state across all components.
Quick Reference
| Pattern | API / Directive | Key Points |
|---|---|---|
| Content collection | defineCollection({ loader, schema }) | Zod schemas, glob/file loaders, type-safe queries |
| Query collection | getCollection('blog') | Returns typed array, supports filter callback |
| Single entry | getEntry('blog', 'my-post') | Fetch by collection name and entry ID |
| Island (load) | <Component client:load /> | Hydrate immediately on page load |
| Island (idle) | <Component client:idle /> | Hydrate when browser is idle |
| Island (visible) | <Component client:visible /> | Hydrate when component enters viewport |
| Island (media) | <Component client:media="(max-width: 768px)" /> | Hydrate on media query match |
| Island (client-only) | <Component client:only="react" /> | Skip SSR, render only on client |
| View transitions | <ClientRouter /> | Add to <head>, enables SPA-like navigation |
| Persist state | transition:persist | Maintain island state across navigations |
| Programmatic navigate | navigate(href) | Client-side navigation from scripts |
| Static output | output: 'static' | Pre-render all pages at build time (default) |
| Server output | output: 'server' | Server-render all pages on demand |
| Hybrid (opt-in SSR) | output: 'static' + per-page prerender = false | Static by default, opt individual pages into SSR |
| Hybrid (opt-in static) | output: 'server' + per-page prerender = true | SSR by default, opt individual pages into static |
| Server islands | <Component server:defer /> | Defer server rendering for dynamic content in static |
| Middleware | onRequest(context, next) | Runs before every route, chain with sequence() |
| Astro DB table | defineTable({ columns }) | Type-safe SQL with column definitions |
| Framework component | Import .jsx / .svelte / .vue | Auto-detected by file extension |
| Integration | astro add react | CLI to add framework adapters and tools |
| Render content | const { Content } = await entry.render() | Compile Markdown/MDX to component |
| Dynamic routes | getStaticPaths() + collection query | Generate pages from collection entries |
| API endpoint | export const GET: APIRoute | Server-rendered REST endpoints |
| Shared island state | nanostores | Reactive state across framework boundaries |
| Environment variables | import.meta.env.PUBLIC_* | PUBLIC_ prefix for client-accessible vars |
| Transition animation | transition:animate="slide" | initial, fade, slide, none |
| Prefetch links | data-astro-prefetch | hover, viewport, load, or false |
| Collection reference | reference('authors') | Type-safe cross-collection relations |
| Script re-execution | data-astro-rerun | Re-run <script> on every view transition navigation |
| Redirect | context.redirect(url, status) | Redirect from middleware or server pages |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Adding client: directive to .astro components | Only UI framework components (React, Svelte, Vue) accept client: |
Using client:load everywhere | Default to client:idle or client:visible; use client:load sparingly |
Forgetting framework string with client:only | Must specify framework: client:only="react" |
| Mixing framework components in non-Astro files | Only .astro files can compose components from multiple frameworks |
Using output: 'server' for mostly static sites | Use output: 'static' with per-page prerender = false for hybrid |
Omitting <ClientRouter /> for view transitions | Must be in <head> of every page (use shared layout) |
Content config not at src/content.config.ts | File must be named content.config.ts in src/ root |
Not awaiting getCollection() calls | Always await collection queries in frontmatter |
Importing from astro:content in client scripts | Content APIs are server-only; pass data as props to client components |
Placing middleware outside src/middleware.ts | Middleware must be at src/middleware.ts or src/middleware/index.ts |
| Passing functions as props to islands | Only serializable data crosses the server/client boundary |
Using transition:persist without matching pages | Component must appear on both old and new page with same persist value |
| Missing adapter for server/hybrid mode | Install an adapter: npx astro add vercel (or node, netlify, etc.) |
Delegation
- Pattern discovery: Use
Exploreagent - Build configuration: Use
Taskagent - Code review: Delegate to
code-revieweragent
If the tailwind skill is available, delegate styling and Tailwind CSS configuration to it.If the vitest skill is available, delegate unit testing patterns to it.If the playwright skill is available, delegate end-to-end testing patterns to it.If the vite skill is available, delegate build configuration and Vite plugin setup to it.If the drizzle skill is available, delegate advanced database query patterns to it.If the sentry skill is available, delegate error monitoring and observability setup to it.If the pino-logging skill is available, delegate server-side logging configuration to it.If the tanstack-query skill is available, delegate client-side data fetching and caching to it.References
- Content collections, schemas, loaders, and querying
- Island architecture and hydration directives
- View transitions and client-side navigation
- Rendering modes: static, server, and hybrid
- Middleware patterns and request handling
- Framework component integration (React, Svelte, Vue)
- Astro DB schema, seeding, and queries
Astro DB
Astro DB is a built-in SQL database designed for Astro projects. It uses a Drizzle-like API with type-safe table definitions and queries.
Setup
npx astro add dbDefining Tables
Define tables in db/config.ts:
import { defineDb, defineTable, column } from 'astro:db';
const Comment = defineTable({
columns: {
id: column.number({ primaryKey: true }),
postSlug: column.text(),
author: column.text(),
body: column.text(),
publishedAt: column.date({ default: new Date() }),
},
});
const Like = defineTable({
columns: {
id: column.number({ primaryKey: true }),
postSlug: column.text(),
userId: column.text(),
},
indexes: [{ on: ['postSlug', 'userId'], unique: true }],
});
export default defineDb({
tables: { Comment, Like },
});Column Types
| Type | Usage |
|---|---|
column.text() | String values |
column.number() | Integer values |
column.boolean() | True/false |
column.date() | Date objects |
column.json() | JSON-serializable data |
Column Options
const Post = defineTable({
columns: {
id: column.number({ primaryKey: true }),
title: column.text(),
views: column.number({ default: 0 }),
metadata: column.json({ optional: true }),
published: column.boolean({ default: false }),
},
});Seeding Data
Create seed data in db/seed.ts:
import { db, Comment } from 'astro:db';
export default async function seed() {
await db.insert(Comment).values([
{
postSlug: 'getting-started',
author: 'Alice',
body: 'Great introduction!',
publishedAt: new Date('2024-01-15'),
},
{
postSlug: 'getting-started',
author: 'Bob',
body: 'Very helpful, thanks!',
publishedAt: new Date('2024-01-16'),
},
]);
}Querying
Select All
---
import { db, Comment } from 'astro:db';
const comments = await db.select().from(Comment);
---
<ul>
{comments.map((c) => (
<li>
<strong>{c.author}</strong>: {c.body}
</li>
))}
</ul>Filtered Query
import { db, Comment, eq } from 'astro:db';
const postComments = await db
.select()
.from(Comment)
.where(eq(Comment.postSlug, 'getting-started'))
.orderBy(Comment.publishedAt);Insert
import { db, Comment } from 'astro:db';
await db.insert(Comment).values({
postSlug: slug,
author: formData.get('author') as string,
body: formData.get('body') as string,
});Update
import { db, Comment, eq } from 'astro:db';
await db
.update(Comment)
.set({ body: 'Updated comment text' })
.where(eq(Comment.id, commentId));Delete
import { db, Comment, eq } from 'astro:db';
await db.delete(Comment).where(eq(Comment.id, commentId));Join
import { db, Comment, Like, eq } from 'astro:db';
const commentsWithLikes = await db
.select()
.from(Comment)
.leftJoin(Like, eq(Comment.postSlug, Like.postSlug));Using in API Routes
Astro DB queries work in server-rendered pages and API endpoints.
import type { APIRoute } from 'astro';
import { db, Comment, eq } from 'astro:db';
export const GET: APIRoute = async ({ params }) => {
const comments = await db
.select()
.from(Comment)
.where(eq(Comment.postSlug, params.slug!));
return new Response(JSON.stringify(comments), {
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request, params }) => {
const body = await request.json();
await db.insert(Comment).values({
postSlug: params.slug!,
author: body.author,
body: body.body,
});
return new Response(JSON.stringify({ success: true }), { status: 201 });
};Remote Database
For production, connect to a hosted Astro Studio database or use the @astrojs/db libSQL remote driver.
import { defineConfig } from 'astro/config';
import db from '@astrojs/db';
export default defineConfig({
integrations: [db()],
});Run astro db push to apply schema changes to the remote database.
Content Collections
Defining Collections
Collections are configured in src/content.config.ts. Each collection requires a loader and optionally a schema for type-safe validation.
import { defineCollection } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
image: z
.object({
url: z.string(),
alt: z.string(),
})
.optional(),
}),
});
const authors = defineCollection({
loader: file('src/data/authors.json'),
schema: z.object({
id: z.string(),
name: z.string(),
bio: z.string(),
avatar: z.string().url(),
}),
});
export const collections = { blog, authors };Built-in Loaders
Glob Loader
Loads files matching a glob pattern from the local filesystem. Each file becomes a collection entry with an auto-generated ID based on the file path.
import { glob } from 'astro/loaders';
const docs = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: './src/data/docs' }),
schema: z.object({
title: z.string(),
order: z.number(),
}),
});File Loader
Loads structured data from a single JSON or YAML file. Each item in the array becomes a collection entry.
import { file } from 'astro/loaders';
const products = defineCollection({
loader: file('src/data/products.json'),
schema: z.object({
id: z.string(),
name: z.string(),
price: z.number(),
category: z.string(),
}),
});Querying Collections
Get All Entries
---
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
const publishedPosts = await getCollection('blog', ({ data }) => {
return !data.draft;
});
const sortedPosts = publishedPosts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<ul>
{sortedPosts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>
<h2>{post.data.title}</h2>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
</a>
</li>
))}
</ul>Get Single Entry
---
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-first-post');
if (!post) {
return Astro.redirect('/404');
}
---
<h1>{post.data.title}</h1>Rendering Content
Use render() to compile Markdown/MDX content to HTML.
---
import { getEntry } from 'astro:content';
const post = await getEntry('blog', Astro.params.slug);
if (!post) {
return Astro.redirect('/404');
}
const { Content, headings } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>Dynamic Routes with Collections
Generate static pages from collection entries using getStaticPaths.
---
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();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>Schema Patterns
Image Schema with Astro Image
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: ({ image }) =>
z.object({
title: z.string(),
cover: image(),
}),
});Reference Between Collections
import { defineCollection, z, reference } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
author: reference('authors'),
relatedPosts: z.array(reference('blog')).default([]),
}),
});
const authors = defineCollection({
schema: z.object({
name: z.string(),
bio: z.string(),
}),
});Resolving References
---
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-post');
const author = await getEntry(post.data.author);
---
<p>Written by {author.data.name}</p>Framework Integration
Astro supports React, Svelte, Vue, Solid, Preact, and Lit components. Framework components are auto-detected by file extension.
Adding Framework Support
npx astro add react
npx astro add svelte
npx astro add vueThis installs the framework package and Astro integration, and updates astro.config.mjs:
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [react(), svelte(), vue()],
});Using Framework Components
Import and use components in .astro files. Without a client: directive, they render to static HTML with no JavaScript.
---
import ReactCard from '../components/ReactCard.jsx';
import SvelteToggle from '../components/SvelteToggle.svelte';
import VueModal from '../components/VueModal.vue';
---
<ReactCard title="Static React" />
<SvelteToggle client:idle />
<VueModal client:visible />Passing Props
Serializable values can be passed as props. Functions, classes, and other non-serializable values cannot cross the server/client boundary.
---
import Counter from '../components/Counter.jsx';
const items = [
{ id: 1, name: 'Item A' },
{ id: 2, name: 'Item B' },
];
---
<Counter
client:idle
initialCount={0}
items={items}
label="Click count"
/>Slots and Children
Default Slot
---
import Wrapper from '../components/Wrapper.jsx';
---
<Wrapper client:idle>
<p>This becomes children in React or default slot in Vue/Svelte.</p>
</Wrapper>React component receiving children:
type WrapperProps = {
children: React.ReactNode;
};
export default function Wrapper({ children }: WrapperProps) {
return <div className="wrapper">{children}</div>;
}Named Slots
Named slots work with Svelte and Vue components.
---
import Layout from '../components/Layout.svelte';
---
<Layout client:idle>
<h1 slot="header">Page Title</h1>
<p>Main content goes here.</p>
<footer slot="footer">Footer content</footer>
</Layout>Svelte component:
<div>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>Sharing State Between Islands
Islands are isolated by default. Use nano stores for shared reactive state across framework boundaries.
npm install nanostores @nanostores/react @nanostores/vueDefine a shared store:
import { atom } from 'nanostores';
export const cartCount = atom(0);
export function addToCart() {
cartCount.set(cartCount.get() + 1);
}React island:
import { useStore } from '@nanostores/react';
import { cartCount, addToCart } from '../stores/cart';
export default function AddToCartButton() {
const count = useStore(cartCount);
return <button onClick={addToCart}>Add to Cart ({count})</button>;
}Vue island:
<script setup lang="ts">
import { useStore } from '@nanostores/vue';
import { cartCount } from '../stores/cart';
const count = useStore(cartCount);
</script>
<template>
<span>Cart: {{ count }}</span>
</template>React with TypeScript
Use .tsx extension for React components with TypeScript.
type CardProps = {
title: string;
description: string;
href?: string;
};
export default function Card({ title, description, href }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
<p>{description}</p>
{href ? <a href={href}>Learn more</a> : null}
</div>
);
}Common Integrations
npx astro add tailwind
npx astro add mdx
npx astro add sitemap
npx astro add db
npx astro add partytownimport { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [react(), tailwind(), mdx(), sitemap()],
});Island Architecture
Astro renders all components to static HTML by default. Interactive "islands" are created by adding a client: directive to UI framework components (React, Svelte, Vue, Solid, Preact). Astro ships zero JavaScript unless a client: directive is explicitly used.
Hydration Directives
client:load
Hydrates immediately when the page loads. Use for above-the-fold interactive elements that must be ready instantly.
---
import Navigation from '../components/Navigation.jsx';
---
<Navigation client:load />client:idle
Hydrates once the browser is idle (uses requestIdleCallback). Best default choice for most interactive components.
---
import Newsletter from '../components/Newsletter.jsx';
---
<Newsletter client:idle />client:visible
Hydrates when the component scrolls into the viewport (uses IntersectionObserver). Ideal for below-the-fold content.
---
import Comments from '../components/Comments.jsx';
---
<Comments client:visible />client:media
Hydrates when a CSS media query matches. Useful for mobile-only or desktop-only interactive components.
---
import MobileMenu from '../components/MobileMenu.jsx';
---
<MobileMenu client:media="(max-width: 768px)" />client:only
Skips server-side rendering entirely. The component renders only on the client. Must specify the framework as a string value.
---
import BrowserOnlyChart from '../components/BrowserOnlyChart.jsx';
---
<BrowserOnlyChart client:only="react" />Choosing the Right Directive
| Priority | Directive | Example Use Case |
|---|---|---|
| Critical (instant) | client:load | Auth state, navigation, hero CTA |
| Standard | client:idle | Newsletter signup, search bar |
| Deferred | client:visible | Comments, footer widgets, carousels |
| Conditional | client:media | Mobile hamburger menu |
| No SSR | client:only | Browser API dependent (canvas, WebGL) |
Mixing Frameworks
Only .astro files can contain components from multiple UI frameworks in the same template.
---
import ReactCounter from '../components/ReactCounter.jsx';
import SvelteToggle from '../components/SvelteToggle.svelte';
import VueCard from '../components/VueCard.vue';
---
<div>
<ReactCounter client:idle initialCount={0} />
<SvelteToggle client:visible />
<VueCard client:idle title="Hello from Vue" />
</div>Nesting Framework Components
Framework components can be nested as children within other framework components from the same or different framework.
---
import ReactSidebar from '../components/ReactSidebar.jsx';
import ReactButton from '../components/ReactButton.jsx';
import SvelteButton from '../components/SvelteButton.svelte';
---
<ReactSidebar client:idle>
<p>Static content passed as children.</p>
<div slot="actions">
<ReactButton client:idle />
<SvelteButton client:idle />
</div>
</ReactSidebar>Passing Data to Islands
Islands receive props just like regular components. Only serializable data can be passed across the server/client boundary.
---
import { getCollection } from 'astro:content';
import PostFilter from '../components/PostFilter.jsx';
const posts = await getCollection('blog');
const tags = [...new Set(posts.flatMap((p) => p.data.tags))];
---
<PostFilter client:idle tags={tags} postCount={posts.length} />Server Islands
Server islands defer rendering of specific components to after the initial page response. The static HTML shell loads first, then the server island streams in.
---
import UserGreeting from '../components/UserGreeting.astro';
import ProductRecommendations from '../components/ProductRecommendations.astro';
---
<h1>Welcome to our store</h1>
<UserGreeting server:defer>
<p slot="fallback">Loading user info...</p>
</UserGreeting>
<ProductRecommendations server:defer>
<div slot="fallback" class="skeleton" />
</ProductRecommendations>Static Components (No Directive)
Without a client: directive, framework components render to static HTML at build time with zero client-side JavaScript.
---
import Card from '../components/Card.jsx';
---
<Card title="Static" description="No JavaScript shipped for this component." />Middleware
Astro middleware runs before every page and API route. Define middleware in src/middleware.ts (or src/middleware/index.ts).
Basic Middleware
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
return response;
});Setting Locals
Pass data from middleware to pages and endpoints via context.locals.
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const sessionToken = context.cookies.get('session')?.value;
if (sessionToken) {
const user = await validateSession(sessionToken);
context.locals.user = user;
}
return next();
});Access locals in any page or endpoint:
---
const { user } = Astro.locals;
---
{user ? (
<p>Welcome, {user.name}</p>
) : (
<a href="/login">Sign in</a>
)}Type-Safe Locals
Declare the locals type in src/env.d.ts:
declare namespace App {
interface Locals {
user?: {
id: string;
name: string;
email: string;
};
}
}Chaining Middleware with sequence()
Compose multiple middleware functions that run in order.
import { defineMiddleware, sequence } from 'astro:middleware';
const logger = defineMiddleware(async (context, next) => {
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
console.log(
`${context.request.method} ${context.url.pathname} ${duration.toFixed(0)}ms`,
);
return response;
});
const auth = defineMiddleware(async (context, next) => {
const protectedPaths = ['/dashboard', '/settings', '/api/private'];
const isProtected = protectedPaths.some((p) =>
context.url.pathname.startsWith(p),
);
if (isProtected && !context.locals.user) {
return context.redirect('/login');
}
return next();
});
export const onRequest = sequence(logger, auth);Modifying Responses
Adding Headers
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
});Returning Early
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
if (context.url.pathname.startsWith('/api/')) {
const apiKey = context.request.headers.get('x-api-key');
if (apiKey !== import.meta.env.API_KEY) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
}
return next();
});Redirects
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const redirects: Record<string, string> = {
'/old-page': '/new-page',
'/blog/old-slug': '/blog/new-slug',
};
const redirect = redirects[context.url.pathname];
if (redirect) {
return context.redirect(redirect, 301);
}
return next();
});Cookies
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const theme = context.cookies.get('theme')?.value ?? 'light';
context.locals.theme = theme;
const response = await next();
context.cookies.set('visited', 'true', {
path: '/',
maxAge: 60 * 60 * 24 * 365,
httpOnly: true,
secure: true,
sameSite: 'lax',
});
return response;
});Rendering Modes
Astro supports three rendering strategies controlled by the output config option and per-page prerender exports.
Static (Default)
All pages are pre-rendered to HTML at build time. No server runtime needed.
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
});Best for: blogs, documentation, marketing sites, portfolios.
Server
All pages are server-rendered on each request. Requires a server adapter.
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});Best for: dashboards, apps with authentication, personalized content.
Opt Individual Pages into Static
In server mode, add prerender = true to any page to pre-render it at build time.
---
export const prerender = true;
---
<html>
<body>
<h1>This page is pre-rendered at build time</h1>
</body>
</html>Hybrid Rendering
Combine static and server rendering in the same project.
Static Default, Opt-in SSR
Use output: 'static' (default) and opt individual pages into server rendering.
---
export const prerender = false;
---
<html>
<body>
<h1>This page is server-rendered</h1>
<p>User: {Astro.locals.user?.name}</p>
</body>
</html>Server Default, Opt-in Static
Use output: 'server' and opt individual pages into static rendering with prerender = true.
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});Adapters
Server and hybrid modes require a deployment adapter.
npx astro add node
npx astro add vercel
npx astro add netlify
npx astro add cloudflareNode Adapter
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});Vercel Adapter
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});API Routes (Endpoints)
Server-rendered projects can define API endpoints.
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
const data = await fetchDataFromDB(params.id);
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
const result = await saveToDatabase(body);
return new Response(JSON.stringify(result), { status: 201 });
};Environment Variables
Access environment variables differently based on rendering context.
const publicKey = import.meta.env.PUBLIC_API_KEY;
const secretKey = import.meta.env.SECRET_API_KEY;Variables prefixed with PUBLIC_ are available on both server and client. All others are server-only.
Choosing a Rendering Mode
| Scenario | Recommended Mode |
|---|---|
| Blog, docs, marketing pages | static (default) |
| Mostly static, few dynamic pages | static + prerender = false |
| App with auth, mostly dynamic | server + prerender = true for static pages |
| Full dynamic application | server |
View Transitions
Astro provides built-in view transitions via the <ClientRouter /> component, enabling SPA-like page navigation without a full page reload. It uses the browser View Transition API with automatic fallback for unsupported browsers.
Setup
Add <ClientRouter /> to the <head> of your layout to enable view transitions site-wide.
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{Astro.props.title}</title>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>Transition Directives
transition:name
Assign a unique name to pair elements across pages for animated transitions.
<h1 transition:name="page-title">{post.data.title}</h1>
<img transition:name={`hero-${post.id}`} src={post.data.image.url} alt="" />transition:animate
Control the animation style for a transitioning element.
<header transition:animate="none">
<nav>...</nav>
</header>
<main transition:animate="slide">
<slot />
</main>
<aside transition:animate="fade">
<slot name="sidebar" />
</aside>Built-in animations: initial (default), fade, slide, none.
transition:persist
Maintain an island component's DOM and state across navigations instead of replacing it.
<AudioPlayer client:load transition:persist />
<VideoPlayer client:load transition:persist="media-player" />The component must appear on both the old and new page with the same transition:persist value.
Programmatic Navigation
<script>
import { navigate } from 'astro:transitions/client';
document.querySelector('#search-form')?.addEventListener('submit', (e) => {
e.preventDefault();
const query = new FormData(e.currentTarget).get('q');
navigate(`/search?q=${encodeURIComponent(String(query))}`);
});
</script>Navigate Options
import { navigate } from 'astro:transitions/client';
navigate('/dashboard', { history: 'replace' });Options for history: 'auto' (default), 'push', 'replace'.
Lifecycle Events
Listen for view transition events on the document.
<script>
document.addEventListener('astro:before-preparation', (event) => {
const { to, from } = event;
});
document.addEventListener('astro:after-preparation', () => {});
document.addEventListener('astro:before-swap', (event) => {
event.newDocument.querySelector('html')?.classList.add('transition-active');
});
document.addEventListener('astro:after-swap', () => {});
document.addEventListener('astro:page-load', () => {});
</script>| Event | When it fires |
|---|---|
astro:before-preparation | Before fetching the new page |
astro:after-preparation | After fetching, before swap |
astro:before-swap | Before replacing the DOM |
astro:after-swap | After DOM swap, before new scripts run |
astro:page-load | After page is fully loaded and interactive |
Script Re-execution
By default, scripts in <head> only run once. Use data-astro-rerun to re-execute a script on every navigation.
<script data-astro-rerun>
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
</script>Prefetching
Astro prefetches linked pages automatically when <ClientRouter /> is active. Control behavior with the data-astro-prefetch attribute.
<a href="/about" data-astro-prefetch>About</a>
<a href="/contact" data-astro-prefetch="hover">Contact</a>
<a href="/dashboard" data-astro-prefetch="viewport">Dashboard</a>
<a href="/admin" data-astro-prefetch="false">Admin (no prefetch)</a>Fallback Behavior
For browsers that do not support the View Transition API, Astro provides automatic fallback with a standard full-page swap. Configure fallback animation:
import { defineConfig } from 'astro/config';
export default defineConfig({
prefetch: {
defaultStrategy: 'hover',
prefetchAll: false,
},
});