git:20260320.766fb9e to git:20260906.ae0cc61

182 added, 589 removed. Audit B to C.

---
name: web-meta-framework-astro
description: Astro content-first framework - islands architecture, content collections, file-based routing, SSR/SSG hybrid rendering, View Transitions, server islands, multi-framework component support
---
- # Astro Framework Patterns
+ # Astro Patterns
- > **Quick Guide:** Astro renders pages to static HTML by default with zero client-side JavaScript. Use `.astro` components for all static content, add `client:*` directives only on interactive framework components (React/Vue/Svelte). Use content collections for type-safe structured content. Choose between static (default) and on-demand (SSR) rendering per-page with `export const prerender`.
+ > **Quick Guide:** Astro renders pages to static HTML with zero client JavaScript by default. `.astro`
+ > components carry static content; framework components become interactive islands only when given a
+ > `client:*` directive. Content collections give structured Markdown type-safe frontmatter through Zod
+ > schemas. Rendering mode is per page: static unless `export const prerender = false`. Astro 6 replaced
+ > `<ViewTransitions />` with `<ClientRouter />`, replaced `Astro.glob()` with `import.meta.glob()`, moved
+ > `z` from `astro:content` to `astro/zod` (now Zod 4), requires string `getStaticPaths()` params, and
+ > requires Node 22.12.0+.
+ **Detailed Resources:**
+
+ - [examples/core.md](examples/core.md) — typed props, expressions, nested layouts, scoped vs global styles, script handling
+ - [examples/islands.md](examples/islands.md) — directive selection, `client:only`, multi-framework islands, server islands, cross-island state
+ - [examples/content.md](examples/content.md) — collection schemas, querying, rendering, references, custom loaders, live collections
+ - [examples/routing.md](examples/routing.md) — static and dynamic routes, rest params, pagination, on-demand routes, endpoints, 404
+ - [examples/integrations.md](examples/integrations.md) — framework integrations, View Transitions, persistence, animations, Starlight
+ - [reference.md](reference.md) — project layout, route priority, collection API, adapters, rendering checklist
+
---
- <critical_requirements>
+ ## Which path applies
- ## CRITICAL: Before Using This Skill
+ - **Every page is static** — the default. No `output` setting and no adapter; dynamic routes need
+ `getStaticPaths()`. Follow [examples/routing.md](examples/routing.md).
+ - **A few pages need request-time data** — keep the default, add `export const prerender = false` to
+ those pages, and install a server adapter. Same file, plus the adapter table in [reference.md](reference.md).
+ - **Most pages need request-time data** — set `output: "server"` and opt individual pages back to
+ static with `export const prerender = true`.
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ---
- **(You MUST keep pages static by default - only add `export const prerender = false` when the page genuinely needs request-time data)**
+ <critical_requirements>
- **(You MUST use `client:*` directives on framework components that need interactivity - without a directive, components render to static HTML with zero JavaScript)**
+ ## Before writing Astro code
- **(You MUST define content collections in `src/content.config.ts` with Zod schemas for type-safe frontmatter)**
+ **Leave pages static and add `export const prerender = false` only where the page reads request-time
+ data.** Static is the default and is where the framework's speed comes from.
- **(You MUST use `<ClientRouter />` from `astro:transitions` for View Transitions - the old `<ViewTransitions />` component is removed in Astro 6)**
+ **Install a server adapter before any page opts out of prerendering.** Without one, a build containing
+ `prerender = false` fails.
- **(You MUST install a server adapter (@astrojs/node, @astrojs/vercel, etc.) before using on-demand rendering)**
+ **Give a framework component a `client:*` directive when it needs to be interactive.** Without a
+ directive it renders to static HTML and its event handlers never run.
- **(You MUST use `getStaticPaths()` for dynamic routes in static mode - it is not needed for on-demand (SSR) routes)**
+ **Define collections in `src/content.config.ts` with a Zod schema.** The schema validates frontmatter at
+ build time and generates the types the queries return.
+ **Use `getStaticPaths()` for dynamic routes in static mode.** On-demand routes read `Astro.params`
+ directly and need no such export.
+
</critical_requirements>
---
**Auto-detection:** Astro, .astro files, astro.config, islands architecture, client:load, client:visible, client:idle, client:only, client:media, server:defer, content collections, defineCollection, defineLiveCollection, getCollection, getLiveCollection, getEntry, getLiveEntry, render, astro:content, astro:transitions, ClientRouter, getStaticPaths, Astro.props, Astro.params, Astro.cookies, Astro.redirect, prerender, astro add, @astrojs/react, @astrojs/vue, @astrojs/svelte, Starlight
- **When to use:**
+ **Applies to:**
- - Building content-driven websites (blogs, docs, marketing, portfolios)
- - Sites where most pages are static with selective interactivity (islands)
- - Projects using content collections for structured Markdown/MDX/YAML content
- - Multi-framework projects mixing React, Vue, Svelte, or Solid components
- - Sites needing hybrid rendering (static pages + some server-rendered pages)
+ - `.astro` component syntax — frontmatter, template expressions, slots, layouts, scoped styles
+ - Islands: client directives, hydration timing, server islands, mixing UI frameworks on one page
+ - Content collections: schemas, loaders, querying, rendering, cross-collection references
+ - File-based routing: static, dynamic, rest parameters, pagination, API endpoints
+ - Rendering mode: static, on-demand, and per-page `prerender` control
+ - View Transitions: `<ClientRouter />`, transition directives, persisted elements
- **When NOT to use:**
+ **Handled elsewhere:**
- - Highly interactive web applications (dashboards, real-time collaboration) - use a full-stack SSR framework or SPA
- - Apps where every page requires user authentication and dynamic data - use a full-stack SSR framework
- - Projects that need React Server Components or Server Actions - use a React SSR framework
+ - What an island renders internally — a hydrated component follows the conventions of whichever UI
+ library wrote it, and Astro settles only the boundary.
+ - Which CSS approach fills a `<style>` block — the scoping rules here are Astro's; the styling system is not.
+ - Which store library fills the cross-island seam — that islands share no tree, and that a module-scoped store is the way across, is settled in [examples/islands.md](examples/islands.md); which store you reach for is not.
+ - Fully interactive applications where every route is user-specific — that shape wants a framework
+ whose default is on-demand rendering rather than one whose default is static output.
- **Key patterns covered:**
+ ---
- - Astro component syntax (.astro files, frontmatter, template expressions, slots)
- - Islands architecture (client directives, server islands, selective hydration)
- - Content collections (schemas, querying, rendering, references, live collections)
- - File-based routing (static routes, dynamic routes, rest parameters, pagination)
- - Rendering modes (static, on-demand/SSR, hybrid with prerender control)
- - View Transitions (ClientRouter, transition directives, persist state)
- - Framework integrations (React, Vue, Svelte, Solid islands)
+ <philosophy>
- **Detailed Resources:**
+ Astro is a **content-first framework that ships zero JavaScript by default**. Most of a page is static
+ HTML; small interactive "islands" hydrate independently, each paying only for itself.
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
+ Four consequences follow, and they explain most of the API:
- **Core patterns:**
+ 1. **A component is static until told otherwise** — `client:*` is the opt-in, so the cost of
+ interactivity is always visible at the call site.
+ 2. **The UI library is a detail** — React, Vue, Svelte, Solid and Preact components are all just island
+ contents, and one page can carry several.
+ 3. **Content is typed data** — collections put a Zod schema between Markdown frontmatter and the code
+ that reads it, so a typo in a post fails the build rather than the page.
+ 4. **Rendering mode is per page** — static and on-demand pages coexist in one project, which is why
+ `prerender` is an export rather than a global setting.
- - [examples/core.md](examples/core.md) - Astro components, props, slots, expressions, layouts
- - [examples/islands.md](examples/islands.md) - Client directives, server islands, multi-framework islands
- - [examples/content.md](examples/content.md) - Content collections, schemas, querying, rendering
- - [examples/routing.md](examples/routing.md) - File-based routing, dynamic routes, SSR/SSG modes
- - [examples/integrations.md](examples/integrations.md) - React/Vue/Svelte islands, View Transitions
+ </philosophy>
---
- <philosophy>
-
- ## Philosophy
-
- Astro is a **content-first web framework** that ships zero JavaScript by default. It pioneered the **islands architecture** where most of the page is fast static HTML, with small interactive "islands" of JavaScript hydrated only where needed.
-
- **Core principles:**
-
- 1. **Content-first** - Optimized for content-driven sites (blogs, docs, marketing, e-commerce)
- 2. **Zero JS by default** - Components render to static HTML unless explicitly hydrated
- 3. **Islands architecture** - Interactive components hydrate independently, reducing JavaScript payloads
- 4. **UI-agnostic** - Use React, Vue, Svelte, Solid, Preact, or plain Astro components
- 5. **File-based routing** - `src/pages/` directory structure maps directly to URLs
- 6. **Type-safe content** - Content collections with Zod schemas enforce structure and provide TypeScript types
- 7. **Hybrid rendering** - Mix static (SSG) and on-demand (SSR) pages in the same project
+ <decision_framework>
- **When to use Astro:**
+ **Static or on-demand?** A page needs on-demand rendering when it reads cookies, headers or
+ user-specific data at request time, or when its data changes faster than you are willing to rebuild.
+ Everything else is static — including data that changes hourly, which a rebuild handles more cheaply
+ than an adapter does.
- - Content-driven websites (blogs, documentation, portfolios, marketing)
- - Sites with mostly static content and occasional interactivity
- - Documentation sites (Starlight integration)
- - E-commerce product pages with interactive carts
- - Multi-framework projects where teams use different UI libraries
+ **`.astro` or a framework component?** No client-side interactivity means `.astro` — zero JavaScript.
+ Simple interactivity (a toggle, a show/hide) is `.astro` plus a `<script>` tag, which is lighter than a
+ framework. Reach for a framework component when the interaction needs that framework's own state model.
- **When NOT to use Astro:**
+ **Which client directive?** The table in Pattern 4 maps each one to what it is for. `client:only` is
+ the last resort, for a component that cannot server-render at all.
- - Fully interactive web applications (use a full-stack SSR framework or SPA)
- - Real-time collaborative apps (use a dedicated SPA with WebSocket support)
- - Projects requiring React Server Components or Server Actions (use a React SSR framework)
+ **Build-time or live collection?** `defineCollection` for anything that is the same on every request —
+ posts, docs, changelogs, author bios. `defineLiveCollection` only when the data must be fresh per
+ request (inventory, pricing), which also forces that page into on-demand rendering.
- </philosophy>
+ </decision_framework>
---
<patterns>
- ## Core Patterns
-
- ### Pattern 1: Astro Component Structure
+ ## Core patterns
- Astro components (`.astro` files) have two parts: a frontmatter script block (between `---` fences) and an HTML template.
+ ### Pattern 1: Astro component structure
- #### Component Anatomy
+ An `.astro` file is a server-only script between `---` fences followed by an HTML template. The script
+ never reaches the browser, so data fetching belongs there.
```astro
---
- // Component Script (frontmatter) - runs on the server only
- import Layout from "../layouts/Layout.astro";
- import { getCollection } from "astro:content";
-
- // Props accessed via Astro.props
- interface Props {
- title: string;
- description?: string;
- }
-
- const { title, description = "Default description" } = Astro.props;
-
- // Server-side data fetching
+ interface Props { title: string; description?: string }
+ const { title, description = "Default" } = Astro.props;
const posts = await getCollection("blog");
---
- <!-- Component Template - HTML with expressions -->
- <Layout title={title}>
- <h1>{title}</h1>
- <p>{description}</p>
-
- <ul>
- {posts.map((post) => (
- <li>
- <a href={`/blog/${post.id}`}>{post.data.title}</a>
- </li>
- ))}
- </ul>
- </Layout>
+ <h1>{title}</h1>
+ {posts.map((post) => <a href={`/blog/${post.id}`}>{post.data.title}</a>)}
- <style>
- /* Scoped to this component by default */
- h1 {
- color: navy;
- font-size: 2rem;
- }
- </style>
+ <style>h1 { color: navy; }</style>
```
- **Why good:** Frontmatter runs server-only (no JavaScript shipped), type-safe props with interface, scoped styles prevent leakage, expressions use JSX-like syntax
-
- ---
+ Styles in a `<style>` block are scoped to the component unless marked `is:global`.
- ### Pattern 2: Slots for Composition
+ Full code: [examples/core.md](examples/core.md)
- Slots allow parent components to inject content into child component templates.
+ ### Pattern 2: Slots for composition
- #### Default and Named Slots
+ A `<slot />` receives content from the parent. Named slots take a matching `slot="name"` attribute, and
+ anything inside the `<slot>` element is fallback content when nothing is passed.
```astro
- ---
- // src/components/Card.astro
- interface Props {
- title: string;
- }
-
- const { title } = Astro.props;
- ---
-
- <article class="card">
- <header>
- <slot name="header">
- <h2>{title}</h2>
- </slot>
- </header>
-
- <div class="body">
- <slot /> <!-- Default slot -->
- </div>
-
- <footer>
- <slot name="footer">
- <p>Default footer</p>
- </slot>
- </footer>
+ <article>
+ <header><slot name="header"><h2>{title}</h2></slot></header>
+ <slot />
+ <footer><slot name="footer" /></footer>
</article>
```
- ```astro
- ---
- // Usage in a page
- import Card from "../components/Card.astro";
- ---
-
- <Card title="My Card">
- <span slot="header"><h2>Custom Header</h2></span>
-
- <p>This goes in the default slot.</p>
-
- <div slot="footer">
- <a href="/more">Read more</a>
- </div>
- </Card>
- ```
-
- **Why good:** Named slots provide flexible composition, fallback content renders when no slot content is provided, matches Web Component slot semantics
-
- ---
+ Full code: [examples/core.md](examples/core.md)
### Pattern 3: Layouts
- Layouts are Astro components that wrap page content with shared UI (header, footer, navigation).
-
- #### Base Layout with Metadata
+ A layout is an ordinary component that wraps page content through its default slot, and layouts nest —
+ a blog layout wrapping a base layout wrapping the page.
```astro
---
- // src/layouts/BaseLayout.astro
- interface Props {
- title: string;
- description?: string;
- }
-
- const { title, description = "My Astro Site" } = Astro.props;
+ const { title } = Astro.props;
---
-
- <!doctype html>
<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <meta name="description" content={description} />
- <title>{title}</title>
- </head>
- <body>
- <nav>
- <a href="/">Home</a>
- <a href="/blog">Blog</a>
- <a href="/about">About</a>
- </nav>
-
- <main>
- <slot />
- </main>
-
- <footer>
- <p>&copy; {new Date().getFullYear()} My Site</p>
- </footer>
- </body>
+ <head><title>{title}</title></head>
+ <body><main><slot /></main></body>
</html>
```
- ```astro
- ---
- // src/pages/about.astro
- import BaseLayout from "../layouts/BaseLayout.astro";
- ---
-
- <BaseLayout title="About Us" description="Learn about our team">
- <h1>About Us</h1>
- <p>We build great things with Astro.</p>
- </BaseLayout>
- ```
-
- **Why good:** Shared layout reduces duplication, metadata props ensure SEO per-page, slot injects page content
-
- ---
-
- ### Pattern 4: Islands Architecture (Client Directives)
+ Full code: [examples/core.md](examples/core.md)
- By default, framework components (React, Vue, Svelte) render to static HTML. Add a `client:*` directive to hydrate them as interactive islands.
+ ### Pattern 4: Islands and client directives
- #### Client Directive Reference
+ Framework components render to static HTML until a `client:*` directive hydrates them. The directive
+ chooses _when_ hydration happens.
- | Directive | When it hydrates | Use for |
- | ---------------- | ------------------------------------ | ---------------------------- |
- | `client:load` | Immediately on page load | Critical interactive UI |
- | `client:idle` | When browser is idle | Lower-priority interactivity |
- | `client:visible` | When component scrolls into viewport | Below-the-fold content |
- | `client:media` | When media query matches | Responsive interactivity |
- | `client:only` | Skips server render, client-only | Browser-dependent components |
+ | Directive | Hydrates | Use for |
+ | ---------------- | --------------------------------------- | ---------------------------- |
+ | `client:load` | Immediately on page load | Critical interactive UI |
+ | `client:idle` | When the browser is idle | Lower-priority interactivity |
+ | `client:visible` | When it scrolls into the viewport | Below-the-fold content |
+ | `client:media` | When a media query matches | Responsive interactivity |
+ | `client:only` | Client only, skipping the server render | Browser-dependent components |
```astro
- ---
- // src/pages/index.astro
- import Header from "../components/Header.astro"; // Static - no JS
- import SearchBar from "../components/SearchBar"; // React component
- import Newsletter from "../components/Newsletter"; // React component
- import Comments from "../components/Comments"; // React component
- import Analytics from "../components/Analytics"; // React component
- ---
-
- <Header />
-
- <!-- Hydrate immediately - user interacts with search right away -->
+ <Header /> <!-- static, zero JS -->
<SearchBar client:load />
-
- <!-- Hydrate when browser is idle - not urgent -->
- <Newsletter client:idle />
-
- <!-- Hydrate only when scrolled into view -->
<Comments client:visible />
-
- <!-- Hydrate only on mobile screens -->
<Analytics client:media="(max-width: 768px)" />
```
- **Why good:** Only interactive components ship JavaScript, hydration is deferred to optimal timing, static components render as zero-JS HTML
+ Full code: [examples/islands.md](examples/islands.md)
- #### Server Islands
+ ### Pattern 5: Server islands
- Server islands defer rendering of dynamic content on the server, allowing static shell to cache while dynamic parts render per-request.
+ `server:defer` renders a component per request while the rest of the page stays cacheable. The
+ `fallback` slot holds its place until it arrives.
```astro
- ---
- // src/pages/product.astro
- import ProductInfo from "../components/ProductInfo.astro"; // Static
- import UserReviews from "../components/UserReviews.astro"; // Dynamic
- ---
-
- <!-- Static product info - cached -->
<ProductInfo product={product} />
- <!-- Server island - rendered per-request, placeholder shown while loading -->
<UserReviews server:defer>
<div slot="fallback">Loading reviews...</div>
</UserReviews>
```
- **Why good:** Static page shell caches and serves instantly, dynamic portions render per-request without blocking the page, fallback content prevents layout shift
-
- ---
-
- ### Pattern 5: Content Collections
+ Full code: [examples/islands.md](examples/islands.md)
- Content collections provide type-safe management of structured content (blog posts, docs, product data).
+ ### Pattern 6: Content collections
- #### Defining Collections
+ A collection pairs a loader with a Zod schema. `getCollection` queries it with an optional filter, and
+ `render()` turns an entry's Markdown into a component.
```typescript
// src/content.config.ts
- import { defineCollection } from "astro:content";
- import { glob, file } from "astro/loaders";
- import { z } from "astro/zod";
-
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
- description: z.string(),
pubDate: z.coerce.date(),
- updatedDate: z.coerce.date().optional(),
- heroImage: z.string().optional(),
draft: z.boolean().default(false),
- tags: z.array(z.string()).default([]),
}),
});
-
- const authors = defineCollection({
- loader: file("src/data/authors.json"),
- schema: z.object({
- name: z.string(),
- bio: z.string(),
- avatar: z.string(),
- }),
- });
-
- export const collections = { blog, authors };
- ```
-
- #### Querying and Rendering Collections
-
- ```astro
- ---
- // src/pages/blog/index.astro
- import { getCollection } from "astro:content";
- import BaseLayout from "../../layouts/BaseLayout.astro";
-
- const allPosts = await getCollection("blog", ({ data }) => {
- return data.draft !== true; // Filter out drafts
- });
-
- // Sort by date descending
- const sortedPosts = allPosts.sort(
- (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
- );
- ---
-
- <BaseLayout title="Blog">
- <h1>Blog</h1>
- <ul>
- {sortedPosts.map((post) => (
- <li>
- <a href={`/blog/${post.id}`}>
- <h2>{post.data.title}</h2>
- <time datetime={post.data.pubDate.toISOString()}>
- {post.data.pubDate.toLocaleDateString()}
- </time>
- </a>
- </li>
- ))}
- </ul>
- </BaseLayout>
+ export const collections = { blog };
```
```astro
- ---
- // src/pages/blog/[id].astro
- import { getCollection, getEntry, render } from "astro:content";
- import BaseLayout from "../../layouts/BaseLayout.astro";
-
- export async function getStaticPaths() {
- const posts = await getCollection("blog");
- return posts.map((post) => ({
- params: { id: post.id },
- props: { post },
- }));
- }
-
- const { post } = Astro.props;
- const { Content } = await render(post);
- ---
-
- <BaseLayout title={post.data.title}>
- <article>
- <h1>{post.data.title}</h1>
- <time datetime={post.data.pubDate.toISOString()}>
- {post.data.pubDate.toLocaleDateString()}
- </time>
- <Content />
- </article>
- </BaseLayout>
+ const posts = await getCollection("blog", ({ data }) => !data.draft);
+ const { Content, headings } = await render(post);
```
- **Why good:** Zod schemas validate frontmatter at build time, TypeScript types are auto-generated, querying with filters is type-safe, `render()` converts Markdown to a component
-
- #### Live Collections (Astro 6+)
-
- For data that changes frequently and needs to be fresh on every request, use `defineLiveCollection` in a separate `src/live.config.ts` file (not `src/content.config.ts`). Query with `getLiveCollection()` and `getLiveEntry()` instead of `getCollection()`. Requires SSR (`prerender = false`). See [examples/content.md](examples/content.md) for full examples.
-
- ---
+ Live collections are a separate mechanism: `defineLiveCollection` in `src/live.config.ts` (not
+ `content.config.ts`), queried with `getLiveCollection()` / `getLiveEntry()`, and only from on-demand
+ pages.
- ### Pattern 6: File-Based Routing
+ Full code: [examples/content.md](examples/content.md)
- The `src/pages/` directory structure directly maps to URL paths.
+ ### Pattern 7: File-based routing
- #### Static Routes
+ `src/pages/` maps to URLs. `[param]` is a single dynamic segment, `[...rest]` matches any depth, and a
+ leading underscore excludes a file from routing.
```
src/pages/
├── index.astro → /
- ├── about.astro → /about
- ├── blog/
- │ ├── index.astro → /blog
- │ └── [id].astro → /blog/:id (dynamic)
- ├── docs/
- │ └── [...slug].astro → /docs/* (rest parameter)
- └── _helpers.ts → excluded (underscore prefix)
- ```
-
- #### Dynamic Routes with getStaticPaths
-
- ```astro
- ---
- // src/pages/tags/[tag].astro
- import { getCollection } from "astro:content";
-
- export async function getStaticPaths() {
- const posts = await getCollection("blog");
- const tags = [...new Set(posts.flatMap((post) => post.data.tags))];
-
- return tags.map((tag) => ({
- params: { tag },
- props: {
- posts: posts.filter((post) => post.data.tags.includes(tag)),
- },
- }));
- }
-
- const { tag } = Astro.params;
- const { posts } = Astro.props;
- ---
-
- <h1>Posts tagged: {tag}</h1>
- <ul>
- {posts.map((post) => (
- <li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
- ))}
- </ul>
- ```
-
- #### Rest Parameters
-
- ```astro
- ---
- // src/pages/docs/[...slug].astro
- export async function getStaticPaths() {
- return [
- { params: { slug: "getting-started" } },
- { params: { slug: "guides/installation" } },
- { params: { slug: "api/reference" } },
- { params: { slug: undefined } }, // matches /docs
- ];
- }
-
- const { slug } = Astro.params;
- ---
-
- <h1>Docs: {slug ?? "Index"}</h1>
- ```
-
- **Why good:** File structure mirrors URL structure, dynamic params are type-safe via getStaticPaths, rest parameters handle arbitrary depth
-
- ---
-
- ### Pattern 7: On-Demand Rendering (SSR)
-
- Opt individual pages into server-side rendering while keeping the rest static.
-
- #### Per-Page SSR
-
- ```astro
- ---
- // src/pages/dashboard.astro
- export const prerender = false; // Render on every request
-
- const cookie = Astro.cookies.get("session");
- if (!cookie) {
- return Astro.redirect("/login");
- }
-
- const user = await getUserFromSession(cookie.value);
- ---
-
- <h1>Welcome, {user.name}</h1>
+ ├── blog/[id].astro → /blog/:id
+ ├── docs/[...slug].astro → /docs and /docs/*
+ └── _helpers.ts → not a route
```
- #### Server Output Mode
-
- ```javascript
- // astro.config.mjs
- import { defineConfig } from "astro/config";
- import node from "@astrojs/node";
-
- export default defineConfig({
- output: "server", // All pages server-rendered by default
- adapter: node({ mode: "standalone" }),
- });
- ```
+ In static mode each dynamic route exports `getStaticPaths()` returning `{ params, props }` pairs;
+ `paginate()` inside it generates numbered pages.
- ```astro
- ---
- // src/pages/about.astro
- export const prerender = true; // Opt this page back to static
- ---
+ Full code: [examples/routing.md](examples/routing.md)
- <h1>About Us</h1>
- ```
+ ### Pattern 8: On-demand rendering and endpoints
- #### API Endpoints
+ `export const prerender = false` moves a page to request time, where `Astro.cookies`, `Astro.redirect()`
+ and a `Response` return value all become available. API endpoints are `.ts` files exporting one handler
+ per HTTP method, and they default to static like everything else.
```typescript
- // src/pages/api/search.ts
export const prerender = false;
- import type { APIRoute } from "astro";
-
export const GET: APIRoute = async ({ url }) => {
const query = url.searchParams.get("q");
- if (!query) {
+ if (!query)
return new Response(JSON.stringify({ error: "Missing query" }), {
status: 400,
- headers: { "Content-Type": "application/json" },
});
- }
-
- const results = await searchDatabase(query);
- return new Response(JSON.stringify(results), {
- headers: { "Content-Type": "application/json" },
- });
+ return new Response(JSON.stringify(await searchDatabase(query)));
};
```
- **Why good:** Static by default for performance, opt into SSR per-page as needed, API endpoints support full request/response control
-
- ---
-
- ### Pattern 8: View Transitions
-
- Astro provides smooth page transitions using the browser's View Transition API via the `<ClientRouter />` component.
-
- #### Enabling View Transitions
-
- ```astro
- ---
- // src/layouts/BaseLayout.astro
- import { ClientRouter } from "astro:transitions";
- ---
+ Full code: [examples/routing.md](examples/routing.md)
- <html lang="en">
- <head>
- <ClientRouter />
- </head>
- <body>
- <slot />
- </body>
- </html>
- ```
+ ### Pattern 9: View Transitions
- #### Transition Directives
+ `<ClientRouter />` in the document head enables cross-page transitions. `transition:name` pairs elements
+ so they morph, and `transition:persist` keeps an element alive across a navigation.
```astro
- ---
- // src/pages/blog/[id].astro
- ---
-
- <!-- Name pairs elements across pages for smooth morphing -->
- <img
- src={post.data.heroImage}
- transition:name={`hero-${post.id}`}
- transition:animate="slide"
- />
-
- <h1 transition:name={`title-${post.id}`}>
- {post.data.title}
- </h1>
+ <head><ClientRouter /></head>
- <!-- Persist interactive elements across navigations -->
- <audio controls transition:persist>
- <source src="/music.mp3" type="audio/mp3" />
- </audio>
+ <img src={post.data.heroImage} transition:name={`hero-${post.id}`} transition:animate="slide" />
+ <audio controls transition:persist><source src="/music.mp3" type="audio/mp3" /></audio>
```
- **Why good:** Smooth transitions between pages without SPA framework, paired elements morph naturally, persistent elements maintain state across navigation
+ Full code: [examples/integrations.md](examples/integrations.md)
</patterns>
---
<red_flags>
- ## RED FLAGS
-
- **High Priority Issues:**
-
- - **Adding `client:load` to every component** - Defeats islands architecture; only hydrate components that need interactivity
- - **Using framework components for static content** - Use `.astro` components for zero-JS static HTML
- - **Missing `getStaticPaths()` on dynamic routes in static mode** - Build will fail
- - **Using `<ViewTransitions />`** - Removed in Astro 6; use `<ClientRouter />` from `astro:transitions`
- - **No server adapter with `prerender = false`** - On-demand rendering requires an adapter
-
- **Medium Priority Issues:**
+ ## Red flags
- - **Fetching data in `<script>` tags instead of frontmatter** - Frontmatter runs server-side; use it for data fetching
- - **Using `client:only` when `client:load` would work** - `client:only` skips SSR, hurting SEO
- - **Missing Zod schema on content collections** - Loses type safety and build-time validation
- - **Using `output: 'server'` for mostly static sites** - Default static mode with per-page SSR opt-in is more performant
+ **Breaks at runtime:**
- **Gotchas & Edge Cases:**
+ - A dynamic route in static mode with no `getStaticPaths()` — the build fails with "getStaticPaths() is required".
+ - `prerender = false` with no server adapter installed — the build fails.
+ - `<ViewTransitions />` — removed in Astro 6; import `<ClientRouter />` from `astro:transitions`.
+ - `Astro.glob()` — removed in Astro 6; use `import.meta.glob()`.
+ - `import { z } from "astro:content"` — removed in Astro 6; import `z` from `astro/zod`, which is Zod 4, so `z.string().email()` is now `z.email()`.
+ - Numeric `getStaticPaths()` params — Astro 6 requires strings; wrap with `String(id)`.
+ - `astro.config.cjs` — Astro 6 requires ESM (`.mjs` or `.ts`).
+ - Functions, class instances or symbols passed as props to a hydrated component — island props must be serializable.
+ - `Astro.props` read outside the frontmatter fence, or an `.astro` component imported into a framework component — neither is available there.
+ - Astro 6 on Node below 22.12.0 — unsupported.
- - **Styles in `.astro` are scoped by default** - Use `<style is:global>` or `:global()` selector for global styles
- - **`client:visible` uses IntersectionObserver** - Component won't hydrate if always off-screen
- - **`Astro.redirect()` only works in on-demand rendered pages** - Static pages cannot redirect at request time
- - **Multiple framework islands share no state** - Each island is independent; use nanostores for cross-island communication
- - **`transition:persist` requires matching `transition:name`** - Elements must have the same name on both pages
- - **Astro 6 requires Node.js 22.12.0+** - Earlier Node versions are not supported
- - **`Astro.glob()` removed in Astro 6** - Use `import.meta.glob()` instead
- - **`z` from `astro:content` removed in Astro 6** - Import `z` from `astro/zod` instead
- - **Astro 6 uses Zod 4** - Some Zod 3 patterns changed (e.g., `z.string().email()` becomes `z.email()`)
- - **`getStaticPaths()` params must be strings** - Number params are no longer allowed in Astro 6
- - **Live collections use `src/live.config.ts`** - Not `src/content.config.ts` (separate config file)
- - **`import.meta.env` values are inlined at build time in Astro 6** - Use `process.env` for runtime secrets in live collections and SSR code
+ **Surprising behaviour:**
- For complete anti-patterns with code examples, see [reference.md](reference.md).
+ - `client:load` on everything defeats the architecture; a component with no interactivity wants no directive at all.
+ - `client:only` skips the server render, so nothing is in the HTML for crawlers — prefer `client:load` wherever the component can render on the server.
+ - Data fetched in a `<script>` tag creates a client waterfall; frontmatter runs server-side and has no such cost.
+ - A collection with no schema loses both build-time validation and generated types.
+ - `output: "server"` on a mostly-static site gives up the default's caching for pages that did not need it.
+ - Unfiltered `getCollection` returns drafts alongside published entries.
+ - `<style>` is scoped by default — reach for `<style is:global>` or the `:global()` selector deliberately.
+ - `<script>` is bundled and deduped; `is:inline` opts out, which is what makes a script re-run on every View Transitions navigation.
+ - `client:visible` waits on IntersectionObserver, so a component that is never on screen never hydrates.
+ - `Astro.redirect()` works only on on-demand pages; a static page cannot redirect at request time.
+ - Islands share no state — cross-island communication needs a store or events you bring.
+ - `transition:persist` needs the same `transition:name` on both pages, or the element is recreated.
+ - Collection entry ids come from filenames unless frontmatter overrides them.
+ - Live collections cannot render MDX, and `import.meta.env` is inlined at build time — read runtime secrets from `process.env`.
+ - A `[...slug]` route matches its own base path when a params entry passes `undefined`.
</red_flags>
-
- ---
-
- <integration>
-
- ## Integration Guide
-
- **Astro is the top-level framework.** It handles routing, rendering, and content management. UI framework components (React, Vue, Svelte) are used as islands within Astro pages.
-
- **Adding framework support:**
-
- ```bash
- # Add framework support via CLI
- npx astro add react
- npx astro add vue
-
- # Add multiple integrations at once
- npx astro add react sitemap
- ```
-
- **Key integrations:**
-
- - **UI Frameworks** - React, Vue, Svelte, Solid used as interactive islands within Astro pages
- - **MDX** - Via `@astrojs/mdx` for components in Markdown
- - **Starlight** - Astro's documentation theme, built on content collections
-
- **Deployment adapters:**
-
- - **@astrojs/vercel** - Vercel deployment with edge/serverless
- - **@astrojs/netlify** - Netlify Functions/Edge
- - **@astrojs/cloudflare** - Cloudflare Workers/Pages
- - **@astrojs/node** - Self-hosted Node.js server
-
- **Does NOT replace:**
-
- - Full-stack SSR frameworks for highly interactive, full-stack React applications
- - SPA frameworks for real-time, fully client-rendered apps
-
- </integration>
-
- ---
-
- <critical_reminders>
-
- ## CRITICAL REMINDERS
-
- > **All code must follow project conventions in CLAUDE.md**
-
- **(You MUST keep pages static by default - only add `export const prerender = false` when the page genuinely needs request-time data)**
-
- **(You MUST use `client:*` directives on framework components that need interactivity - without a directive, components render to static HTML with zero JavaScript)**
-
- **(You MUST define content collections in `src/content.config.ts` with Zod schemas for type-safe frontmatter)**
-
- **(You MUST use `<ClientRouter />` from `astro:transitions` for View Transitions - the old `<ViewTransitions />` component is removed in Astro 6)**
-
- **(You MUST install a server adapter (@astrojs/node, @astrojs/vercel, etc.) before using on-demand rendering)**
-
- **(You MUST use `getStaticPaths()` for dynamic routes in static mode - it is not needed for on-demand (SSR) routes)**
-
- **Failure to follow these rules will ship unnecessary JavaScript, break builds, cause missing pages, or produce type errors.**
-
- </critical_reminders>