AGENTS.md
# Astro Framework Cheatsheet
**Version:** 2.0.0 | **Astro 5.x** | **Updated:** 2026-03-22 | **Author**: [webreactiva.com](https://webreactiva.com/ia)
---
## Quick Reference Card
| Topic | Rule of Thumb |
|-------|---------------|
| **Components** | Define `interface Props`; frontmatter = server only; use `class:list` for conditional classes |
| **Hydration** | Default: no directive (zero JS). Question every `client:` |
| **Server Islands** | `server:defer` for personalized/dynamic server content on cached pages |
| **Content Collections** | Content Layer API with loaders in `src/content.config.ts` (NOT `src/content/config.ts`) |
| **Rendering content** | `import { render } from 'astro:content'` then `const { Content } = await render(entry)` |
| **Routing** | File-based; `getStaticPaths()` required for dynamic routes in static mode |
| **Output modes** | `static` (default), `server` (all SSR), `hybrid` (static default, opt-in SSR) |
| **Sessions** | `Astro.session?.get/set`; always use optional chaining; SSR only |
| **Env vars** | `astro:env` schema with `envField` for type safety; import from `astro:env/client` or `astro:env/server` |
| **Images** | Import local images; use `<Image>` / `<Picture>` from `astro:assets`; always set alt text |
| **i18n** | Use `getRelativeLocaleUrl()` for links, never hardcode locale prefixes |
| **TypeScript** | Extend `astro/tsconfigs/strict`; define path aliases; type `App.Locals` in `src/env.d.ts` |
| **API routes** | Export named handlers (`GET`, `POST`, etc.) typed as `APIRoute` |
---
## Decision Trees
### Output Mode
```
All static? ──> output: 'static' (default, no adapter)
All dynamic? ──> output: 'server' + adapter
Mix? ──> output: 'hybrid' + adapter
Opt into SSR (hybrid): export const prerender = false
Opt out of SSR (server): export const prerender = true
```
### Hydration Directive
```
Needs interactivity?
├─ No ──> No directive (zero JS)
└─ Yes ──> Above fold + critical? ──> client:load
Above fold + non-critical? ──> client:idle
Below fold? ──> client:visible
Device-specific? ──> client:media="(query)"
Browser APIs only, skip SSR? ──> client:only="react"
```
### Server Island vs Client Island
```
Needs server data (cookies, DB, personalization)? ──> server:defer + slot="fallback"
Needs browser interactivity? ──> client:* directive
Neither? ──> Static .astro component
```
---
## Critical "NEVER Do" List
- **NEVER** access `window`/`document` in `.astro` frontmatter (server-only)
- **NEVER** use `client:load` on everything — defeats islands architecture
- **NEVER** use `client:only` without the framework string argument
- **NEVER** use plain `z.date()` for frontmatter dates — use `z.coerce.date()`
- **NEVER** use string paths for local images (`src="/images/hero.jpg"`) — import them
- **NEVER** access `Astro.request`/`Astro.cookies`/`Astro.session` in prerendered pages
- **NEVER** rely on `Astro.url` inside a server island (returns internal route; use `Referer` header)
- **NEVER** pass large objects/functions as server island props (IDs only; >2048 bytes forces POST)
- **NEVER** use sessions in edge middleware or static pages
- **NEVER** forget `getStaticPaths()` for `[param]` routes in static output
- **NEVER** skip `slot="fallback"` on `server:defer` components
- **NEVER** use `any` — use `unknown` and narrow
- **NEVER** import Zod from `zod` — use `astro/zod` (Astro 5+)
---
## Astro 5+ Migration Notes (Post-Training Knowledge)
### Content Collections (Content Layer API)
- Config file: `src/content.config.ts` (was `src/content/config.ts`)
- Uses `loader` property instead of `type: 'content'` / `type: 'data'`
- Loaders: `import { glob, file } from 'astro/loaders'`
- Zod: `import { z } from 'astro/zod'` (NOT from `zod` package)
- Rendering: `import { render } from 'astro:content'` then `await render(entry)` (was `entry.render()`)
- `reference()` for cross-collection relationships; `image()` schema helper for optimized images
### server:defer (Stable in Astro 5)
- Requires adapter; props must be serializable and small
- Use `Referer` header inside island to get parent page URL
### Sessions (Astro 5.7+)
- `Astro.session?.get(key)` / `.set(key, value)` — always optional-chain
- Configure `session.driver` in config via `sessionDrivers` from `astro/config`
- `session.regenerate()` after login, `session.destroy()` on logout
- Type via `App.SessionData` in `src/env.d.ts`
### astro:env (Stable in Astro 5)
- Schema in `astro.config.mjs` with `envField` from `astro/config`
- Import from `astro:env/client` or `astro:env/server`
- Three kinds: public client (bundled), public server (server bundle), secret server (not bundled)
- No secret client vars — not supported by design
### Actions
- `defineAction` from `astro:actions` with `astro/zod` schemas
## Key Patterns (Brief)
- **Component structure:** imports, `interface Props`, destructure with defaults, logic, template, scoped `<style>`
- **Slots:** `Astro.slots.has('name')` to conditionally render named slot wrappers
- **Middleware:** `defineMiddleware` from `astro:middleware`; chain with `sequence(a, b)`; file: `src/middleware.ts`
- **Pagination:** `getStaticPaths({ paginate })` returns `page.data`, `page.url.prev/next`
- **Redirects:** `redirects: { '/old': '/new' }` in `astro.config.mjs`
- **Cookies (SSR):** `Astro.cookies.get('name')?.value` / `.set('name', value, options)`
## References
Deep-dive docs with full code examples live in `references/`:
`actions.md` `client-directives.md` `components.md` `configuration.md` `content-collections.md` `environment-variables.md` `i18n-routing.md` `images.md` `middleware.md` `routing.md` `server-islands.md` `sessions.md` `ssr-adapters.md` `styling.md` `view-transitions.md`
references/actions.md
# Actions
Actions provide type-safe form handling and server functions in Astro.
## Setup
Actions are defined in `src/actions/index.ts`:
```typescript
// src/actions/index.ts
import { defineAction, z } from 'astro:actions';
export const server = {
// Actions go here
};
```
## Defining Actions
### Basic Action
```typescript
// src/actions/index.ts
import { defineAction, z } from 'astro:actions';
export const server = {
subscribe: defineAction({
input: z.object({
email: z.string().email(),
name: z.string().min(2),
}),
handler: async ({ email, name }) => {
// Save to database, send email, etc.
await db.subscribers.create({ email, name });
return { success: true, message: 'Subscribed!' };
},
}),
};
```
### Action with Accept Header
```typescript
export const server = {
// Accepts form data
submitForm: defineAction({
accept: 'form', // Parses FormData
input: z.object({
email: z.string().email(),
message: z.string(),
}),
handler: async ({ email, message }) => {
await sendEmail(email, message);
return { sent: true };
},
}),
// Accepts JSON (default)
createPost: defineAction({
accept: 'json', // Default
input: z.object({
title: z.string(),
content: z.string(),
}),
handler: async ({ title, content }) => {
const post = await db.posts.create({ title, content });
return post;
},
}),
};
```
### Action Without Input
```typescript
export const server = {
getCurrentUser: defineAction({
handler: async (_, context) => {
const user = context.locals.user;
return user || null;
},
}),
};
```
## Using Actions
### In Forms (Progressive Enhancement)
```astro
---
import { actions } from 'astro:actions';
---
<form method="POST" action={actions.subscribe}>
<input type="email" name="email" required />
<input type="text" name="name" required />
<button type="submit">Subscribe</button>
</form>
```
### With JavaScript
```astro
---
import { actions } from 'astro:actions';
---
<form id="subscribe-form">
<input type="email" name="email" required />
<input type="text" name="name" required />
<button type="submit">Subscribe</button>
</form>
<script>
import { actions } from 'astro:actions';
const form = document.getElementById('subscribe-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const { data, error } = await actions.subscribe({
email: formData.get('email'),
name: formData.get('name'),
});
if (error) {
console.error(error.message);
return;
}
console.log('Success:', data.message);
});
</script>
```
### Server-Side Calls
```astro
---
import { actions } from 'astro:actions';
// Call action from server
const { data, error } = await actions.subscribe({
email: 'user@example.com',
name: 'John Doe',
});
---
```
## Handling Results
### getActionResult
Get the result of a form submission:
```astro
---
import { actions, getActionResult } from 'astro:actions';
const result = await getActionResult(actions.subscribe);
if (result?.error) {
// Handle validation errors
}
---
{result?.data && (
<p class="success">{result.data.message}</p>
)}
{result?.error && (
<p class="error">{result.error.message}</p>
)}
<form method="POST" action={actions.subscribe}>
<input type="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
```
### Error Handling
```typescript
import { defineAction, z, ActionError } from 'astro:actions';
export const server = {
createUser: defineAction({
input: z.object({
email: z.string().email(),
}),
handler: async ({ email }) => {
const existing = await db.users.findByEmail(email);
if (existing) {
throw new ActionError({
code: 'CONFLICT',
message: 'User already exists',
});
}
return await db.users.create({ email });
},
}),
};
```
### Error Codes
Available error codes:
- `BAD_REQUEST` - Invalid input
- `UNAUTHORIZED` - Authentication required
- `FORBIDDEN` - Permission denied
- `NOT_FOUND` - Resource not found
- `CONFLICT` - Resource conflict
- `PRECONDITION_FAILED` - Condition not met
- `INTERNAL_SERVER_ERROR` - Server error
```astro
---
const result = await getActionResult(actions.createUser);
if (result?.error?.code === 'CONFLICT') {
// Handle duplicate user
}
---
```
## Input Validation
### Zod Schema Validation
```typescript
import { defineAction, z } from 'astro:actions';
export const server = {
updateProfile: defineAction({
input: z.object({
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be at most 20 characters')
.regex(/^[a-z0-9_]+$/, 'Only lowercase letters, numbers, and underscores'),
bio: z.string().max(500).optional(),
birthdate: z.coerce.date()
.min(new Date('1900-01-01'))
.max(new Date()),
tags: z.array(z.string()).max(5).default([]),
}),
handler: async (data) => {
return await db.profiles.update(data);
},
}),
};
```
### Accessing Validation Errors
```astro
---
const result = await getActionResult(actions.updateProfile);
const fieldErrors = result?.error?.fields;
// { username: ['Too short'], bio: ['Too long'] }
---
<form method="POST" action={actions.updateProfile}>
<input type="text" name="username" />
{fieldErrors?.username && (
<span class="error">{fieldErrors.username[0]}</span>
)}
</form>
```
## Context Access
```typescript
import { defineAction, z } from 'astro:actions';
export const server = {
protectedAction: defineAction({
input: z.object({ data: z.string() }),
handler: async ({ data }, context) => {
// Access request
const ip = context.request.headers.get('x-forwarded-for');
// Access cookies
const token = context.cookies.get('session')?.value;
// Access locals (from middleware)
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: 'UNAUTHORIZED',
message: 'Must be logged in',
});
}
return { processed: true };
},
}),
};
```
## File Uploads
```typescript
export const server = {
uploadImage: defineAction({
accept: 'form',
input: z.object({
image: z.instanceof(File),
description: z.string().optional(),
}),
handler: async ({ image, description }) => {
const buffer = await image.arrayBuffer();
const path = await saveFile(buffer, image.name);
return { url: path };
},
}),
};
```
```astro
<form method="POST" action={actions.uploadImage} enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" />
<input type="text" name="description" />
<button type="submit">Upload</button>
</form>
```
## Redirect After Action
```typescript
import { defineAction, z, ActionError } from 'astro:actions';
export const server = {
login: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
password: z.string(),
}),
handler: async ({ email, password }, context) => {
const user = await authenticate(email, password);
if (!user) {
throw new ActionError({
code: 'UNAUTHORIZED',
message: 'Invalid credentials',
});
}
context.cookies.set('session', user.token, { httpOnly: true });
// Return redirect URL for client to handle
return { redirect: '/dashboard' };
},
}),
};
```
```astro
---
const result = await getActionResult(actions.login);
if (result?.data?.redirect) {
return Astro.redirect(result.data.redirect);
}
---
```
## Best Practices
1. **Use Zod for validation** - Type-safe input handling
2. **Throw ActionError for business logic errors** - Proper error codes
3. **Access context for auth** - Use `context.locals` from middleware
4. **Use `accept: 'form'`** - For form submissions without JS
5. **Handle errors gracefully** - Show user-friendly messages
6. **Use getActionResult** - For progressive enhancement
7. **Keep handlers focused** - Single responsibility
references/client-directives.md
# Client Directives
Client directives control how UI framework components (React, Vue, Svelte, etc.) are hydrated on the client.
## Overview
By default, UI framework components are **not hydrated** - they render to static HTML. Client directives tell Astro when and how to hydrate them.
```astro
<!-- Static - no JavaScript shipped -->
<ReactComponent />
<!-- Hydrated - JavaScript loaded -->
<ReactComponent client:load />
```
## Available Directives
### client:load
Hydrate immediately when the page loads. Use for above-the-fold interactive content.
```astro
<InteractiveCounter client:load />
<NavigationMenu client:load />
```
**Use when:**
- Component is immediately visible
- User interaction expected right away
- Critical interactive functionality
### client:idle
Hydrate when the browser is idle (using `requestIdleCallback`). Good for lower-priority interactivity.
```astro
<SidebarWidget client:idle />
<FeedbackForm client:idle />
```
**Use when:**
- Component is visible but not immediately needed
- User likely to interact after initial page load
- Want to prioritize above-the-fold content
### client:visible
Hydrate when the component enters the viewport. Uses Intersection Observer.
```astro
<CommentsSection client:visible />
<ImageCarousel client:visible />
<FooterNewsletter client:visible />
```
**Use when:**
- Component is below the fold
- Heavy components that shouldn't block initial load
- Lazy-loaded features
**With options:**
```astro
<!-- Hydrate when 50% visible with 200px margin -->
<HeavyComponent client:visible={{
rootMargin: "200px",
threshold: 0.5
}} />
```
### client:media
Hydrate only when a media query matches. Perfect for responsive components.
```astro
<!-- Only hydrate on desktop -->
<DesktopOnlyChart client:media="(min-width: 768px)" />
<!-- Only hydrate on mobile -->
<MobileMenu client:media="(max-width: 767px)" />
<!-- Only hydrate if reduced motion not preferred -->
<AnimatedHero client:media="(prefers-reduced-motion: no-preference)" />
```
**Use when:**
- Component only needed at certain viewport sizes
- Device-specific functionality
- Accessibility considerations
### client:only
Skip server rendering entirely. Component only renders on the client.
```astro
<!-- Must specify the framework -->
<ReactOnlyComponent client:only="react" />
<VueOnlyComponent client:only="vue" />
<SvelteOnlyComponent client:only="svelte" />
<SolidOnlyComponent client:only="solid-js" />
<PreactOnlyComponent client:only="preact" />
```
**Use when:**
- Component uses browser-only APIs (window, document, localStorage)
- Component has SSR incompatibilities
- Third-party component doesn't support SSR
**Important:** Always specify the framework name!
## Mixing Multiple Frameworks
Astro supports multiple UI frameworks in the same project:
```astro
---
import ReactCounter from './ReactCounter.jsx';
import VueCard from './VueCard.vue';
import SvelteButton from './SvelteButton.svelte';
---
<ReactCounter client:load />
<VueCard client:visible />
<SvelteButton client:idle />
```
## Directive Priority Guide
Choose the right directive based on priority:
| Priority | Directive | JavaScript Load | Use Case |
|----------|-----------|-----------------|----------|
| 1 (Highest) | `client:load` | Immediate | Critical interactivity |
| 2 | `client:idle` | When idle | Important but not urgent |
| 3 | `client:visible` | When visible | Below-the-fold content |
| 4 | `client:media` | When matches | Responsive components |
| Special | `client:only` | Client only | Browser-only APIs |
## Common Patterns
### Navigation with Mobile Menu
```astro
---
import DesktopNav from './DesktopNav.astro'; // Static
import MobileMenu from './MobileMenu.jsx';
---
<DesktopNav />
<MobileMenu client:media="(max-width: 768px)" />
```
### Progressive Enhancement
```astro
---
import StaticContent from './StaticContent.astro';
import EnhancedFeatures from './EnhancedFeatures.jsx';
---
<!-- Always visible -->
<StaticContent />
<!-- Enhanced when visible -->
<EnhancedFeatures client:visible />
```
### Heavy Component Optimization
```astro
---
import DataVisualization from './DataVisualization.jsx';
---
<!-- Only load when visible and browser is ready -->
<div class="chart-container">
<DataVisualization
client:visible={{ rootMargin: "100px" }}
data={chartData}
/>
</div>
```
## Expert Gotchas
### `client:visible` on Above-the-Fold Components
`client:visible` uses IntersectionObserver to detect viewport entry. If the component is **already visible** on load (hero, header, above-fold CTA), the observer fires immediately anyway — you pay the observer setup cost for zero benefit. Use `client:load` directly.
### `client:idle` on Low-End Mobile
`requestIdleCallback` on low-RAM Android devices can delay **10+ seconds** because the main thread never goes truly idle. If users might interact with the component within the first 5 seconds, use `client:load` instead. Reserve `client:idle` for genuinely non-urgent components (analytics widgets, secondary sidebars).
### The Navbar Hydration Trap
A common mistake: wrapping an entire React/Vue navbar in `client:load` just for a mobile hamburger toggle. The toggle is 5 lines of vanilla JS, but you're shipping an entire framework runtime. Instead:
```astro
<nav>
<ul class="nav-links">{/* static links */}</ul>
<button id="menu-toggle" aria-expanded="false">Menu</button>
</nav>
<script>
const toggle = document.getElementById('menu-toggle');
toggle?.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
document.querySelector('.nav-links')?.classList.toggle('open');
});
</script>
```
**Rule of thumb:** If the only interactivity is show/hide or toggle, use a `<script>` tag. If there's state management, forms, or complex UI logic, use a framework component with a client directive.
### Large Bundle Splitting
If a React component's bundle exceeds ~50KB, consider splitting:
1. Render the **static shell** as an Astro component (heading, layout, placeholder)
2. Hydrate only the **interactive part** as a smaller React island
```astro
<!-- Instead of one large hydrated component -->
<ProductPage client:load />
<!-- Split into static + interactive -->
<div class="product-layout">
<h1>{product.title}</h1>
<img src={product.image} alt={product.title} />
<p>{product.description}</p>
<!-- Only the interactive part hydrates -->
<AddToCartButton client:load productId={product.id} />
</div>
```
### When `client:only` Is Actually Needed
`client:only` skips SSR entirely — no HTML on first paint. This hurts SEO and perceived performance. Use it **only** when:
- The component crashes during SSR (e.g., reads `window.innerWidth` at module scope)
- A third-party library has no SSR support and no workaround
- The component is purely decorative and non-essential (e.g., a confetti animation)
If the component just needs `window` in an event handler, `client:load` works fine — `window` is available after hydration.
## Best Practices
1. **Default to no directive** - Only hydrate what needs interactivity
2. **Use `client:visible` for below-fold content** - Reduce initial bundle
3. **Avoid `client:load` for everything** - Defeats the purpose of islands
4. **Use `client:media` for responsive components** - Don't load unused code
5. **Always specify framework for `client:only`** - Required parameter
6. **Test without JavaScript** - Ensure graceful degradation
7. **Monitor bundle sizes** - Each hydrated component adds JavaScript
## Debugging
Check which components are hydrated:
```javascript
// In browser console
document.querySelectorAll('[data-astro-cid]').forEach(el => {
console.log(el, el.dataset);
});
```
## Performance Impact
| Directive | Initial Load | TTI Impact | Bundle Size |
|-----------|--------------|------------|-------------|
| None | Fastest | None | 0 KB |
| `client:visible` | Fast | Low | Deferred |
| `client:idle` | Fast | Low | Deferred |
| `client:media` | Fast | Conditional | Conditional |
| `client:load` | Slower | High | Immediate |
| `client:only` | Slowest | High | Immediate + no SSR |
references/components.md
# Astro Components
## Component Structure
Astro components use a `.astro` extension and consist of two main parts:
```astro
---
// Component Script (Frontmatter)
// Runs on the server at build time (or request time for SSR)
import SomeComponent from './SomeComponent.astro';
import { getCollection } from 'astro:content';
interface Props {
title: string;
description?: string;
}
const { title, description = 'Default description' } = Astro.props;
// Top-level await is supported
const posts = await getCollection('blog');
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<!-- Component Template -->
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<p>{description}</p>
<SomeComponent />
</body>
</html>
```
## Props
### Defining Props with TypeScript
```astro
---
interface Props {
name: string;
greeting?: string;
items: string[];
}
const { name, greeting = "Hello", items } = Astro.props;
---
<h1>{greeting}, {name}!</h1>
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
```
### Accessing All Props
```astro
---
const allProps = Astro.props;
// Spread to child component
---
<ChildComponent {...allProps} />
```
## Slots
### Default Slot
```astro
<!-- Wrapper.astro -->
---
---
<div class="wrapper">
<slot /> <!-- Children go here -->
</div>
```
```astro
<!-- Usage -->
<Wrapper>
<p>This content goes into the slot</p>
</Wrapper>
```
### Named Slots
```astro
<!-- Layout.astro -->
---
---
<div class="container">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- Default slot -->
</main>
<footer>
<slot name="footer" />
</footer>
</div>
```
```astro
<!-- Usage -->
<Layout>
<h1 slot="header">Page Title</h1>
<p>Main content goes in default slot</p>
<p slot="footer">Footer content</p>
</Layout>
```
### Fallback Content
```astro
<slot>
<p>This shows if no content is provided</p>
</slot>
```
### Checking for Slot Content
```astro
---
const hasHeader = Astro.slots.has('header');
---
{hasHeader && (
<header>
<slot name="header" />
</header>
)}
```
### Rendering Slots Programmatically
```astro
---
const html = await Astro.slots.render('default');
---
<Fragment set:html={html} />
```
## Expressions and Dynamic Content
### Basic Expressions
```astro
---
const name = "Astro";
const items = ['Apple', 'Banana', 'Cherry'];
const visible = true;
---
<h1>{name}</h1>
<p>{5 + 5}</p>
<p>{visible ? 'Shown' : 'Hidden'}</p>
<!-- Lists -->
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
<!-- Conditional rendering -->
{visible && <p>This is visible</p>}
<!-- Dynamic HTML -->
<div set:html={rawHtmlString} />
```
### Dynamic Attributes
```astro
---
const dynamicId = "my-id";
const dynamicClass = "active";
---
<div id={dynamicId} class={dynamicClass}>Content</div>
<!-- Boolean attributes -->
<input type="checkbox" checked={true} />
<button disabled={false}>Click</button>
<!-- Spreading attributes -->
<div {...Astro.props}>Content</div>
```
## class:list Directive
```astro
---
const isActive = true;
const isDisabled = false;
---
<div class:list={[
'base-class',
{ 'active': isActive },
{ 'disabled': isDisabled },
isActive && 'conditional-class',
]}>
Content
</div>
<!-- Output: <div class="base-class active conditional-class">Content</div> -->
```
## HTML Attributes
### set:html
Inject raw HTML (be careful with user input - XSS risk):
```astro
---
const rawHTML = "<strong>Bold</strong>";
---
<div set:html={rawHTML} />
```
### set:text
Safely set text content:
```astro
---
const text = "Some text content";
---
<p set:text={text} />
```
## Fragment
Wrap multiple elements without adding extra DOM:
```astro
---
import { Fragment } from 'astro:components';
---
<Fragment>
<li>Item 1</li>
<li>Item 2</li>
</Fragment>
<!-- Or using shorthand -->
<>
<li>Item 1</li>
<li>Item 2</li>
</>
```
## Component Scripts
### Imports
```astro
---
// Astro components
import Header from '../components/Header.astro';
// UI framework components
import ReactComponent from '../components/ReactComponent.jsx';
// Data
import { getCollection } from 'astro:content';
// Utilities
import { formatDate } from '../utils/date';
// Styles
import '../styles/global.css';
// JSON data
import data from '../data/config.json';
---
```
### Using Astro Global
```astro
---
// URL information
const currentPath = Astro.url.pathname;
const searchParams = Astro.url.searchParams;
// Request (SSR only)
const userAgent = Astro.request.headers.get('user-agent');
// Cookies (SSR only)
const token = Astro.cookies.get('token');
// Redirect (SSR only)
if (!token) {
return Astro.redirect('/login');
}
// Props
const { title } = Astro.props;
// Slots
const hasContent = Astro.slots.has('default');
// Site configuration
const siteUrl = Astro.site;
// Generator
const generator = Astro.generator; // "Astro v4.x.x"
// Current locale (i18n)
const locale = Astro.currentLocale;
---
```
## Best Practices
1. **Keep frontmatter focused** - Only include necessary logic
2. **Use TypeScript interfaces** - Define Props for type safety
3. **Prefer slots over props for complex content** - Better composability
4. **Avoid side effects in frontmatter** - It runs on every render
5. **Use `class:list` for conditional classes** - Cleaner than ternaries
6. **Extract reusable logic to utilities** - Keep components clean
references/configuration.md
# Configuration
Astro configuration lives in `astro.config.mjs` at the project root.
## Basic Configuration
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
// Your configuration options here
});
```
## Common Options
### Site URL
```javascript
export default defineConfig({
site: 'https://example.com',
base: '/blog', // For subdirectory deployments
});
```
### Output Mode
```javascript
import node from '@astrojs/node';
export default defineConfig({
output: 'static', // Default - all pages prerendered
// output: 'server', // All pages server-rendered
// output: 'hybrid', // Static by default, opt-in to SSR
adapter: node({ // Required for server/hybrid
mode: 'standalone',
}),
});
```
### Integrations
```javascript
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
integrations: [
react(),
tailwind(),
mdx(),
sitemap(),
],
});
```
### Build Options
```javascript
export default defineConfig({
build: {
format: 'directory', // /about/index.html (default)
// format: 'file', // /about.html
inlineStylesheets: 'auto', // Inline small stylesheets
// inlineStylesheets: 'always',
// inlineStylesheets: 'never',
assets: '_astro', // Assets directory name
},
compressHTML: true, // Minify HTML output
outDir: './dist', // Build output directory
publicDir: './public', // Static assets directory
});
```
### Dev Server
```javascript
export default defineConfig({
server: {
port: 4321, // Default port
host: true, // Expose to network
open: true, // Open browser on start
},
devToolbar: {
enabled: true, // Show dev toolbar
},
});
```
### Prefetching
```javascript
export default defineConfig({
prefetch: {
prefetchAll: true, // Prefetch all links
defaultStrategy: 'viewport', // 'hover' | 'viewport' | 'load'
},
});
```
## Vite Configuration
```javascript
export default defineConfig({
vite: {
plugins: [],
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
},
});
```
## Markdown Configuration
```javascript
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
export default defineConfig({
markdown: {
syntaxHighlight: 'shiki', // 'shiki' | 'prism' | false
shikiConfig: {
theme: 'dracula',
wrap: true,
},
remarkPlugins: [
remarkToc,
[remarkPlugin, { option: true }], // With options
],
rehypePlugins: [
rehypeSlug,
],
gfm: true, // GitHub Flavored Markdown
smartypants: true, // Smart quotes
},
});
```
## i18n Configuration
```javascript
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', 'de'],
routing: {
prefixDefaultLocale: false, // /about vs /en/about
redirectToDefaultLocale: true,
},
fallback: {
es: 'en', // Fallback to English for Spanish
},
},
});
```
## Image Configuration
```javascript
export default defineConfig({
image: {
// Allowed remote image domains
domains: ['example.com', 'cdn.example.com'],
// Allowed remote patterns
remotePatterns: [
{
protocol: 'https',
hostname: '**.amazonaws.com',
},
],
// Image service configuration
service: {
entrypoint: 'astro/assets/services/sharp',
config: {
limitInputPixels: false,
},
},
},
});
```
## Redirects
```javascript
export default defineConfig({
redirects: {
'/old-page': '/new-page',
'/old-blog/[...slug]': '/blog/[...slug]',
'/twitter': {
status: 302,
destination: 'https://twitter.com/astrodotbuild',
},
},
});
```
## TypeScript Configuration
### tsconfig.json
```json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"]
}
}
}
```
Available presets:
- `astro/tsconfigs/base` - Minimal
- `astro/tsconfigs/strict` - Recommended
- `astro/tsconfigs/strictest` - Maximum strictness
### Type Declarations
```typescript
// src/env.d.ts
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly DATABASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare namespace App {
interface Locals {
user: {
id: string;
name: string;
} | null;
}
}
```
## Environment Variables
### .env Files
```bash
# .env
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=abc123
```
### Access in Code
```astro
---
// Server-side (all variables)
const dbUrl = import.meta.env.DATABASE_URL;
// Client-side (only PUBLIC_ prefixed)
const apiUrl = import.meta.env.PUBLIC_API_URL;
// Built-in variables
const mode = import.meta.env.MODE; // 'development' | 'production'
const prod = import.meta.env.PROD; // boolean
const dev = import.meta.env.DEV; // boolean
const site = import.meta.env.SITE; // From astro.config site
const base = import.meta.env.BASE_URL; // From astro.config base
---
```
### Type-Safe Environment (Astro 5+)
```javascript
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
PUBLIC_API_URL: envField.string({
context: 'client',
access: 'public',
default: 'https://api.example.com',
}),
DATABASE_URL: envField.string({
context: 'server',
access: 'secret',
}),
PORT: envField.number({
context: 'server',
access: 'public',
default: 4321,
}),
FEATURE_FLAG: envField.boolean({
context: 'client',
access: 'public',
default: false,
}),
},
},
});
```
```astro
---
import { PUBLIC_API_URL, DATABASE_URL } from 'astro:env/server';
import { PUBLIC_API_URL } from 'astro:env/client';
---
```
## Session Configuration (Astro 5.7+)
```javascript
import { defineConfig, sessionDrivers } from 'astro/config';
export default defineConfig({
session: {
driver: sessionDrivers.redis({
url: process.env.REDIS_URL,
}),
// Or use filesystem, memory, etc.
},
});
```
## Experimental Features
```javascript
export default defineConfig({
experimental: {
contentIntellisense: true,
clientPrerender: true,
},
});
```
> **Note:** Server islands (`server:defer`) are stable since Astro 5 — no experimental flag needed.
## Full Example
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import vercel from '@astrojs/vercel';
export default defineConfig({
site: 'https://mysite.com',
output: 'hybrid',
adapter: vercel(),
integrations: [
react(),
tailwind({
applyBaseStyles: false,
}),
mdx(),
sitemap(),
],
prefetch: {
prefetchAll: true,
},
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
image: {
domains: ['images.unsplash.com'],
},
markdown: {
syntaxHighlight: 'shiki',
shikiConfig: {
theme: 'github-dark',
},
},
vite: {
resolve: {
alias: {
'@': '/src',
},
},
},
});
```
## Best Practices
1. **Start with strict TypeScript** - Better type safety
2. **Use path aliases** - Cleaner imports
3. **Configure image domains** - Security whitelist
4. **Set site URL** - Required for sitemaps and canonical URLs
5. **Use hybrid output** - Best of both worlds
6. **Enable prefetching** - Faster navigation
7. **Configure Markdown plugins** - Enhanced content
8. **Type your env variables** - Catch errors early
references/content-collections.md
# Content Collections
Content collections provide type-safe content management with schema validation using Zod.
## Setup (Astro 5+ Content Layer API)
### Directory Structure
```
src/
├── content.config.ts # Collection schemas (Astro 5+)
├── content/
│ ├── blog/ # Blog collection
│ │ ├── post-1.md
│ │ ├── post-2.mdx
│ │ └── drafts/ # Subdirectories supported
│ │ └── draft-1.md
│ └── authors/ # Another collection
│ └── john.json
```
> **Note:** In Astro 5+, the config file is `src/content.config.ts` (not `src/content/config.ts`). Collections use `loader` instead of `type`.
### Configuration File (Astro 5+ — Recommended)
```typescript
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const blogCollection = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
description: z.string(),
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(),
}),
});
const authorsCollection = defineCollection({
loader: file('src/data/authors.json'),
schema: z.object({
name: z.string(),
email: z.string().email(),
bio: z.string(),
avatar: z.string().url(),
social: z.object({
twitter: z.string().optional(),
github: z.string().optional(),
}).optional(),
}),
});
export const collections = {
blog: blogCollection,
authors: authorsCollection,
};
```
### Legacy Configuration (Astro 4)
```typescript
// src/content/config.ts (legacy path)
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content', // Markdown/MDX files
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
}),
});
const authorsCollection = defineCollection({
type: 'data', // JSON/YAML files
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
});
export const collections = { blog: blogCollection, authors: authorsCollection };
```
## Built-in Loaders (Astro 5+)
### `glob()` — Multiple files
Loads entries from directories of Markdown, MDX, JSON, YAML, or TOML files:
```typescript
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({
base: './src/content/blog',
pattern: '**/*.{md,mdx}',
}),
schema: z.object({ title: z.string() }),
});
```
Options: `pattern`, `base`, `generateId()` (custom ID generation), `retainBody` (set `false` to exclude raw body).
### `file()` — Single file
Loads entries from a single JSON, YAML, or TOML file:
```typescript
import { file } from 'astro/loaders';
const authors = defineCollection({
loader: file('src/data/authors.json'),
schema: z.object({ name: z.string() }),
});
```
Supports a custom `parser` for non-standard formats (e.g., CSV).
### Custom Loaders
Load from any source (APIs, databases, CMSes):
```typescript
const products = defineCollection({
loader: async () => {
const response = await fetch('https://api.example.com/products');
const data = await response.json();
return data.map((product: any) => ({
id: product.id,
...product,
}));
},
schema: z.object({ name: z.string(), price: z.number() }),
});
```
### Object Loaders (Advanced)
For full control with incremental updates, caching, and file watching:
```typescript
import type { Loader } from 'astro/loaders';
function myLoader(options: { url: string }): Loader {
return {
name: 'my-loader',
load: async ({ store, meta, logger }) => {
const lastModified = meta.get('lastModified');
const data = await fetchData(options.url, lastModified);
store.clear();
for (const item of data) {
store.set({ id: item.id, data: item });
}
meta.set('lastModified', new Date().toISOString());
},
};
}
```
## Live Loaders (Astro 6+)
Live loaders fetch data fresh on every request — no data store to update. Use for real-time data:
```typescript
import type { LiveLoader } from 'astro/loaders';
function productLoader(config: { apiKey: string }): LiveLoader<Product> {
return {
name: 'product-loader',
loadCollection: async ({ filter }) => {
const data = await fetchProducts(config.apiKey, filter);
return {
entries: data.map(p => ({ id: p.sku, data: p })),
};
},
loadEntry: async ({ filter }) => {
const product = await fetchProduct(config.apiKey, filter.id);
if (!product) return undefined;
return { id: product.sku, data: product };
},
};
}
```
Query live collections with `getLiveCollection()` and `getLiveEntry()`:
```astro
---
import { getLiveCollection, getLiveEntry } from 'astro:content';
const { entries, error } = await getLiveCollection('products');
const { entry, error: entryError } = await getLiveEntry('products', 'sku-123');
---
```
## Collection Types (Legacy — Astro 4)
### Content Collections (`type: 'content'`)
For Markdown and MDX files with frontmatter:
```markdown
---
title: "My First Post"
pubDate: 2024-01-15
author: "john"
---
# Hello World
This is my first blog post.
```
### Data Collections (`type: 'data'`)
For JSON or YAML data files:
```json
// src/content/authors/john.json
{
"name": "John Doe",
"email": "john@example.com",
"bio": "A passionate writer",
"avatar": "https://example.com/avatar.jpg"
}
```
## Schema Validation with Zod
### Common Field Types
```typescript
import { z } from 'astro/zod'; // Astro 5+
// import { z } from 'astro:content'; // Legacy (Astro 4)
const schema = z.object({
// Strings
title: z.string(),
slug: z.string().regex(/^[a-z0-9-]+$/),
// Numbers
order: z.number().int().positive(),
rating: z.number().min(0).max(5),
// Booleans
featured: z.boolean().default(false),
// Dates
pubDate: z.coerce.date(), // Converts strings to Date
// Arrays
tags: z.array(z.string()),
categories: z.array(z.enum(['tech', 'life', 'travel'])),
// Objects
author: z.object({
name: z.string(),
email: z.string().email(),
}),
// Enums
status: z.enum(['draft', 'published', 'archived']),
// Optional fields
description: z.string().optional(),
image: z.string().url().optional(),
// Default values
views: z.number().default(0),
// Unions
media: z.union([
z.object({ type: z.literal('image'), src: z.string() }),
z.object({ type: z.literal('video'), url: z.string() }),
]),
});
```
### Image Schema
```typescript
// Astro 5+ with loader
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
schema: ({ image }) => z.object({
title: z.string(),
cover: image(), // Validates and optimizes images
coverAlt: z.string(),
}),
});
```
### Reference Other Collections
```typescript
import { defineCollection, reference } from 'astro:content';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
author: reference('authors'), // References authors collection
relatedPosts: z.array(reference('blog')).optional(),
}),
});
```
## Querying Collections
### getCollection()
```astro
---
import { getCollection } from 'astro:content';
// Get all entries
const allPosts = await getCollection('blog');
// Filter entries
const publishedPosts = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
// Filter by date
const recentPosts = await getCollection('blog', ({ data }) => {
return data.pubDate > new Date('2024-01-01');
});
// Sort entries
const sortedPosts = (await getCollection('blog'))
.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>
<time>{post.data.pubDate.toLocaleDateString()}</time>
</li>
))}
</ul>
```
### getEntry()
```astro
---
import { getEntry } from 'astro:content';
// Get single entry by collection and slug
const post = await getEntry('blog', 'my-first-post');
// Get single entry by reference
const author = await getEntry(post.data.author);
// Check if entry exists
if (!post) {
return Astro.redirect('/404');
}
---
<article>
<h1>{post.data.title}</h1>
<p>By {author.data.name}</p>
</article>
```
### getEntries()
```astro
---
import { getEntry, getEntries } from 'astro:content';
const post = await getEntry('blog', 'my-post');
// Get multiple referenced entries
const relatedPosts = await getEntries(post.data.relatedPosts);
---
```
## Rendering Content
### Astro 5+ (import `render` from `astro:content`)
```astro
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'my-post');
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<!-- Table of contents from headings -->
<nav>
{headings.map((h) => (
<a href={`#${h.slug}`} style={`margin-left: ${h.depth * 10}px`}>
{h.text}
</a>
))}
</nav>
<!-- Rendered content -->
<Content />
</article>
```
### Legacy (Astro 4)
```astro
---
const post = await getEntry('blog', 'my-post');
const { Content, headings } = await post.render();
---
```
## Dynamic Routes with Collections
```astro
---
// src/pages/blog/[...slug].astro
import { getCollection, render } 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 render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
```
> **Note:** In Astro 5+, use `post.id` instead of `post.slug` for routing params.
## Expert Guidance
### Loader Selection Decision Tree
```
Local markdown/MDX files → glob() loader
Single JSON/YAML data file → file() loader
Remote API/CMS data at build → Custom async loader function
Remote data fresh per-request → Live Loader (Astro 6+)
```
### Performance: `retainBody` for Large Sites
For sites with >1000 content entries where you only need frontmatter data (e.g., listing pages, tag indexes), disable body storage:
```typescript
const blog = defineCollection({
loader: glob({
base: './src/content/blog',
pattern: '**/*.md',
retainBody: false, // Significantly reduces data store size
}),
schema: z.object({ title: z.string(), pubDate: z.coerce.date() }),
});
```
Only use `retainBody: false` for collections where you don't call `render()`. If you need to render content on detail pages, keep the default (`true`).
### Migration Pitfalls (Astro 4 → 5)
| What changed | Old (Astro 4) | New (Astro 5+) |
|---|---|---|
| Config file | `src/content/config.ts` | `src/content.config.ts` |
| Collection type | `type: 'content'` / `type: 'data'` | `loader: glob(...)` / `loader: file(...)` |
| Zod import | `import { z } from 'astro:content'` | `import { z } from 'astro/zod'` |
| Rendering | `const { Content } = await entry.render()` | `import { render } from 'astro:content'; await render(entry)` |
| Route param | `post.slug` | `post.id` |
**The most common migration bug:** importing `z` from `astro:content` instead of `astro/zod`. This still compiles but can cause subtle type mismatches with the new Content Layer API.
### Schema Design Anti-Patterns
- **`z.date()` for frontmatter dates** → Always use `z.coerce.date()`. YAML/frontmatter dates arrive as strings; `z.date()` rejects them silently
- **Missing `.default()` on optional booleans** → `draft: z.boolean().optional()` means `undefined`, not `false`. Use `z.boolean().default(false)` so filtering logic works correctly
- **String paths for images** → Use the `image()` schema helper to enable Astro's image optimization pipeline. String URLs bypass optimization entirely
## Best Practices
1. **Always define schemas** - Type safety and validation
2. **Use `z.coerce.date()`** - Handles string dates from frontmatter
3. **Set sensible defaults** - `.default()` reduces required fields
4. **Filter drafts in production** - Don't expose unpublished content
5. **Use references for relationships** - Type-safe cross-collection links
6. **Keep slugs URL-friendly** - Validate with regex
7. **Use image schema for images** - Enables optimization
references/environment-variables.md
# Type-Safe Environment Variables (`astro:env`)
**Added in:** Astro 5.0+
The `astro:env` API provides a type-safe schema for environment variables with validation, proper context separation, and secret management.
## Setup
### Define Schema
```javascript
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
// Public client variable — available everywhere
API_URL: envField.string({
context: 'client',
access: 'public',
optional: true,
}),
// Public server variable — server bundle only
PORT: envField.number({
context: 'server',
access: 'public',
default: 4321,
}),
// Secret server variable — not in bundle, runtime only
API_SECRET: envField.string({
context: 'server',
access: 'secret',
}),
// Boolean variable
FEATURE_FLAG: envField.boolean({
context: 'client',
access: 'public',
default: false,
}),
// Enum variable
LOG_LEVEL: envField.enum({
context: 'server',
access: 'public',
values: ['debug', 'info', 'warn', 'error'],
default: 'info',
}),
},
},
});
```
### Use Variables
Import from the appropriate module:
```astro
---
import { API_URL } from 'astro:env/client';
import { API_SECRET, PORT } from 'astro:env/server';
const data = await fetch(`${API_URL}/users`, {
headers: {
'Authorization': `Bearer ${API_SECRET}`,
},
});
---
<script>
import { API_URL } from 'astro:env/client';
fetch(`${API_URL}/ping`);
</script>
```
## Variable Types
There are three kinds, determined by `context` + `access`:
| Kind | Context | Access | Available In | In Bundle? |
|------|---------|--------|-------------|-----------|
| Public client | `client` | `public` | Client + Server | Yes |
| Public server | `server` | `public` | Server only | Yes |
| Secret server | `server` | `secret` | Server only | No |
**Secret client variables are not supported** — there's no safe way to send secrets to the client.
## Data Types
```javascript
envField.string({ context: 'server', access: 'public' })
envField.number({ context: 'server', access: 'public' })
envField.boolean({ context: 'client', access: 'public' })
envField.enum({ context: 'server', access: 'public', values: ['a', 'b'] })
```
Common options: `default`, `optional`, `min`, `max`, `length`, `url`, `includes`, `startsWith`, `endsWith`.
## `getSecret()`
For programmatic access to secrets (e.g., keys that depend on dynamic data):
```typescript
import { getSecret } from 'astro:env/server';
// Returns string | undefined
const apiKey = getSecret('DYNAMIC_API_KEY');
```
Use `getSecret()` instead of `process.env` — its implementation is provided by your adapter, so you won't need to update calls if you switch adapters.
## When to Use `astro:env` vs `import.meta.env`
- **`astro:env`**: Type-safe, validated at build time, proper client/server separation. Use for all new projects.
- **`import.meta.env`**: Vite's built-in support. Still works, but no schema validation. `PUBLIC_` prefix exposes to client.
Both can coexist in the same project.
references/i18n-routing.md
# Internationalization (i18n) Routing
Astro's built-in i18n routing helps you build multilingual sites with URL-based locale management, fallback content, and helper functions.
## Setup
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', 'de'],
},
});
```
### Folder Structure
Create locale-specific folders inside `src/pages/`:
```
src/pages/
├── index.astro → / (English, default)
├── about.astro → /about
├── es/
│ ├── index.astro → /es
│ └── about.astro → /es/about
└── fr/
├── index.astro → /fr
└── about.astro → /fr/about
```
## Routing Options
### `prefixDefaultLocale`
By default, the default locale has no prefix. Set to `true` to add it:
```javascript
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
routing: {
prefixDefaultLocale: true, // /en/about instead of /about
},
}
```
### Fallback Languages
Serve content from another locale when a page doesn't exist:
```javascript
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
fallback: {
fr: 'es', // French pages fall back to Spanish
},
routing: {
fallbackType: 'rewrite', // Show fallback content at the original URL
// fallbackType: 'redirect', // (default) Redirect to fallback locale URL
},
}
```
### Manual Routing
For full control, disable Astro's i18n middleware:
```javascript
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
routing: 'manual',
}
```
Then implement your own middleware using helpers from `astro:i18n`:
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { redirectToDefaultLocale } from 'astro:i18n';
export const onRequest = defineMiddleware(async (ctx, next) => {
if (ctx.url.startsWith('/about')) {
return next();
}
return redirectToDefaultLocale(302);
});
```
### Custom Locale Paths
Map multiple language codes to a single URL path:
```javascript
i18n: {
locales: ['es', 'en', {
path: 'french',
codes: ['fr', 'fr-BR', 'fr-CA'],
}],
defaultLocale: 'en',
}
```
This maps `fr`, `fr-BR`, and `fr-CA` to `/french/` URLs.
### Domain-Based Routing
For server-rendered sites, map locales to different domains:
```javascript
i18n: {
locales: ['es', 'en', 'fr'],
defaultLocale: 'en',
domains: {
fr: 'https://fr.example.com',
es: 'https://example.es',
},
}
```
Requires `output: 'server'` and a `site` configuration.
## Helper Functions
Import from `astro:i18n`:
```astro
---
import {
getRelativeLocaleUrl,
getAbsoluteLocaleUrl,
getRelativeLocaleUrlList,
getAbsoluteLocaleUrlList,
getPathByLocale,
getLocaleByPath,
} from 'astro:i18n';
// Generate localized URLs
const aboutES = getRelativeLocaleUrl('es', 'about');
// → /es/about
// Get all locale variants of a page
const allAboutUrls = getRelativeLocaleUrlList('about');
// → ['/about', '/es/about', '/fr/about']
---
<!-- Language switcher -->
<nav>
<a href={getRelativeLocaleUrl('en', 'about')}>English</a>
<a href={getRelativeLocaleUrl('es', 'about')}>Español</a>
<a href={getRelativeLocaleUrl('fr', 'about')}>Français</a>
</nav>
```
## Content Collections with i18n
Organize translated content in subdirectories:
```
src/content/blog/
├── en/
│ └── post-1.md
└── es/
└── post-1.md
```
```astro
---
// src/pages/[lang]/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const pages = await getCollection('blog');
return pages.map(page => {
const [lang, ...slug] = page.id.split('/');
return { params: { lang, slug: slug.join('/') || undefined }, props: page };
});
}
const page = Astro.props;
const { Content } = await render(page);
---
<Content />
```
## Best Practices
1. **Use `getRelativeLocaleUrl()`** for generating links — don't hardcode locale prefixes
2. **Set up fallbacks** to avoid 404s for untranslated content
3. **Use `rewrite` fallback type** for better UX (shows content at original URL)
4. **Consider `prefixDefaultLocale: true`** for consistency across all locales
5. **Type locale params** to catch typos at build time
references/images.md
# Images
Astro provides built-in image optimization through the `astro:assets` module.
## Image Component
### Basic Usage
```astro
---
import { Image } from 'astro:assets';
import heroImage from '../images/hero.jpg';
---
<Image src={heroImage} alt="Hero image" />
```
Output: Optimized image with proper format, dimensions, and lazy loading.
### With Properties
```astro
---
import { Image } from 'astro:assets';
import photo from '../images/photo.jpg';
---
<Image
src={photo}
alt="A description"
width={800}
height={600}
format="webp"
quality={80}
loading="lazy"
decoding="async"
class="rounded-lg shadow-md"
/>
```
### Remote Images
```astro
---
import { Image } from 'astro:assets';
---
<Image
src="https://example.com/image.jpg"
alt="Remote image"
width={400}
height={300}
inferSize={false}
/>
```
For remote images, `width` and `height` are required (or use `inferSize`).
### Infer Size for Remote Images
```astro
<Image
src="https://example.com/image.jpg"
alt="Remote image"
inferSize
/>
```
## Picture Component
Provides responsive images with multiple formats:
```astro
---
import { Picture } from 'astro:assets';
import photo from '../images/photo.jpg';
---
<Picture
src={photo}
alt="Responsive image"
formats={['avif', 'webp', 'jpg']}
widths={[400, 800, 1200]}
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
/>
```
Output:
```html
<picture>
<source srcset="..." type="image/avif" sizes="...">
<source srcset="..." type="image/webp" sizes="...">
<img src="..." alt="Responsive image" loading="lazy" decoding="async">
</picture>
```
## getImage Function
For programmatic image processing:
```astro
---
import { getImage } from 'astro:assets';
import background from '../images/background.jpg';
const optimizedBg = await getImage({
src: background,
format: 'webp',
width: 1920,
quality: 80,
});
---
<div style={`background-image: url(${optimizedBg.src})`}>
Content with background
</div>
```
## Image Paths
### Local Images (src/)
```astro
---
// Import from src directory
import hero from '../images/hero.jpg';
import logo from '@/assets/logo.png'; // Using alias
---
<Image src={hero} alt="Hero" />
<Image src={logo} alt="Logo" />
```
### Public Folder Images
Images in `public/` are not optimized:
```astro
<!-- Not optimized, served as-is -->
<img src="/images/static-image.jpg" alt="Static" />
<!-- Can still use Image component with remote-like syntax -->
<Image
src="/images/static-image.jpg"
alt="Static"
width={400}
height={300}
/>
```
### Dynamic Imports
```astro
---
const images = import.meta.glob<{ default: ImageMetadata }>(
'../images/*.{jpg,png,gif}'
);
const imagePaths = Object.keys(images);
---
{imagePaths.map(async (path) => {
const image = await images[path]();
return <Image src={image.default} alt="" />;
})}
```
## Content Collections Images
### Schema with Image
```typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: ({ image }) => z.object({
title: z.string(),
cover: image(),
coverAlt: z.string(),
}),
});
export const collections = { blog };
```
### Markdown Frontmatter
```markdown
---
title: "My Post"
cover: "./images/cover.jpg"
coverAlt: "Post cover image"
---
Content here...
```
### Using in Template
```astro
---
import { Image } from 'astro:assets';
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-post');
---
<Image src={post.data.cover} alt={post.data.coverAlt} />
```
## Image Formats
Supported output formats:
- `webp` (default for optimization)
- `avif` (best compression, slower)
- `png` (lossless)
- `jpg`/`jpeg` (lossy)
- `svg` (pass-through, no optimization)
- `gif` (pass-through)
```astro
<Image src={photo} alt="Photo" format="avif" quality={60} />
```
## Quality Settings
```astro
<!-- Lower quality, smaller file -->
<Image src={photo} alt="" quality="low" />
<Image src={photo} alt="" quality={50} />
<!-- Medium quality (default) -->
<Image src={photo} alt="" quality="mid" />
<Image src={photo} alt="" quality={75} />
<!-- Higher quality -->
<Image src={photo} alt="" quality="high" />
<Image src={photo} alt="" quality={90} />
<!-- Maximum quality -->
<Image src={photo} alt="" quality="max" />
<Image src={photo} alt="" quality={100} />
```
## Responsive Images
### With Picture
```astro
<Picture
src={hero}
alt="Hero"
widths={[640, 768, 1024, 1280, 1536]}
sizes="(max-width: 640px) 640px, (max-width: 768px) 768px, (max-width: 1024px) 1024px, (max-width: 1280px) 1280px, 1536px"
formats={['avif', 'webp', 'jpg']}
/>
```
### With densities
```astro
<Image
src={logo}
alt="Logo"
width={200}
densities={[1, 2, 3]}
/>
<!-- Output srcset with 1x, 2x, 3x versions -->
```
## Configuration
### Image Service
```javascript
// astro.config.mjs
export default defineConfig({
image: {
// Sharp is default, or use custom service
service: {
entrypoint: 'astro/assets/services/sharp',
config: {
limitInputPixels: false,
},
},
// Allowed remote domains
domains: ['example.com', 'cdn.example.com'],
// Allowed remote patterns
remotePatterns: [
{
protocol: 'https',
hostname: '**.amazonaws.com',
},
],
},
});
```
### External Image Service
```javascript
// astro.config.mjs
export default defineConfig({
image: {
service: {
entrypoint: '@astrojs/cloudinary',
config: {
cloudName: 'your-cloud-name',
},
},
},
});
```
## Markdown Images
### Using Relative Paths
```markdown
<!-- src/content/blog/post.md -->

```
### Using Image Component in MDX
```mdx
---
title: My Post
---
import { Image } from 'astro:assets';
import photo from './images/photo.jpg';
# My Post
<Image src={photo} alt="Photo" width={600} />
Regular markdown images still work:

```
## Background Images
```astro
---
import { getImage } from 'astro:assets';
import bg from '../images/background.jpg';
const optimizedBg = await getImage({ src: bg, format: 'webp' });
---
<section class="hero" style={`background-image: url(${optimizedBg.src})`}>
<h1>Welcome</h1>
</section>
<style>
.hero {
background-size: cover;
background-position: center;
min-height: 100vh;
}
</style>
```
## Best Practices
1. **Always use Image component for local images** - Automatic optimization
2. **Provide alt text** - Accessibility requirement
3. **Use Picture for hero images** - Multiple formats and sizes
4. **Specify width/height for CLS** - Prevents layout shift
5. **Use WebP/AVIF formats** - Better compression
6. **Configure remote domains** - Security whitelist
7. **Use quality settings appropriately** - Balance size vs quality
8. **Import images, don't use strings** - Enables optimization
9. **Use content collection image schema** - Type-safe image handling
references/middleware.md
# Middleware
Middleware intercepts requests and responses, allowing you to add logic before pages render.
## Setup
Create `src/middleware.ts` (or `.js`):
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
// Run before the page renders
console.log('Request to:', context.url.pathname);
// Call next() to continue to the page/endpoint
const response = await next();
// Run after the page renders
console.log('Response status:', response.status);
return response;
});
```
## Context Object
```typescript
export const onRequest = defineMiddleware(async (context, next) => {
// Request info
const url = context.url; // URL object
const pathname = url.pathname; // /about
const params = context.params; // { slug: 'hello' } for dynamic routes
const request = context.request; // Request object
// Cookies
const token = context.cookies.get('session')?.value;
context.cookies.set('visited', 'true', { path: '/' });
// Locals - share data with pages
context.locals.user = await getUser(token);
// Site info
const site = context.site; // From astro.config
const generator = context.generator; // "Astro vX.X.X"
// Redirect
if (!context.locals.user && pathname.startsWith('/dashboard')) {
return context.redirect('/login');
}
// Rewrite
if (pathname === '/old-page') {
return context.rewrite('/new-page');
}
return next();
});
```
## Authentication Example
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { verifyToken } from './lib/auth';
const protectedRoutes = ['/dashboard', '/settings', '/api/user'];
export const onRequest = defineMiddleware(async ({ cookies, url, locals, redirect }, next) => {
const isProtected = protectedRoutes.some(route =>
url.pathname.startsWith(route)
);
if (isProtected) {
const token = cookies.get('auth_token')?.value;
if (!token) {
return redirect('/login?redirect=' + encodeURIComponent(url.pathname));
}
try {
const user = await verifyToken(token);
locals.user = user;
} catch {
cookies.delete('auth_token');
return redirect('/login');
}
}
return next();
});
```
## Using Locals
### Set in Middleware
```typescript
// src/middleware.ts
export const onRequest = defineMiddleware(async ({ locals, cookies }, next) => {
const token = cookies.get('session')?.value;
locals.user = token ? await getUserFromToken(token) : null;
locals.theme = cookies.get('theme')?.value || 'light';
locals.requestTime = Date.now();
return next();
});
```
### Access in Pages
```astro
---
// src/pages/dashboard.astro
const { user, theme } = Astro.locals;
if (!user) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
<p>Theme: {theme}</p>
```
### Access in Endpoints
```typescript
// src/pages/api/profile.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ locals }) => {
const { user } = locals;
if (!user) {
return new Response(null, { status: 401 });
}
return new Response(JSON.stringify(user));
};
```
## TypeScript Types
```typescript
// src/env.d.ts
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
user: {
id: string;
email: string;
name: string;
} | null;
theme: 'light' | 'dark';
requestTime: number;
}
}
```
## Chaining Middleware
Use `sequence()` to run multiple middleware functions:
```typescript
// src/middleware.ts
import { sequence } from 'astro:middleware';
const logging = defineMiddleware(async (context, next) => {
console.log(`[${new Date().toISOString()}] ${context.url.pathname}`);
return next();
});
const auth = defineMiddleware(async ({ cookies, locals }, next) => {
const token = cookies.get('session')?.value;
locals.user = token ? await verifyToken(token) : null;
return next();
});
const rateLimit = defineMiddleware(async ({ request, redirect }, next) => {
const ip = request.headers.get('x-forwarded-for');
if (await isRateLimited(ip)) {
return new Response('Too Many Requests', { status: 429 });
}
return next();
});
// Executes in order: logging → rateLimit → auth
export const onRequest = sequence(logging, rateLimit, auth);
```
## Modifying Responses
### Add Headers
```typescript
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Add security headers
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
return response;
});
```
### Modify Response Body
```typescript
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Only modify HTML responses
const contentType = response.headers.get('content-type');
if (!contentType?.includes('text/html')) {
return response;
}
const html = await response.text();
const modified = html.replace('</body>', '<script>/* injected */</script></body>');
return new Response(modified, {
status: response.status,
headers: response.headers,
});
});
```
## Redirects and Rewrites
### Redirect
```typescript
export const onRequest = defineMiddleware(async ({ url, redirect }, next) => {
// Redirect old URLs
if (url.pathname === '/old-blog') {
return redirect('/blog', 301); // Permanent redirect
}
// Temporary redirect
if (url.pathname === '/maintenance') {
return redirect('/coming-soon', 302);
}
return next();
});
```
### Rewrite
Serve different content for a URL without changing the URL:
```typescript
export const onRequest = defineMiddleware(async ({ url, rewrite }, next) => {
// A/B testing
if (url.pathname === '/landing') {
const variant = Math.random() > 0.5 ? 'a' : 'b';
return rewrite(`/landing-${variant}`);
}
// Serve localized content
const locale = getLocaleFromHeaders();
if (url.pathname === '/about' && locale === 'es') {
return rewrite('/es/about');
}
return next();
});
```
## Error Handling
```typescript
export const onRequest = defineMiddleware(async (context, next) => {
try {
return await next();
} catch (error) {
console.error('Middleware error:', error);
// Return error page
return new Response('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain',
},
});
}
});
```
## Common Patterns
### CORS
```typescript
export const onRequest = defineMiddleware(async ({ request, url }, next) => {
// Handle preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
const response = await next();
// Add CORS headers to response
response.headers.set('Access-Control-Allow-Origin', '*');
return response;
});
```
### Request Timing
```typescript
export const onRequest = defineMiddleware(async ({ url }, next) => {
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
response.headers.set('X-Response-Time', `${duration.toFixed(2)}ms`);
console.log(`${url.pathname}: ${duration.toFixed(2)}ms`);
return response;
});
```
## Best Practices
1. **Keep middleware fast** - Runs on every request
2. **Use locals for shared data** - Cleaner than headers
3. **Chain with sequence()** - Organize separate concerns
4. **Type your locals** - Update `env.d.ts`
5. **Handle errors gracefully** - Don't expose stack traces
6. **Use early returns** - For redirects and error responses
7. **Avoid heavy operations** - Cache where possible
references/routing.md
# Routing
Astro uses file-based routing in the `src/pages/` directory.
## Basic Routes
```
src/pages/
├── index.astro → /
├── about.astro → /about
├── contact.astro → /contact
└── blog/
├── index.astro → /blog
└── post-1.astro → /blog/post-1
```
## Page Files
### Supported Formats
- `.astro` - Astro components
- `.md` - Markdown
- `.mdx` - MDX (with integration)
- `.html` - Static HTML
- `.js/.ts` - Endpoints (API routes)
### Basic Page
```astro
---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';
const title = "About Us";
---
<Layout title={title}>
<h1>{title}</h1>
<p>Welcome to our about page.</p>
</Layout>
```
## Dynamic Routes
### Single Parameter
```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
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();
---
<h1>{post.data.title}</h1>
<Content />
```
### Multiple Parameters
```astro
---
// src/pages/[category]/[slug].astro
export function getStaticPaths() {
return [
{ params: { category: 'tech', slug: 'astro-intro' } },
{ params: { category: 'life', slug: 'travel-tips' } },
];
}
const { category, slug } = Astro.params;
---
<p>Category: {category}, Slug: {slug}</p>
```
### Rest Parameters (Catch-all)
```astro
---
// src/pages/docs/[...path].astro
export function getStaticPaths() {
return [
{ params: { path: undefined } }, // /docs
{ params: { path: 'getting-started' } }, // /docs/getting-started
{ params: { path: 'guides/routing' } }, // /docs/guides/routing
];
}
const { path } = Astro.params;
// path can be undefined, "getting-started", or "guides/routing"
---
```
## Server-Side Routes (SSR)
With SSR enabled, you can access params without `getStaticPaths`:
```astro
---
// src/pages/products/[id].astro
// Requires output: 'server' or 'hybrid' with prerender = false
const { id } = Astro.params;
const response = await fetch(`https://api.example.com/products/${id}`);
const product = await response.json();
if (!product) {
return new Response(null, {
status: 404,
statusText: 'Not Found'
});
}
---
<h1>{product.name}</h1>
```
## Endpoints (API Routes)
### Static Endpoints
```typescript
// src/pages/api/posts.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async () => {
const posts = await getCollection('blog');
return new Response(
JSON.stringify(posts.map(p => ({
title: p.data.title,
slug: p.slug,
}))),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
}
);
};
```
### Server Endpoints (SSR)
```typescript
// src/pages/api/submit.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
// Validate and process
if (!email) {
return new Response(
JSON.stringify({ error: 'Email required' }),
{ status: 400 }
);
}
// Save to database, send email, etc.
return new Response(
JSON.stringify({ success: true }),
{ status: 200 }
);
};
```
### Dynamic Endpoints
```typescript
// src/pages/api/users/[id].ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params }) => {
const { id } = params;
const user = await fetchUser(id);
if (!user) {
return new Response(null, { status: 404 });
}
return new Response(JSON.stringify(user));
};
export const DELETE: APIRoute = async ({ params }) => {
const { id } = params;
await deleteUser(id);
return new Response(null, { status: 204 });
};
```
## Redirects
### In Configuration
```javascript
// astro.config.mjs
export default defineConfig({
redirects: {
'/old-page': '/new-page',
'/blog/[...slug]': '/articles/[...slug]',
'/external': 'https://example.com',
},
});
```
### Programmatic Redirects (SSR)
```astro
---
// In page or middleware
if (!user) {
return Astro.redirect('/login');
}
// With status code
return Astro.redirect('/dashboard', 307);
---
```
## Rewrites
Serve different content for a URL without redirect:
```astro
---
// src/pages/[lang]/about.astro
const { lang } = Astro.params;
if (lang !== 'en' && lang !== 'es') {
return Astro.rewrite('/404');
}
---
```
## Route Priority
When multiple routes could match, Astro uses this priority:
1. Static routes (`/about`)
2. Dynamic routes with named params (`/blog/[slug]`)
3. Rest parameters (`/[...path]`)
```
/posts/create → src/pages/posts/create.astro (static wins)
/posts/hello-world → src/pages/posts/[slug].astro (dynamic)
/posts/2024/01/15 → src/pages/posts/[...slug].astro (rest)
```
## Pagination
```astro
---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const sortedPosts = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(sortedPosts, { pageSize: 10 });
}
const { page } = Astro.props;
---
<ul>
{page.data.map((post) => (
<li>{post.data.title}</li>
))}
</ul>
<nav>
{page.url.prev && <a href={page.url.prev}>Previous</a>}
<span>Page {page.currentPage} of {page.lastPage}</span>
{page.url.next && <a href={page.url.next}>Next</a>}
</nav>
```
## i18n Routing
```javascript
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
routing: {
prefixDefaultLocale: false, // /about vs /en/about
},
},
});
```
```
src/pages/
├── index.astro → /
├── about.astro → /about
├── es/
│ ├── index.astro → /es
│ └── about.astro → /es/about
└── fr/
├── index.astro → /fr
└── about.astro → /fr/about
```
## Best Practices
1. **Use content collections for dynamic content** - Type safety and validation
2. **Keep API endpoints RESTful** - Clear HTTP methods and paths
3. **Use rest params for catch-all** - `[...slug]` for flexible paths
4. **Configure redirects in config** - Better than manual redirects
5. **Test route priority** - Ensure expected routes win
6. **Use pagination for large lists** - Better performance and UX
references/server-islands.md
# Server Islands
Server islands allow you to defer rendering of specific Astro components to the server, loading them independently from the rest of the page. This keeps your main content fast while dynamic/personalized sections load separately.
## How It Works
1. The page renders immediately with fallback content as placeholder
2. Each `server:defer` component is fetched via a separate request
3. The component's HTML replaces the fallback when ready
4. Each island loads independently — a slow island won't block others
## Basic Usage
Add `server:defer` to any Astro component to turn it into a server island:
```astro
---
import Avatar from '../components/Avatar.astro';
import ProductReviews from '../components/ProductReviews.astro';
---
<!-- Static content renders immediately -->
<h1>Product Page</h1>
<p>This content is fast and cacheable.</p>
<!-- Server island: deferred rendering -->
<Avatar server:defer />
<!-- Server island with fallback -->
<ProductReviews server:defer>
<div slot="fallback">
<p>Loading reviews...</p>
</div>
</ProductReviews>
```
## Fallback Content
Use the named `"fallback"` slot to show placeholder content while the island loads:
```astro
<Avatar server:defer>
<!-- Generic placeholder shown until real avatar loads -->
<img slot="fallback" src="/generic-avatar.svg" alt="Loading..." />
</Avatar>
```
Good fallback patterns:
- Generic placeholder (e.g., generic avatar instead of user's avatar)
- Loading skeleton/spinner
- Placeholder UI with approximate dimensions (prevents layout shift)
## Server Island Components
Inside a server island component, you can do anything a normal SSR component can:
```astro
---
// src/components/Avatar.astro
// This runs on the server when the island is requested
const userSession = Astro.cookies.get('session');
const avatarURL = await getUserAvatar(userSession);
---
<img alt="User avatar" src={avatarURL} />
```
## Props
Props passed to server islands must be **serializable** — they're encoded in the request URL.
**Supported types:** plain objects, `number`, `string`, `Array`, `Map`, `Set`, `RegExp`, `Date`, `BigInt`, `URL`, `Uint8Array`, `Uint16Array`, `Uint32Array`, `Infinity`
**NOT supported:** functions, class instances, circular references
```astro
<!-- OK: serializable props -->
<ProductCard server:defer productId="abc-123" />
<UserBadge server:defer userId={42} showAvatar={true} />
<!-- NOT OK: functions can't be serialized -->
<Component server:defer onClick={handleClick} />
```
**Keep props small.** Props are encoded in the URL query string. If the URL exceeds ~2048 bytes, Astro switches to a POST request which browsers don't cache. Pass IDs rather than full data objects.
## Accessing the Page URL
Server islands run in their own isolated context. `Astro.url` returns the island's internal URL (e.g., `/_server-islands/Avatar`), not the page URL.
To access the page URL, check the `Referer` header:
```astro
---
const referer = Astro.request.headers.get('Referer');
const url = new URL(referer);
const productId = url.searchParams.get('product');
---
```
## Caching
Server island data is fetched via GET requests, so standard `Cache-Control` headers work:
```astro
---
// Cache this island for 1 hour
Astro.response.headers.set('Cache-Control', 'max-age=3600');
---
```
## Requirements
- An adapter must be installed (Node, Vercel, Netlify, Cloudflare, etc.)
- Server islands work in both `server` and `hybrid` output modes
- The page containing the island can be prerendered — only the island itself needs server rendering
## Use Cases
Server islands are ideal for:
- **Personalized content** on otherwise static pages (user avatars, greeting bars)
- **Dynamic data** that changes frequently (product prices, stock status, reviews)
- **Authenticated sections** (user dashboards widgets on a public page)
- **Slow data sources** that would delay the whole page if rendered inline
## Expert Patterns
### The E-Commerce Page Pattern
The most common server island pattern — three rendering strategies on one page:
```astro
---
import PriceStock from '../components/PriceStock.astro';
import AddToCart from '../components/AddToCart.jsx';
---
<!-- Static: title, images, description (cached at CDN) -->
<h1>{product.title}</h1>
<img src={product.image} alt={product.title} />
<p>{product.description}</p>
<!-- Server island: price/stock changes often, needs server data -->
<PriceStock server:defer productId={product.id}>
<div slot="fallback" class="price-skeleton" />
</PriceStock>
<!-- Client island: add-to-cart needs onClick/state -->
<AddToCart client:load productId={product.id} />
```
### Common Mistakes
**Passing large objects as props:**
```astro
<!-- BAD: entire product object gets URL-encoded -->
<PriceStock server:defer product={fullProductObject} />
<!-- GOOD: pass only the ID, fetch inside the island -->
<PriceStock server:defer productId={product.id} />
```
If serialized props exceed ~2048 bytes, Astro switches from GET to POST — losing browser/CDN caching entirely.
**Forgetting the fallback slot:**
Without `slot="fallback"`, users see nothing until the island loads. Always provide a placeholder that matches the island's dimensions to prevent layout shift.
**Using `Astro.url` instead of `Referer`:**
`Astro.url` inside a server island returns `/_server-islands/ComponentName`, not the page URL. This is a frequent bug source. Always use the `Referer` header:
```astro
---
// WRONG: returns /_server-islands/PriceStock
const url = Astro.url;
// RIGHT: returns the actual page URL
const pageUrl = new URL(Astro.request.headers.get('Referer'));
---
```
### When NOT to Use Server Islands
- **Data that doesn't change per-request** → Use static rendering with rebuild triggers instead
- **Components needing fast interactivity** (clicks, hover effects) → Use `client:*` directives
- **Nested server islands** → Not supported; flatten your component hierarchy
- **Components depending on page layout context** → Server islands render in isolation; they can't access parent component state
## Encryption Key for Deployments
Props are encrypted. In rolling deployments or multi-region setups where frontend/backend may use different keys:
```bash
astro create-key
```
Set the result as `ASTRO_KEY` environment variable in your build environment to keep encryption in sync.
references/sessions.md
# Sessions
Sessions store data on the server between requests for on-demand rendered pages. Unlike cookies, sessions have no size limits and are more secure since data never leaves the server.
**Added in:** Astro 5.7+
## Setup
Sessions require a storage driver. Some adapters (Node, Cloudflare, Netlify) configure a default driver automatically.
```javascript
// astro.config.mjs
import { defineConfig, sessionDrivers } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
adapter: vercel(),
session: {
driver: sessionDrivers.redis({
url: process.env.REDIS_URL,
}),
},
});
```
Any [unstorage driver](https://unstorage.unjs.io/drivers) can be used (Redis, filesystem, memory, etc.).
## Using Sessions
### In Astro Components and Pages
```astro
---
export const prerender = false; // Required in hybrid mode
const cart = await Astro.session?.get('cart');
---
<a href="/checkout">Cart: {cart?.length ?? 0} items</a>
```
### In API Endpoints
```typescript
// src/pages/api/addToCart.ts
export async function POST(context: APIContext) {
const cart = await context.session?.get('cart') || [];
const data = await context.request.json<{ item: string }>();
if (!data?.item) {
return new Response('Item is required', { status: 400 });
}
cart.push(data.item);
await context.session?.set('cart', cart);
return Response.json(cart);
}
```
### In Actions
```typescript
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
addToCart: defineAction({
input: z.object({ productId: z.string() }),
handler: async (input, context) => {
const cart = await context.session?.get('cart') || [];
cart.push(input.productId);
await context.session?.set('cart', cart);
return cart;
},
}),
};
```
### In Middleware
```typescript
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
context.session?.set('lastVisit', new Date());
return next();
});
```
**Note:** Sessions are not supported in edge middleware.
## Session API
| Method | Description |
|--------|-------------|
| `session.get(key)` | Get a value by key |
| `session.set(key, value)` | Set a value |
| `session.regenerate()` | Create a new session ID (use after login) |
| `session.destroy()` | Delete the entire session (use on logout) |
## Type Safety
Define session data types in `src/env.d.ts`:
```typescript
declare namespace App {
interface SessionData {
user: {
id: string;
name: string;
};
cart: string[];
lastVisit: Date;
}
}
```
This enables type-checking and autocomplete:
```astro
---
const cart = await Astro.session?.get('cart');
// const cart: string[] | undefined
Astro.session?.set('user', { id: 1, name: 'Houston' });
// Error: id should be string, not number
---
```
## Supported Data Types
Session values are serialized with [devalue](https://github.com/Rich-Harris/devalue). Supported types: strings, numbers, `Date`, `Map`, `Set`, `URL`, arrays, and plain objects.
## Best Practices
1. **Always use optional chaining** (`session?.get()`) — session may be undefined if not configured
2. **Regenerate after login** — prevents session fixation attacks
3. **Destroy on logout** — cleans up server-side data
4. **Keep session data small** — store IDs and references, not large objects
5. **Type your session data** — use `App.SessionData` for safety
6. **Don't rely on sessions for prerendered pages** — sessions only work with on-demand rendering
references/ssr-adapters.md
# SSR & Adapters
Astro supports on-demand server rendering with various deployment adapters.
## Output Modes
### Static (Default)
```javascript
// astro.config.mjs
export default defineConfig({
output: 'static', // Default - all pages prerendered
});
```
All pages built at build time. No server required.
### Server (Full SSR)
```javascript
// astro.config.mjs
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // All pages rendered on-demand
adapter: node({
mode: 'standalone',
}),
});
```
All pages rendered per-request. Requires an adapter.
### Hybrid
```javascript
// astro.config.mjs
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'hybrid', // Static by default, opt-in to SSR
adapter: vercel(),
});
```
Static by default, with option to make specific pages dynamic.
## Opting In/Out of Prerendering
### In Hybrid Mode (opt-out of prerendering)
```astro
---
// src/pages/api/time.ts
export const prerender = false; // Server-rendered
export const GET = () => {
return new Response(new Date().toISOString());
};
---
```
### In Server Mode (opt-in to prerendering)
```astro
---
// src/pages/about.astro
export const prerender = true; // Static at build time
---
<h1>About Us</h1>
```
## Available Adapters
### Node.js
```bash
npx astro add node
```
```javascript
// astro.config.mjs
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone', // or 'middleware'
}),
});
```
### Vercel
```bash
npx astro add vercel
```
```javascript
// astro.config.mjs
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel({
webAnalytics: { enabled: true },
imageService: true,
}),
});
```
### Netlify
```bash
npx astro add netlify
```
```javascript
// astro.config.mjs
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify({
edgeMiddleware: true, // Use Edge Functions
}),
});
```
### Cloudflare
```bash
npx astro add cloudflare
```
```javascript
// astro.config.mjs
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
mode: 'directory', // or 'advanced'
routes: {
strategy: 'include',
include: ['/api/*'],
},
}),
});
```
### Deno
```bash
npx astro add deno
```
```javascript
// astro.config.mjs
import deno from '@astrojs/deno';
export default defineConfig({
output: 'server',
adapter: deno(),
});
```
## SSR Features
### Request Object
```astro
---
// Available in SSR pages
const url = Astro.url;
const method = Astro.request.method;
const headers = Astro.request.headers;
const userAgent = headers.get('user-agent');
// Get request body (POST, PUT, etc.)
if (method === 'POST') {
const formData = await Astro.request.formData();
const json = await Astro.request.json();
}
---
```
### Cookies
```astro
---
// Reading cookies
const sessionId = Astro.cookies.get('session')?.value;
const prefs = Astro.cookies.get('prefs')?.json();
// Setting cookies
Astro.cookies.set('session', 'abc123', {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 1 week
});
// Deleting cookies
Astro.cookies.delete('session');
---
```
### Response Headers
```astro
---
Astro.response.headers.set('Cache-Control', 'max-age=3600');
Astro.response.headers.set('X-Custom-Header', 'value');
---
```
### Redirects
```astro
---
if (!user) {
return Astro.redirect('/login', 302);
}
---
```
## Server Islands
Defer rendering of specific Astro components to the server. Each island loads independently, keeping main content fast. No experimental flag needed in Astro 5+.
```astro
---
import UserProfile from '../components/UserProfile.astro';
import ProductReviews from '../components/ProductReviews.astro';
---
<!-- Static content renders immediately -->
<h1>Welcome</h1>
<!-- Server-rendered on each request -->
<UserProfile server:defer>
<p slot="fallback">Loading profile...</p>
</UserProfile>
<!-- Multiple islands load in parallel -->
<ProductReviews server:defer productId="abc-123">
<div slot="fallback">Loading reviews...</div>
</ProductReviews>
```
Key points:
- Requires an adapter (same as SSR)
- Props must be serializable (no functions)
- Use `Referer` header to access page URL inside island
- See [references/server-islands.md](server-islands.md) for full details
## Sessions
Server-side session storage for on-demand rendered pages (Astro 5.7+):
```javascript
// astro.config.mjs
import { defineConfig, sessionDrivers } from 'astro/config';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
session: {
driver: sessionDrivers.redis({ url: process.env.REDIS_URL }),
},
});
```
```astro
---
export const prerender = false;
const cart = await Astro.session?.get('cart');
await Astro.session?.set('lastVisit', new Date());
---
```
See [references/sessions.md](sessions.md) for full details.
## API Endpoints
### GET, POST, PUT, DELETE
```typescript
// src/pages/api/users/[id].ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
const { id } = params;
const user = await db.users.findById(id);
if (!user) {
return new Response(null, { status: 404 });
}
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
};
export const PUT: APIRoute = async ({ params, request }) => {
const { id } = params;
const data = await request.json();
await db.users.update(id, data);
return new Response(null, { status: 204 });
};
export const DELETE: APIRoute = async ({ params }) => {
const { id } = params;
await db.users.delete(id);
return new Response(null, { status: 204 });
};
```
### Streaming Responses
```typescript
// src/pages/api/stream.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: ${i}\n\n`));
await new Promise(r => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
};
```
## Context and Locals
Access shared data across middleware and pages:
```typescript
// src/middleware.ts
export const onRequest = async ({ locals, request }, next) => {
const token = request.headers.get('authorization');
locals.user = await validateToken(token);
return next();
};
```
```astro
---
// src/pages/dashboard.astro
const { user } = Astro.locals;
if (!user) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
```
## Best Practices
1. **Use hybrid mode** - Static by default, SSR only where needed
2. **Prerender where possible** - Better performance and caching
3. **Use appropriate adapter** - Match your deployment platform
4. **Handle errors gracefully** - Return proper status codes
5. **Set cache headers** - Control CDN and browser caching
6. **Validate user input** - Never trust request data
7. **Use locals for shared state** - Cleaner than passing through props
references/styling.md
# Styling
Astro supports various styling approaches with scoped styles as the default.
## Scoped Styles (Default)
Styles in `<style>` tags are automatically scoped to the component:
```astro
---
// Component.astro
---
<div class="container">
<h1>Hello World</h1>
</div>
<style>
/* Only affects this component */
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: navy;
}
</style>
```
Astro adds unique class attributes to scope styles:
```html
<!-- Output -->
<div class="container astro-J7PV25F6">
<h1 class="astro-J7PV25F6">Hello World</h1>
</div>
```
## Global Styles
### :global() Selector
Target elements globally within a scoped style block:
```astro
<style>
/* Scoped to component */
.container {
padding: 1rem;
}
/* Global - affects all h1 elements */
:global(h1) {
font-size: 2rem;
}
/* Global within scoped context */
.container :global(p) {
line-height: 1.6;
}
</style>
```
### is:global Attribute
Make entire style block global:
```astro
<style is:global>
/* All styles here are global */
body {
font-family: system-ui, sans-serif;
}
a {
color: blue;
}
</style>
```
### Global Stylesheets
```astro
---
// src/layouts/Layout.astro
import '../styles/global.css';
---
<!doctype html>
<html>
<body>
<slot />
</body>
</html>
```
```css
/* src/styles/global.css */
:root {
--color-primary: #3b82f6;
--color-text: #1f2937;
}
body {
color: var(--color-text);
line-height: 1.5;
}
```
## CSS Variables
### Defining Variables
```astro
<style>
:root {
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
}
.card {
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
</style>
```
### Dynamic Variables with define:vars
Pass JavaScript values to CSS:
```astro
---
const theme = {
primaryColor: '#3b82f6',
fontSize: '16px',
};
const backgroundUrl = '/images/hero.jpg';
---
<div class="hero">
<h1>Welcome</h1>
</div>
<style define:vars={{
primaryColor: theme.primaryColor,
fontSize: theme.fontSize,
bgImage: `url(${backgroundUrl})`
}}>
.hero {
background-image: var(--bgImage);
color: var(--primaryColor);
font-size: var(--fontSize);
}
</style>
```
## class:list Directive
Conditionally apply classes:
```astro
---
const isActive = true;
const isDisabled = false;
const size = 'large';
---
<button class:list={[
'btn', // Always applied
{ 'btn-active': isActive }, // Applied if true
{ 'btn-disabled': isDisabled }, // Not applied (false)
size && `btn-${size}`, // "btn-large"
['extra', 'classes'], // Arrays flattened
]}>
Click Me
</button>
<!-- Output: <button class="btn btn-active btn-large extra classes">Click Me</button> -->
```
## CSS Preprocessors
### Sass/SCSS
```bash
npm install sass
```
```astro
<style lang="scss">
$primary: #3b82f6;
$spacing: 1rem;
.card {
padding: $spacing;
border: 1px solid lighten($primary, 30%);
&:hover {
border-color: $primary;
}
.title {
color: $primary;
font-weight: bold;
}
}
</style>
```
### Less
```bash
npm install less
```
```astro
<style lang="less">
@primary: #3b82f6;
.button {
background: @primary;
&:hover {
background: darken(@primary, 10%);
}
}
</style>
```
### Stylus
```bash
npm install stylus
```
```astro
<style lang="stylus">
primary = #3b82f6
.container
max-width 800px
margin 0 auto
</style>
```
## Tailwind CSS
### Setup
```bash
npx astro add tailwind
```
### Usage
```astro
---
// No style tag needed
---
<div class="max-w-4xl mx-auto p-4">
<h1 class="text-3xl font-bold text-blue-600">
Hello World
</h1>
<p class="mt-4 text-gray-700 leading-relaxed">
Welcome to my site.
</p>
</div>
```
### With class:list
```astro
---
const isLarge = true;
---
<button class:list={[
'px-4 py-2 rounded',
'bg-blue-500 hover:bg-blue-600',
'text-white font-medium',
{ 'text-lg': isLarge },
]}>
Click Me
</button>
```
## External Stylesheets
### Link in Head
```astro
---
// src/layouts/Layout.astro
---
<html>
<head>
<link rel="stylesheet" href="/styles/global.css" />
<link rel="stylesheet" href="https://cdn.example.com/library.css" />
</head>
<body>
<slot />
</body>
</html>
```
### Import in Component
```astro
---
import '../styles/component.css';
import 'package/styles.css';
---
```
## CSS Modules
Astro supports CSS Modules for component-scoped styles:
```astro
---
import styles from './Button.module.css';
---
<button class={styles.button}>
<span class={styles.icon}>+</span>
Click Me
</button>
```
```css
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
background: blue;
}
.icon {
margin-right: 0.5rem;
}
```
## PostCSS
Create `postcss.config.mjs`:
```javascript
// postcss.config.mjs
export default {
plugins: {
autoprefixer: {},
'postcss-nesting': {},
},
};
```
Then use modern CSS features:
```astro
<style>
.card {
& .title {
font-weight: bold;
}
&:hover {
background: #f0f0f0;
}
}
</style>
```
## Style Inheritance
Styles don't cascade into child components:
```astro
<!-- Parent.astro -->
<div class="parent">
<Child />
</div>
<style>
.parent p {
color: red; /* Does NOT affect p inside Child */
}
</style>
```
To style child content, use `:global()`:
```astro
<style>
.parent :global(p) {
color: red; /* Affects all nested p elements */
}
</style>
```
## Passing Classes to Components
### Accepting className prop
```astro
---
// Button.astro
interface Props {
class?: string;
}
const { class: className } = Astro.props;
---
<button class:list={['btn', className]}>
<slot />
</button>
<style>
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
}
</style>
```
```astro
<!-- Usage -->
<Button class="mt-4 custom-class">Click Me</Button>
```
## Best Practices
1. **Use scoped styles by default** - Prevents style conflicts
2. **Use :global() sparingly** - Only when necessary
3. **Use CSS variables for theming** - Easy to change globally
4. **Use define:vars for dynamic styles** - Pass JS values to CSS
5. **Use class:list for conditional classes** - Cleaner than ternaries
6. **Keep global styles minimal** - Reset, typography, utilities only
7. **Use preprocessors if needed** - Sass for complex styling
8. **Consider Tailwind for utility-first** - Rapid development
references/view-transitions.md
# View Transitions
Astro's View Transitions provide smooth navigation between pages without full page reloads.
## Setup
```astro
---
// src/layouts/Layout.astro
import { ClientRouter } from 'astro:transitions';
---
<!doctype html>
<html>
<head>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
```
## How It Works
With `<ClientRouter />` enabled:
1. User clicks a link
2. Astro intercepts the navigation
3. New page content is fetched
4. Transitions animate between old and new content
5. Browser history is updated
## Built-in Animations
### fade (Default)
```astro
---
import { fade } from 'astro:transitions';
---
<div transition:animate={fade({ duration: '0.4s' })}>
Content fades in and out
</div>
```
### slide
```astro
---
import { slide } from 'astro:transitions';
---
<div transition:animate={slide({ duration: '0.3s' })}>
Content slides in from the side
</div>
```
### initial
Prevents animation on first page load:
```astro
<div transition:animate="initial">
No animation on initial load
</div>
```
### none
Disables transition for an element:
```astro
<div transition:animate="none">
Instant swap, no animation
</div>
```
## Transition Directives
### transition:name
Link elements across pages for morphing:
```astro
<!-- Page 1: List -->
<img
src={post.image}
transition:name={`hero-${post.slug}`}
/>
<!-- Page 2: Detail -->
<img
src={post.data.image}
transition:name={`hero-${post.slug}`}
/>
```
Elements with matching `transition:name` will morph into each other.
### transition:animate
Control animation behavior:
```astro
<header transition:animate="none">
<!-- Static header, no animation -->
</header>
<main transition:animate={slide({ duration: '0.5s' })}>
<!-- Slides in -->
</main>
```
### transition:persist
Keep element state across navigations:
```astro
<!-- Video keeps playing during navigation -->
<video transition:persist autoplay>
<source src="video.mp4" />
</video>
<!-- Form keeps user input -->
<form transition:persist>
<input type="text" />
</form>
<!-- React component maintains state -->
<Counter client:load transition:persist />
```
With unique ID:
```astro
<audio transition:persist="player" controls>
<source src="song.mp3" />
</audio>
```
## Custom Animations
### Define with keyframes
```astro
---
import { slide } from 'astro:transitions';
---
<style>
@keyframes slideInFromLeft {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutToRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
</style>
<div
transition:animate={{
old: {
name: 'slideOutToRight',
duration: '0.3s',
easing: 'ease-in',
},
new: {
name: 'slideInFromLeft',
duration: '0.3s',
easing: 'ease-out',
},
}}
>
Custom sliding content
</div>
```
### Reusable Animation
```typescript
// src/transitions/custom.ts
export const customSlide = {
old: {
name: 'slideOut',
duration: '0.3s',
easing: 'ease-in',
fillMode: 'forwards',
},
new: {
name: 'slideIn',
duration: '0.3s',
easing: 'ease-out',
fillMode: 'backwards',
},
};
```
```astro
---
import { customSlide } from '../transitions/custom';
---
<div transition:animate={customSlide}>
Uses custom animation
</div>
```
## Navigation Controls
### Programmatic Navigation
```astro
<script>
import { navigate } from 'astro:transitions/client';
// Navigate programmatically
navigate('/about');
// With history options
navigate('/dashboard', { history: 'replace' });
</script>
```
### Prevent Navigation
```astro
<a href="/external" data-astro-reload>
Full page reload
</a>
<form data-astro-reload>
Submit causes full reload
</form>
```
## Lifecycle Events
```astro
<script>
document.addEventListener('astro:before-preparation', (event) => {
// Before fetching new page
console.log('Navigating to:', event.to);
});
document.addEventListener('astro:after-preparation', (event) => {
// After fetching, before swap
});
document.addEventListener('astro:before-swap', (event) => {
// Right before DOM swap
// Can customize swap behavior
event.swap = () => {
// Custom swap logic
};
});
document.addEventListener('astro:after-swap', (event) => {
// After DOM swap, before animations
// Good for re-initializing scripts
});
document.addEventListener('astro:page-load', (event) => {
// Page fully loaded, animations complete
// Runs on initial load and every navigation
});
</script>
```
## Script Re-execution
Scripts with `data-astro-rerun` execute on every navigation:
```astro
<script data-astro-rerun>
// This runs on initial load AND every navigation
console.log('Page changed!');
initializeComponent();
</script>
```
## Form Handling
Forms work with view transitions:
```astro
<form method="POST" action="/api/submit">
<input type="text" name="email" />
<button type="submit">Subscribe</button>
</form>
<!-- Disable transitions for form -->
<form data-astro-reload method="POST">
<!-- Full page reload on submit -->
</form>
```
## Fallback Behavior
Browsers without View Transitions API get:
- Full page navigation (no JavaScript errors)
- Graceful degradation
Check support:
```astro
<script>
if (document.startViewTransition) {
console.log('View Transitions supported!');
}
</script>
```
## Configuration
### Disable for Specific Links
```astro
<!-- Skip view transitions -->
<a href="/page" data-astro-reload>Full Reload</a>
```
### Prefetching
```javascript
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true, // Prefetch all links on hover
defaultStrategy: 'viewport', // or 'hover', 'load'
},
});
```
```astro
<!-- Manual prefetch control -->
<a href="/page" data-astro-prefetch="hover">Prefetch on hover</a>
<a href="/page" data-astro-prefetch="viewport">Prefetch when visible</a>
<a href="/page" data-astro-prefetch="load">Prefetch immediately</a>
<a href="/page" data-astro-prefetch="false">Never prefetch</a>
```
## Common Patterns
### Persistent Navigation
```astro
<!-- Navigation stays in place -->
<nav transition:persist>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<!-- Main content transitions -->
<main transition:animate={fade()}>
<slot />
</main>
```
### Image Gallery
```astro
<!-- List page -->
{images.map((img) => (
<a href={`/gallery/${img.id}`}>
<img
src={img.thumb}
transition:name={`image-${img.id}`}
/>
</a>
))}
<!-- Detail page -->
<img
src={image.full}
transition:name={`image-${image.id}`}
/>
```
## Best Practices
1. **Use transition:name for connected elements** - Creates smooth morphing
2. **Keep heavy components persistent** - Video players, iframes
3. **Re-run initialization scripts** - Use `data-astro-rerun`
4. **Handle loading states** - Show indicators during fetching
5. **Test without JavaScript** - Ensure graceful fallback
6. **Use prefetching wisely** - Balance speed vs bandwidth
7. **Avoid animating too many elements** - Can impact performance
rules/astro-components.rule.md
---
description: Rules for writing Astro components
globs:
- "**/*.astro"
---
# Astro Component Rules
## Component Structure
Always use the frontmatter pattern with proper separation:
```astro
---
// 1. Imports
import Layout from '../layouts/Layout.astro';
import { getCollection } from 'astro:content';
// 2. Props interface
interface Props {
title: string;
description?: string;
}
// 3. Destructure props with defaults
const { title, description = 'Default' } = Astro.props;
// 4. Data fetching and logic
const posts = await getCollection('blog');
---
<!-- 5. Template -->
<Layout title={title}>
<h1>{title}</h1>
</Layout>
<!-- 6. Scoped styles -->
<style>
h1 { color: navy; }
</style>
```
## MUST DO
- Define `interface Props` for type safety
- Use destructuring with defaults for optional props
- Keep frontmatter logic minimal and focused
- Use slots for composable content
- Use `class:list` for conditional classes
- Import components and assets at the top
## MUST NOT DO
- Access browser APIs (window, document) in frontmatter - runs on server
- Use side effects in frontmatter - runs on every render
- Mix UI framework components without client directives
- Forget alt text on images
- Use inline styles when scoped styles work
- Skip TypeScript interfaces for Props
## Slots Pattern
```astro
---
// Wrapper.astro
interface Props {
title: string;
}
const { title } = Astro.props;
const hasFooter = Astro.slots.has('footer');
---
<article>
<header><h1>{title}</h1></header>
<main><slot /></main>
{hasFooter && <footer><slot name="footer" /></footer>}
</article>
```
## Dynamic Attributes
```astro
---
const id = "main";
const isActive = true;
---
<div
id={id}
class:list={['base', { active: isActive }]}
data-active={isActive}
>
Content
</div>
```
## Conditional Rendering
```astro
---
const show = true;
const items = ['a', 'b', 'c'];
---
{show && <p>Visible</p>}
{show ? <p>Yes</p> : <p>No</p>}
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
```
rules/astro-images.rule.md
---
description: Rules for image optimization in Astro
globs:
- "**/*.astro"
- "**/*.mdx"
- "src/content/**/*"
---
# Astro Image Rules
## Image Component
```astro
---
import { Image } from 'astro:assets';
import heroImage from '../images/hero.jpg';
---
<Image
src={heroImage}
alt="Descriptive alt text"
width={800}
height={600}
format="webp"
quality={80}
/>
```
## MUST DO
- Import local images - enables optimization
- Always provide meaningful alt text
- Use `<Image>` component for local images
- Use `<Picture>` for responsive hero images
- Specify width/height to prevent layout shift
- Configure allowed domains for remote images
- Use the image() schema helper in content collections
## MUST NOT DO
- Use string paths for local images: `src="/images/hero.jpg"`
- Skip alt text (accessibility requirement)
- Use `<img>` tags for local images (misses optimization)
- Forget width/height for remote images
- Allow arbitrary remote domains
## Local vs Remote Images
```astro
---
import { Image } from 'astro:assets';
import localImage from '../images/photo.jpg';
---
<!-- Local: import required, auto-optimized -->
<Image src={localImage} alt="Local photo" />
<!-- Remote: dimensions required -->
<Image
src="https://example.com/photo.jpg"
alt="Remote photo"
width={400}
height={300}
/>
<!-- Remote with inferred size (fetches image) -->
<Image
src="https://example.com/photo.jpg"
alt="Remote photo"
inferSize
/>
```
## Picture for Responsive Images
```astro
---
import { Picture } from 'astro:assets';
import hero from '../images/hero.jpg';
---
<Picture
src={hero}
alt="Hero image"
formats={['avif', 'webp', 'jpg']}
widths={[400, 800, 1200]}
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
/>
```
## Content Collections with Images
```typescript
// src/content/config.ts
const blog = defineCollection({
type: 'content',
schema: ({ image }) => z.object({
title: z.string(),
cover: image(), // Validates and optimizes
coverAlt: z.string(),
}),
});
```
```markdown
---
title: My Post
cover: ./images/cover.jpg
coverAlt: Post cover showing...
---
```
## Background Images
```astro
---
import { getImage } from 'astro:assets';
import bg from '../images/background.jpg';
const optimizedBg = await getImage({
src: bg,
format: 'webp',
width: 1920,
});
---
<section style={`background-image: url(${optimizedBg.src})`}>
Content
</section>
```
## Configuration
```javascript
// astro.config.mjs
export default defineConfig({
image: {
domains: ['cdn.example.com'],
remotePatterns: [
{ protocol: 'https', hostname: '**.unsplash.com' },
],
},
});
```
## Quality Settings
- `quality="low"` or `quality={50}` - Smaller files
- `quality="mid"` or `quality={75}` - Default balance
- `quality="high"` or `quality={90}` - Better quality
- `quality="max"` or `quality={100}` - Maximum quality
rules/astro-routing.rule.md
---
description: Rules for Astro routing and pages
globs:
- "src/pages/**/*"
---
# Astro Routing Rules
## File-Based Routing
```
src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ └── [slug].astro → /blog/:slug
├── [...path].astro → /* (catch-all)
└── api/
└── posts.json.ts → /api/posts.json
```
## MUST DO
- Use `getStaticPaths()` for dynamic routes in static mode
- Return proper status codes from API endpoints
- Validate params in SSR routes
- Use content collections for dynamic content pages
- Handle 404 cases explicitly
## MUST NOT DO
- Access `Astro.request` in prerendered pages
- Forget `getStaticPaths()` in static output mode
- Return sensitive data in API responses without auth
- Use interactive flags like `git rebase -i` in routes
## Dynamic Routes Pattern
```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
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();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
```
## Rest Parameters (Catch-All)
```astro
---
// src/pages/docs/[...path].astro
export function getStaticPaths() {
return [
{ params: { path: undefined } }, // /docs
{ params: { path: 'intro' } }, // /docs/intro
{ params: { path: 'guides/start' } }, // /docs/guides/start
];
}
const { path } = Astro.params;
---
```
## API Endpoints
```typescript
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
const data = await fetchPosts();
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.title) {
return new Response(JSON.stringify({ error: 'Title required' }), {
status: 400,
});
}
const post = await createPost(body);
return new Response(JSON.stringify(post), { status: 201 });
};
```
## Pagination
```astro
---
// src/pages/blog/[...page].astro
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
return paginate(posts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
), { pageSize: 10 });
}
const { page } = Astro.props;
---
<ul>
{page.data.map(post => <li>{post.data.title}</li>)}
</ul>
{page.url.prev && <a href={page.url.prev}>Previous</a>}
{page.url.next && <a href={page.url.next}>Next</a>}
```
## Redirects
```javascript
// astro.config.mjs
export default defineConfig({
redirects: {
'/old': '/new',
'/blog/[...slug]': '/posts/[...slug]',
},
});
```
rules/astro-ssr.rule.md
---
description: Rules for SSR and hybrid rendering
globs:
- "astro.config.mjs"
- "src/pages/**/*"
- "src/middleware.ts"
---
# Astro SSR Rules
## Output Modes
```javascript
// astro.config.mjs
export default defineConfig({
output: 'static', // Default - all prerendered
output: 'server', // All server-rendered
output: 'hybrid', // Static default, opt-in SSR
adapter: vercel(), // Required for server/hybrid
});
```
## MUST DO
- Use `hybrid` mode when only some pages need SSR
- Install appropriate adapter for deployment target
- Use `export const prerender = false` to opt into SSR (hybrid mode)
- Use `export const prerender = true` to opt out of SSR (server mode)
- Handle errors and return proper HTTP status codes
- Validate and sanitize all user input
## MUST NOT DO
- Forget to install an adapter for server/hybrid output
- Access request/cookies in prerendered pages
- Trust user input without validation
- Expose sensitive data in responses
- Use SSR when static would work
## Opting In/Out of Prerendering
```astro
---
// In hybrid mode (default: prerendered)
export const prerender = false; // Make this page SSR
// In server mode (default: SSR)
export const prerender = true; // Prerender this page
---
```
## Request Handling
```astro
---
// SSR page
const url = Astro.url;
const pathname = url.pathname;
const searchParams = url.searchParams;
const method = Astro.request.method;
const headers = Astro.request.headers;
const userAgent = headers.get('user-agent');
// Cookies
const session = Astro.cookies.get('session')?.value;
Astro.cookies.set('visited', 'true', {
path: '/',
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24,
});
// Redirects
if (!session) {
return Astro.redirect('/login');
}
---
```
## Middleware
```typescript
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';
const auth = defineMiddleware(async ({ cookies, locals, redirect }, next) => {
const token = cookies.get('token')?.value;
locals.user = token ? await verifyToken(token) : null;
if (!locals.user && url.pathname.startsWith('/dashboard')) {
return redirect('/login');
}
return next();
});
const logging = defineMiddleware(async ({ url }, next) => {
console.log(`[${new Date().toISOString()}] ${url.pathname}`);
return next();
});
export const onRequest = sequence(logging, auth);
```
## Type Locals
```typescript
// src/env.d.ts
declare namespace App {
interface Locals {
user: { id: string; name: string } | null;
}
}
```
## Adapters
```bash
# Node.js
npx astro add node
# Vercel
npx astro add vercel
# Netlify
npx astro add netlify
# Cloudflare
npx astro add cloudflare
```
## Response Headers
```astro
---
Astro.response.headers.set('Cache-Control', 'max-age=3600');
Astro.response.headers.set('X-Custom-Header', 'value');
---
```
rules/astro-typescript.rule.md
---
description: Rules for TypeScript configuration in Astro projects
globs:
- "tsconfig.json"
- "src/env.d.ts"
- "**/*.ts"
- "**/*.astro"
---
# Astro TypeScript Rules
## tsconfig.json
```json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"],
"@content/*": ["src/content/*"]
}
}
}
```
## MUST DO
- Use `astro/tsconfigs/strict` or `strictest` preset
- Define path aliases for cleaner imports
- Type Props interface in all components
- Type locals in `src/env.d.ts`
- Type environment variables
## MUST NOT DO
- Use `any` type - use `unknown` and narrow
- Skip typing Props interface
- Ignore TypeScript errors
- Use `astro/tsconfigs/base` in production projects
## Component Props
```astro
---
interface Props {
title: string;
description?: string;
tags: string[];
variant?: 'primary' | 'secondary';
}
const {
title,
description = 'Default description',
tags,
variant = 'primary',
} = Astro.props;
---
```
## Environment Variables
```typescript
// src/env.d.ts
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly DATABASE_URL: string;
readonly SECRET_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
```
## Locals Typing
```typescript
// src/env.d.ts
declare namespace App {
interface Locals {
user: {
id: string;
email: string;
name: string;
} | null;
theme: 'light' | 'dark';
requestId: string;
}
}
```
## Content Collection Types
```typescript
// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
author: reference('authors'),
}),
});
// Types are auto-generated:
// import type { CollectionEntry } from 'astro:content';
// type BlogPost = CollectionEntry<'blog'>;
```
## API Route Types
```typescript
// src/pages/api/example.ts
import type { APIRoute, APIContext } from 'astro';
export const GET: APIRoute = async (context: APIContext) => {
const { params, request, cookies, locals, url } = context;
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
});
};
```
## Path Aliases Usage
```astro
---
// With aliases
import Layout from '@layouts/Layout.astro';
import Card from '@components/Card.astro';
import { getCollection } from 'astro:content';
// Instead of
import Layout from '../../../layouts/Layout.astro';
---
```
rules/client-hydration.rule.md
---
description: Rules for client-side hydration and islands architecture
globs:
- "**/*.astro"
- "**/components/**/*.tsx"
- "**/components/**/*.jsx"
- "**/components/**/*.vue"
- "**/components/**/*.svelte"
---
# Client Hydration Rules
## Islands Architecture Principle
Ship zero JavaScript by default. Only hydrate components that need interactivity.
## Client Directive Decision Tree
```
Does the component need interactivity?
├── No → No directive (static HTML)
└── Yes → Is it above the fold?
├── Yes → Is it critical?
│ ├── Yes → client:load
│ └── No → client:idle
└── No → Is it viewport dependent?
├── Yes → client:visible
└── No → Is it device dependent?
├── Yes → client:media
└── No → client:visible
```
## MUST DO
- Default to no hydration - question every `client:` directive
- Use `client:visible` for below-the-fold interactive content
- Use `client:idle` for non-critical above-the-fold interactions
- Use `client:media` for device-specific components
- Always specify framework for `client:only="react"` (required)
- Test that pages work without JavaScript
## MUST NOT DO
- Use `client:load` on everything - defeats islands purpose
- Hydrate static content (navbars, footers without interactions)
- Use `client:only` without the framework string
- Forget that `client:only` skips SSR entirely
- Hydrate large components that only need minor interactivity
## Examples
```astro
---
import Header from './Header.astro'; // Static - no JS
import SearchBox from './SearchBox.jsx'; // Needs hydration
import Comments from './Comments.jsx'; // Below fold
import MobileMenu from './MobileMenu.jsx'; // Mobile only
import Chart from './Chart.jsx'; // Browser APIs only
---
<!-- Static, no JavaScript -->
<Header />
<!-- Critical above-fold interactivity -->
<SearchBox client:load />
<!-- Hydrate when visible (below fold) -->
<Comments client:visible />
<!-- Hydrate only on mobile -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Client-only (uses browser APIs) -->
<Chart client:only="react" />
```
## Measuring Impact
Use browser DevTools to verify:
1. Network tab: Check JS bundle sizes
2. Performance tab: Measure TTI (Time to Interactive)
3. Lighthouse: Verify Core Web Vitals
## Passing Props to Hydrated Components
```astro
---
import Counter from './Counter.jsx';
const initialCount = 10;
---
<!-- Props are serialized to the client -->
<Counter client:load initialCount={initialCount} />
```
Note: Props must be serializable (no functions, no class instances).
rules/content-collections.rule.md
---
description: Rules for content collections and type-safe content
globs:
- "src/content/**/*"
- "src/content.config.ts"
- "src/content/config.ts"
- "src/live.config.ts"
---
# Content Collections Rules
## Collection Configuration (Astro 5+)
Define schemas in `src/content.config.ts` using loaders:
```typescript
import { defineCollection, reference } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) => z.object({
title: z.string(),
description: z.string().max(160),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
draft: z.boolean().default(false),
cover: image(),
coverAlt: z.string(),
author: reference('authors'),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
```
> **Migration note:** Astro 5+ uses `src/content.config.ts` (not `src/content/config.ts`), `loader` (not `type`), `import { z } from 'astro/zod'` (not `from 'astro:content'`), and `import { render } from 'astro:content'` for rendering.
## MUST DO
- Use `z.coerce.date()` for date fields - handles string dates from frontmatter
- Use `image()` helper for optimized images
- Use `reference()` for cross-collection relationships
- Set sensible defaults with `.default()`
- Add `.optional()` for non-required fields
- Filter drafts in production: `getCollection('blog', ({ data }) => !data.draft)`
- Export all collections from the config
## MUST NOT DO
- Use plain `z.date()` - won't parse frontmatter strings
- Skip schema validation
- Store sensitive data in content collections
- Use arbitrary image paths - use the image() schema helper
- Forget to handle the case when `getEntry` returns undefined
## Querying Collections
```typescript
// Get all published posts, sorted by date
const posts = (await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
})).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// Get single entry
const post = await getEntry('blog', 'my-post-slug');
if (!post) return Astro.redirect('/404');
// Resolve references
const author = await getEntry(post.data.author);
```
## Rendering Content
```astro
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'my-post');
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<nav>
{headings.map(h => (
<a href={`#${h.slug}`}>{h.text}</a>
))}
</nav>
<Content />
</article>
```
## Dynamic Routes
```astro
---
// src/pages/blog/[...slug].astro
import { getCollection, render } 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 render(post);
---
```
## File Organization
```
src/
├── content.config.ts # Required: collection schemas (Astro 5+)
├── content/
│ ├── blog/
│ │ ├── post-1.md
│ │ └── post-2.mdx
│ └── authors/
│ └── john.json
```
rules/server-islands.rule.md
---
description: Rules for server islands with deferred rendering
globs:
- "**/*.astro"
- "astro.config.mjs"
---
# Server Islands Rules
## When to Use `server:defer`
Use server islands when a component needs server-side data (cookies, DB, APIs) but the surrounding page can be static or cached.
```astro
<!-- Personalized component on an otherwise static page -->
<UserGreeting server:defer>
<p slot="fallback">Welcome!</p>
</UserGreeting>
```
## MUST DO
- Always provide meaningful `slot="fallback"` content to prevent layout shift
- Keep props small and serializable (pass IDs, not full objects)
- Install an adapter — server islands require on-demand rendering
- Use `Referer` header to access the parent page URL inside the island
- Set `Cache-Control` headers on islands that don't change per-user
## MUST NOT DO
- Pass functions or class instances as props — they can't be serialized
- Rely on `Astro.url` for the page URL — it returns the island's internal route
- Use `server:defer` on components that don't need server data — use static rendering instead
- Pass large data objects as props — exceeding ~2048 bytes in URL forces POST (uncacheable)
- Forget to handle the loading state — always provide fallback content
## Decision: Server Island vs Client Island
```
Does the component need dynamic SERVER data (cookies, DB, personalization)?
├── Yes → server:defer (Server Island)
└── No → Does it need browser interactivity (clicks, state)?
├── Yes → client:load/idle/visible (Client Island)
└── No → No directive (Static HTML)
```
rules/sessions.rule.md
---
description: Rules for server-side sessions
globs:
- "src/pages/**/*"
- "src/actions/**/*"
- "src/middleware.ts"
- "astro.config.mjs"
---
# Sessions Rules
## When to Use Sessions
Use sessions for server-side state that persists across requests: user data, shopping carts, form state, preferences. Sessions are more secure and flexible than cookies for storing structured data.
## MUST DO
- Configure a storage driver in `astro.config.mjs` (or use an adapter that provides one)
- Use optional chaining: `Astro.session?.get()` — session may be undefined
- Call `session.regenerate()` after login to prevent session fixation
- Call `session.destroy()` on logout to clean up server data
- Type session data via `App.SessionData` in `src/env.d.ts`
- Use sessions only in on-demand rendered pages (`prerender = false`)
## MUST NOT DO
- Access `Astro.session` in prerendered/static pages — sessions require a server
- Store large blobs in sessions — keep data small, store references/IDs
- Use sessions in edge middleware — not supported
- Forget to handle undefined session (when driver isn't configured)
- Store sensitive secrets directly — sessions are server-side but still use reasonable data hygiene
## Pattern: Session with Auth
```astro
---
export const prerender = false;
const user = await Astro.session?.get('user');
if (!user) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
```
SKILL.md
---
name: astro-framework
description: Astro framework specialist for building fast, content-driven websites with islands architecture. Use when creating Astro components, configuring hydration (client:load/idle/visible/media), using server:defer (server islands), Content Layer API (glob/file loaders, live loaders), sessions, astro:env, i18n routing, actions, SSR adapters, view transitions, or integrating React/Vue/Svelte/Solid. Not for full-SPA frameworks (Next.js, Remix, SvelteKit).
license: MIT
metadata:
author: delineas
version: "2.0.0"
category: framework
tags: astro, islands, ssr, ssg, content-collections, content-layer, view-transitions, server-islands, sessions, i18n, actions, astro-env
---
# Astro Framework Specialist
Senior Astro specialist with deep expertise in islands architecture, content-driven websites, and hybrid rendering strategies.
## Role Definition
You are a senior frontend engineer with extensive Astro experience. You specialize in building fast, content-focused websites using Astro's islands architecture, content collections, and hybrid rendering. You understand when to ship JavaScript and when to keep things static.
## When to Use This Skill
Activate this skill when:
- Building content-driven websites (blogs, docs, marketing sites)
- Implementing islands architecture with selective hydration
- Using server islands (`server:defer`) for deferred server rendering
- Creating content collections with the Content Layer API (loaders, glob, file)
- Setting up SSR with adapters (Node, Vercel, Netlify, Cloudflare)
- Building API endpoints and server actions
- Implementing view transitions for SPA-like navigation
- Managing server-side sessions for user state
- Configuring type-safe environment variables with `astro:env`
- Setting up i18n routing for multilingual sites
- Integrating UI frameworks (React, Vue, Svelte, Solid)
- Optimizing images and performance
- Configuring `astro.config.mjs`
- Building live data collections with Live Loaders
## Core Workflow
1. **Analyze requirements** → Identify static vs dynamic content, hydration needs, data sources
2. **Design structure** → Plan pages, layouts, components, content collections with loaders
3. **Implement components** → Create Astro components with proper client/server directives
4. **Configure routing** → Set up file-based routing, dynamic routes, endpoints, i18n
5. **Optimize delivery** → Configure adapters, image optimization, view transitions, caching
## Expert Decision Frameworks
### Output Mode Selection
```
static (default)
├── Blog, docs, landing pages, portfolios
├── Content changes per-deploy, not per-request
├── <500 pages and builds under 5 min
└── No user-specific content needed
hybrid (80% of real-world projects)
├── Mostly static + login/dashboard/API routes
├── E-commerce: static catalog + dynamic cart/checkout
├── Use server islands to avoid making whole pages SSR
└── Best balance of performance + flexibility
server (rarely needed)
├── >80% of pages need request data (cookies, headers, DB)
├── Full SaaS/dashboard behind auth
└── Warning: you lose edge HTML caching on all pages
```
**Signs you picked wrong:**
- Builds >10 min with `getStaticPaths` → switch to `hybrid`
- Using `prerender = false` on >50% of pages → switch to `server`
- Whole app is `server` but only 2 pages read cookies → switch to `hybrid`
### Hydration Strategy — Common Mistakes
- **`client:visible` on hero/header** → It's already in viewport at load time, so it hydrates immediately anyway. Use `client:load` directly and skip the IntersectionObserver overhead.
- **`client:idle` on mobile** → `requestIdleCallback` on low-RAM devices can take 10+ seconds. For anything the user might interact with in the first 5 seconds, use `client:load`.
- **Large React component with `client:load`** → If bundle >50KB, consider splitting: render the static shell in Astro, hydrate only the interactive part. Or use `client:idle` if it's below the fold.
- **Hydrating navbars/footers** → If the only interactivity is a mobile menu toggle, write it in vanilla JS inside a `<script>` tag instead of hydrating an entire React component.
### Server Islands vs Client Islands vs Static
```
Does the component need data from the server on EACH request?
(cookies, user session, DB query, personalization)
│
├── Yes → server:defer (Server Island)
│ ├── User avatars, greeting bars, cart counts
│ ├── Personalized recommendations on product pages
│ └── A/B test variants resolved server-side
│
└── No → Does it need browser interactivity?
│
├── Yes → client:* directive (Client Island)
│ ├── Search boxes, forms with validation
│ ├── Image carousels, interactive charts
│ └── Anything needing onClick/onChange/state
│
└── No → No directive (Static HTML, zero JS)
├── Navigation, footers, content sections
├── Cards, lists, formatted text
└── This should be ~90% of most sites
```
**The e-commerce pattern:** Product page is static (title, images, description) + `server:defer` for price/stock (changes often) + `client:load` for add-to-cart button (needs interactivity). Three rendering strategies on one page.
### When NOT to Use Astro
Astro excels at content-heavy sites with islands of interactivity. Consider other frameworks when:
- The app is a full SPA with client-side routing and heavy state (→ Next.js, SvelteKit, Remix)
- Real-time collaborative features are core (→ Next.js + WebSockets)
- Every page is behind auth with no public content (→ SPA framework)
- You need React Server Components (→ Next.js)
### Content Collections — Loader Selection
```
Local markdown/MDX files → glob() loader
Single JSON/YAML data file → file() loader
Remote API/CMS data at build time → Custom async loader function
Remote data that must be fresh per-request → Live Loader (Astro 6+)
```
**Performance tip:** For sites with >1000 content entries, use `glob()` with `retainBody: false` if you don't need raw markdown body — significantly reduces data store size.
## Reference Documentation
Load detailed guidance based on your current task:
| Topic | Reference | When to Load |
|-------|-----------|--------------|
| Components | [references/components.md](references/components.md) | Writing Astro components, Props, slots, expressions |
| Client Directives | [references/client-directives.md](references/client-directives.md) | Hydration strategies, `client:load`, `client:visible`, `client:idle` |
| Content Collections | [references/content-collections.md](references/content-collections.md) | Content Layer API, loaders, schemas, `getCollection`, `getEntry`, live loaders |
| Routing | [references/routing.md](references/routing.md) | Pages, dynamic routes, endpoints, redirects |
| SSR & Adapters | [references/ssr-adapters.md](references/ssr-adapters.md) | On-demand rendering, adapters, server islands, sessions |
| Server Islands | [references/server-islands.md](references/server-islands.md) | `server:defer`, fallback content, deferred rendering |
| Sessions | [references/sessions.md](references/sessions.md) | `Astro.session`, server-side state, shopping carts |
| View Transitions | [references/view-transitions.md](references/view-transitions.md) | ClientRouter, animations, transition directives |
| Actions | [references/actions.md](references/actions.md) | Form handling, `defineAction`, validation |
| Middleware | [references/middleware.md](references/middleware.md) | `onRequest`, sequence, `context.locals` |
| Styling | [references/styling.md](references/styling.md) | Scoped CSS, global styles, `class:list` |
| Images | [references/images.md](references/images.md) | `<Image />`, `<Picture />`, optimization |
| Configuration | [references/configuration.md](references/configuration.md) | `astro.config.mjs`, TypeScript, env variables |
| Environment Variables | [references/environment-variables.md](references/environment-variables.md) | `astro:env`, `envField`, type-safe env schema |
| i18n Routing | [references/i18n-routing.md](references/i18n-routing.md) | Multilingual sites, locales, `astro:i18n` helpers |
## Guidelines by Context
Context-specific rules are available in the `rules/` directory:
- `rules/astro-components.rule.md` → Component structure patterns
- `rules/client-hydration.rule.md` → Hydration strategy decisions
- `rules/content-collections.rule.md` → Collection schema best practices (Content Layer API)
- `rules/astro-routing.rule.md` → Routing patterns and dynamic routes
- `rules/astro-ssr.rule.md` → SSR configuration and adapters
- `rules/astro-images.rule.md` → Image optimization patterns
- `rules/astro-typescript.rule.md` → TypeScript configuration
- `rules/server-islands.rule.md` → Server island patterns and `server:defer`
- `rules/sessions.rule.md` → Server-side session management
## Critical Rules
### MUST DO
- Use islands architecture—only hydrate interactive components
- Choose appropriate client directives based on interaction needs
- Use `server:defer` for personalized/dynamic content on static pages
- Define content collection schemas with Zod for type safety
- Use Content Layer API with loaders (`glob`, `file`) in `src/content.config.ts`
- Import Zod from `astro/zod` and render from `astro:content` (Astro 5+)
- Use `<Image />` and `<Picture />` for optimized images
- Implement proper error boundaries for client components
- Use TypeScript with strict mode for type safety
- Configure appropriate adapter for deployment target
- Use `Astro.props` for component data passing
- Use `astro:env` schema for type-safe environment variables
- Use `Astro.session` for server-side state management
### MUST NOT DO
- Hydrate components that don't need interactivity (use `client:` only when necessary)
- Use `client:only` without specifying the framework
- Import images with string paths (use import statements)
- Skip schema validation in content collections
- Mix `server` and `hybrid` output modes incorrectly
- Access `Astro.request` in prerendered pages
- Use browser APIs in component frontmatter (server-side code)
- Forget to install adapters for SSR deployment
- Pass functions as props to `server:defer` components (not serializable)
- Access `Astro.session` in prerendered pages (requires on-demand rendering)
- Use `src/content/config.ts` for new projects (use `src/content.config.ts` with loaders)
## Quick Reference
### Component Structure
```astro
---
// Component Script (runs on server)
interface Props {
title: string;
count?: number;
}
const { title, count = 0 } = Astro.props;
const data = await fetch('https://api.example.com/data');
---
<!-- Component Template -->
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
</div>
<style>
/* Scoped by default */
h1 { color: navy; }
</style>
```
### Directive Priority
1. **No directive** → Static HTML, zero JavaScript
2. **`server:defer`** → Deferred server rendering (server island)
3. **`client:load`** → Hydrate immediately on page load
4. **`client:idle`** → Hydrate when browser is idle
5. **`client:visible`** → Hydrate when component enters viewport
6. **`client:media`** → Hydrate when media query matches
7. **`client:only`** → Skip SSR, render only on client
### Content Collection Schema (Astro 5+)
```typescript
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
draft: z.boolean().default(false),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { blog };
```
### Server Island
```astro
---
import UserAvatar from '../components/UserAvatar.astro';
---
<UserAvatar server:defer>
<img slot="fallback" src="/generic-avatar.svg" alt="Loading..." />
</UserAvatar>
```
## Output Format
When implementing Astro features, provide:
1. Component file (`.astro` with frontmatter and template)
2. Configuration updates (`astro.config.mjs` if needed)
3. Content collection schema (if using collections)
4. TypeScript types (for Props and data)
5. Brief explanation of hydration strategy chosen
## Technologies
Astro 5+/6+, Islands Architecture, Content Layer API (glob/file loaders, live loaders), Zod Schemas, View Transitions API, Server Islands (`server:defer`), Sessions, Actions, Middleware, astro:env (type-safe environment variables), i18n Routing, Adapters (Node, Vercel, Netlify, Cloudflare, Deno), React/Vue/Svelte/Solid integrations, Image Optimization, MDX, Markdoc, TypeScript, Scoped CSS, Tailwind CSS