Claude Skill

astro-ops

Astro framework patterns, islands architecture, content collections, rendering strategies, and deployment. Use for: astro, islands architecture, content collections, astro cloudflare, view transitions, partial hydration, astrojs, SSG, SSR, hybrid rendering, astro adapter.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_astro-ops-3dfaf0b.zip · 25 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/astro-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Astro Operations

Facts verified as of 2026-07.

Comprehensive patterns for Astro framework development: islands architecture, content collections, rendering strategies, view transitions, and multi-platform deployment.

Rendering Strategy Decision Tree

Which rendering strategy?
│
├─ Is content mostly static (blog, docs, marketing)?
│  ├─ YES → Does it change less than daily?
│  │  ├─ YES → SSG (output: 'static')
│  │  │        Fastest TTFB, CDN-cacheable, zero runtime cost
│  │  └─ NO  → Hybrid (output: 'hybrid')
│  │           Default static + opt-in SSR per route
│  └─ NO  → Does every page need personalization?
│     ├─ YES → SSR (output: 'server')
│     │        Dynamic per-request, auth-aware, real-time data
│     └─ NO  → Hybrid (output: 'hybrid')
│              Static shell + server islands for dynamic parts
│
├─ Does the app need real-time interactivity (dashboard, SPA)?
│  ├─ YES → Is it a full SPA with client-side routing?
│  │  ├─ YES → Consider React/Vue SPA instead, or Astro + client:only
│  │  └─ NO  → Hybrid + islands architecture
│  │           Interactive islands in static pages
│  └─ NO  → SSG (output: 'static')
│
├─ Build time concerns (>10k pages)?
│  ├─ YES → Hybrid with on-demand rendering
│  │        Prerender popular pages, SSR the long tail
│  └─ NO  → SSG handles it fine
│
└─ Need edge computing (low latency globally)?
   ├─ YES → SSR + Cloudflare/Vercel Edge adapter
   └─ NO  → SSR + Node adapter or SSG

Configuration

// astro.config.mjs
import { defineConfig } from 'astro/config';

// SSG (default) - all pages prerendered at build time
export default defineConfig({
  output: 'static',
});

// SSR - all pages rendered on request
export default defineConfig({
  output: 'server',
  adapter: cloudflare(), // or vercel(), netlify(), node()
});

// Hybrid - static default, opt-in SSR per page
export default defineConfig({
  output: 'hybrid',
  adapter: cloudflare(),
});
---
// In hybrid mode, opt OUT of prerendering for specific pages:
export const prerender = false;
// In SSR mode, opt IN to prerendering:
export const prerender = true;
---

Islands Architecture Quick Reference

Directive Hydrates When JS Shipped Use Case
client:load Immediately on page load Full bundle Above-fold interactive (nav, hero CTA)
client:idle After page is idle (requestIdleCallback) Full bundle Below-fold interactive (comment form, chat)
client:visible When scrolled into viewport Full bundle Far-down-page (footer widget, carousel)
client:media When media query matches Full bundle Mobile-only nav, responsive components
client:only="react" Immediately, skip SSR entirely Full bundle Components that can't SSR (canvas, WebGL)
(none) Never - static HTML only Zero JS Static content, cards, headers
---
import NavBar from '../components/NavBar.tsx';
import CommentForm from '../components/CommentForm.tsx';
import ImageCarousel from '../components/ImageCarousel.svelte';
import MobileMenu from '../components/MobileMenu.vue';
import ThreeScene from '../components/ThreeScene.tsx';
---

<!-- Loads immediately - critical interactivity -->
<NavBar client:load />

<!-- Loads after page is idle - non-critical -->
<CommentForm client:idle />

<!-- Loads when scrolled into view - lazy -->
<ImageCarousel client:visible />

<!-- Loads only on mobile -->
<MobileMenu client:media="(max-width: 768px)" />

<!-- Client-only, no SSR (WebGL can't run on server) -->
<ThreeScene client:only="react" />

Content Collections Quick Start

Define Schema

// src/content.config.ts (Astro 5) or src/content/config.ts (Astro 4)
import { defineCollection, z, reference } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string().max(160),
    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: reference('authors'), // Reference another collection
  }),
});

const authors = defineCollection({
  loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
  schema: z.object({
    name: z.string(),
    avatar: z.string(),
    bio: z.string(),
    socials: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
    }).optional(),
  }),
});

export const collections = { blog, authors };

Query Collections

---
import { getCollection, getEntry } from 'astro:content';

// Get all non-draft blog posts, sorted by date
const posts = (await getCollection('blog', ({ data }) => !data.draft))
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

// Get a single entry
const post = await getEntry('blog', 'my-first-post');

// Resolve a reference
const author = await getEntry(post.data.author);

// Render content
const { Content, headings } = await post.render();
---

<Content />

Content Collections vs External CMS

Criterion Content Collections External CMS (Payload, etc.)
Content type Local markdown/MDX, docs, blogs Relational data models
Authors Developers (version-controlled) Editors (admin UI, multi-user auth)
Validation Type-safe via Zod at build time CMS-side schemas + API contracts
Update cadence Deploys with the site Independent of deployments
API needs None (build-time queries) REST/GraphQL for other consumers
Workflow Git PRs, simple review Editorial workflows, drafts, roles

Rule of thumb: start with Content Collections; reach for a CMS only when non-developers need to publish without a deploy, or when content is genuinely relational.

Project Structure Reference

project-root/
├── astro.config.mjs          # Astro configuration
├── tsconfig.json              # TypeScript config (extends astro/tsconfigs)
├── package.json
├── public/                    # Static assets (copied as-is)
│   ├── favicon.svg
│   ├── robots.txt
│   └── og-image.png
├── src/
│   ├── pages/                 # File-based routing
│   │   ├── index.astro        # → /
│   │   ├── about.astro        # → /about
│   │   ├── blog/
│   │   │   ├── index.astro    # → /blog
│   │   │   └── [slug].astro   # → /blog/:slug (dynamic)
│   │   ├── api/
│   │   │   └── search.ts      # → /api/search (API endpoint)
│   │   └── [...slug].astro    # → catch-all/404
│   ├── layouts/
│   │   ├── BaseLayout.astro   # HTML shell, <head>, global styles
│   │   └── BlogPost.astro     # Blog post layout
│   ├── components/
│   │   ├── Header.astro       # Static Astro component
│   │   ├── Footer.astro
│   │   ├── NavBar.tsx         # React island
│   │   └── Counter.svelte     # Svelte island
│   ├── content/               # Content collections source files
│   │   ├── blog/
│   │   │   ├── post-one.md
│   │   │   └── post-two.mdx
│   │   └── authors/
│   │       └── jane.json
│   ├── content.config.ts      # Collection schemas (Astro 5)
│   ├── middleware.ts           # Request/response middleware
│   ├── styles/
│   │   └── global.css
│   └── lib/                   # Shared utilities
│       ├── utils.ts
│       └── constants.ts
└── .env                       # Environment variables

View Transitions Quick Reference

---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---

<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>

Transition Directives

<!-- Persist element across pages (keeps state, avoids re-render) -->
<audio transition:persist id="player">
  <source src="/music.mp3" />
</audio>

<!-- Named transition for animation pairing -->
<img transition:name="hero" src={post.heroImage} />

<!-- Custom animation -->
<div transition:animate="slide">Content</div>
<div transition:animate="fade">Content</div>
<div transition:animate="none">No animation</div>

<!-- Persist with name (for multiple persistent elements) -->
<video transition:persist="media-player" />

Lifecycle Events

<script>
  document.addEventListener('astro:before-preparation', (e) => {
    // Before new page is fetched - cancel navigation, show loading
  });

  document.addEventListener('astro:after-preparation', (e) => {
    // New page fetched, before swap
  });

  document.addEventListener('astro:before-swap', (e) => {
    // Customize DOM swap behavior
  });

  document.addEventListener('astro:after-swap', () => {
    // DOM updated - reinitialize scripts
  });

  document.addEventListener('astro:page-load', () => {
    // Page fully loaded (fires on initial + every navigation)
    // Use this instead of DOMContentLoaded with View Transitions
  });
</script>

Back/Forward Handling

// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,         // Prefetch all links on hover
    defaultStrategy: 'hover',  // 'hover' | 'tap' | 'viewport' | 'load'
  },
});
<!-- Per-link prefetch control -->
<a href="/about" data-astro-prefetch>Prefetch on hover (default)</a>
<a href="/blog" data-astro-prefetch="viewport">Prefetch when visible</a>
<a href="/contact" data-astro-prefetch="load">Prefetch immediately</a>
<a href="/external" data-astro-prefetch="false">No prefetch</a>

Deployment Decision Tree

Where to deploy?
│
├─ Need edge computing + Cloudflare ecosystem (KV, D1, R2)?
│  └─ Cloudflare Pages/Workers
│     Adapter: @astrojs/cloudflare
│     Best for: Global edge, Workers bindings, cost-effective
│
├─ Need serverless + Vercel ecosystem (ISR, analytics)?
│  └─ Vercel
│     Adapter: @astrojs/vercel
│     Best for: Next.js migration, image optimization, ISR
│
├─ Need serverless + Netlify ecosystem (forms, identity)?
│  └─ Netlify
│     Adapter: @astrojs/netlify
│     Best for: JAMstack, built-in forms, split testing
│
├─ Need full server control (Docker, custom runtime)?
│  └─ Node.js (standalone or Express/Fastify)
│     Adapter: @astrojs/node
│     Best for: Self-hosted, WebSocket, long-running processes
│
└─ Pure static site (no SSR needed)?
   └─ Any static host (GitHub Pages, S3, Cloudflare Pages)
      No adapter needed, output: 'static'
      Best for: Blogs, docs, marketing sites

Adapter Installation

# Cloudflare
npx astro add cloudflare

# Vercel
npx astro add vercel

# Netlify
npx astro add netlify

# Node.js
npx astro add node

Common Gotchas

Gotcha Why Fix
Hydration mismatch errors Server HTML differs from client render (dates, random IDs, browser APIs) Use client:only for browser-dependent components, or ensure deterministic rendering
import.meta.env undefined in client Only PUBLIC_ prefixed vars are exposed to client-side code Rename to PUBLIC_MY_VAR or pass via props from server
Dynamic routes 404 in SSG getStaticPaths() not returning all possible params Ensure getStaticPaths() returns every valid path, or switch to hybrid/SSR
Images not optimizing Using <img> instead of Astro's <Image /> component Import from astro:assets: import { Image } from 'astro:assets' and use local imports for src
SSR fails without adapter output: 'server' or 'hybrid' requires a deployment adapter Install adapter: npx astro add cloudflare (or vercel, netlify, node)
MDX components not rendering Custom components not passed to MDX content Pass components via <Content components={{ MyComponent }} /> or use astro.config.mjs MDX config
Content collection schema changes not reflected Type generation is cached, stale .astro types Run astro sync to regenerate types, restart dev server
client:* on Astro components Client directives only work on framework components (React, Vue, Svelte) Astro components are static-only; extract interactive parts to a framework component
document / window is not defined Server-side code cannot access browser globals Guard with if (typeof window !== 'undefined') or move to client:only
Styles leaking between components Using global CSS instead of scoped styles Use <style> (scoped by default in .astro) or <style is:global> intentionally
View Transitions break scripts DOMContentLoaded only fires once with View Transitions Use astro:page-load event instead, which fires on every navigation
Env vars missing in production .env not loaded or platform env vars not configured Use envField in astro.config.mjs for validation; set vars in platform dashboard

Production Security Checklist

For every production deployment, address:

  • CSP headers - configure a restrictive Content-Security-Policy (see middleware patterns in references/deployment.md)
  • Remote image restrictions - enforce explicit image.domains / remotePatterns allow-lists; never derive image URLs from user input (SSRF risk)
  • Host header validation - verify the request host matches expected domains in middleware (SSR/hybrid only)
  • Secrets management - on Cloudflare, use Workers Bindings (wrangler secret put), not env vars baked into code; elsewhere use platform secret stores
  • HTTPS only - ensure all external resources (scripts, images, fonts) load over HTTPS
  • Input validation - sanitize all user input in SSR contexts (query params, form bodies, cookies)

Reference Files

File Contents Lines
references/content-collections.md Schema patterns, Zod types, querying, MDX, content layer API, migrations ~500
references/islands-rendering.md Islands deep dive, client directives, framework integration, server islands ~550
references/deployment.md Cloudflare/Vercel/Netlify/Node adapters, env vars, optimization ~500

See Also

  • typescript-ops - TypeScript patterns used throughout Astro projects
  • tailwind-ops - Tailwind CSS integration with Astro (@astrojs/tailwind)
  • javascript-ops - Core JS patterns for client-side island code
  • container-orchestration - Docker patterns for self-hosted Astro (Node adapter)
  • Astro Documentation
  • Astro Integration Guide
Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • content-collections.md 18 KB
      # Content Collections Reference
      
      Comprehensive guide to Astro content collections: schema definition, querying, references, MDX integration, and the Content Layer API.
      
      ## Schema Definition with Zod
      
      ### Basic Schema
      
      ```typescript
      // src/content.config.ts (Astro 5+)
      import { defineCollection, z } from 'astro:content';
      import { glob, file } from 'astro/loaders';
      
      const blog = defineCollection({
        loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
        schema: z.object({
          title: z.string(),
          description: z.string(),
          pubDate: z.coerce.date(),
          draft: z.boolean().default(false),
        }),
      });
      
      export const collections = { blog };
      ```
      
      ### All Supported Zod Types
      
      ```typescript
      import { defineCollection, z, reference } from 'astro:content';
      import { glob, file } from 'astro/loaders';
      
      const fullSchema = defineCollection({
        loader: glob({ pattern: '**/*.mdx', base: './src/content/posts' }),
        schema: ({ image }) => z.object({
          // String types
          title: z.string(),
          slug: z.string().optional(),
          description: z.string().max(160),
          canonical: z.string().url().optional(),
      
          // Number types
          readingTime: z.number().positive().optional(),
          order: z.number().int().min(0).default(0),
      
          // Date types
          pubDate: z.coerce.date(),                    // Accepts string or Date
          updatedDate: z.coerce.date().optional(),
      
          // Boolean
          draft: z.boolean().default(false),
          featured: z.boolean().default(false),
      
          // Enum
          category: z.enum(['tutorial', 'guide', 'reference', 'blog']),
          status: z.enum(['draft', 'review', 'published']).default('draft'),
      
          // Arrays
          tags: z.array(z.string()).default([]),
          relatedSlugs: z.array(z.string()).optional(),
      
          // Nested objects
          author: z.object({
            name: z.string(),
            email: z.string().email().optional(),
          }),
      
          // Union types
          layout: z.union([
            z.literal('default'),
            z.literal('wide'),
            z.literal('full'),
          ]).default('default'),
      
          // Image (validated by Astro, returns optimized metadata)
          heroImage: image().optional(),
          thumbnail: image().refine((img) => img.width >= 200, {
            message: 'Thumbnail must be at least 200px wide',
          }).optional(),
      
          // References to other collections
          author_ref: reference('authors'),
          relatedPosts: z.array(reference('blog')).default([]),
      
          // Custom transforms
          title_normalized: z.string().transform((val) => val.toLowerCase().trim()),
      
          // Passthrough for unknown fields
          // extra: z.record(z.unknown()),
        }),
      });
      ```
      
      ### Image Schema
      
      ```typescript
      // The image() helper validates that referenced images exist at build time
      const gallery = defineCollection({
        loader: glob({ pattern: '**/*.md', base: './src/content/gallery' }),
        schema: ({ image }) => z.object({
          title: z.string(),
          cover: image(),
          // Refine with dimension constraints
          hero: image().refine((img) => img.width >= 1080, {
            message: 'Hero image must be at least 1080px wide',
          }),
          // Array of images
          photos: z.array(image()).default([]),
        }),
      });
      ```
      
      Usage in frontmatter:
      
      ```markdown
      ---
      title: My Gallery
      cover: ./images/cover.jpg        # Relative path to image
      hero: ../../assets/hero.png      # Can reference shared assets
      photos:
        - ./images/photo1.jpg
        - ./images/photo2.jpg
      ---
      ```
      
      ## References Between Collections
      
      ### Defining References
      
      ```typescript
      // src/content.config.ts
      import { defineCollection, z, reference } from 'astro:content';
      import { glob, file } from 'astro/loaders';
      
      const authors = defineCollection({
        loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
        schema: z.object({
          name: z.string(),
          avatar: z.string(),
          bio: z.string(),
          website: z.string().url().optional(),
        }),
      });
      
      const categories = defineCollection({
        loader: file('src/data/categories.json'),
        schema: z.object({
          name: z.string(),
          slug: z.string(),
          description: z.string(),
        }),
      });
      
      const blog = defineCollection({
        loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
        schema: z.object({
          title: z.string(),
          // Single reference
          author: reference('authors'),
          // Optional reference
          reviewer: reference('authors').optional(),
          // Array of references
          categories: z.array(reference('categories')).default([]),
          // Self-reference (same collection)
          relatedPosts: z.array(reference('blog')).default([]),
        }),
      });
      
      export const collections = { authors, categories, blog };
      ```
      
      ### Blog post frontmatter with references
      
      ```markdown
      ---
      title: Getting Started with Astro
      author: jane-doe
      reviewer: john-smith
      categories:
        - tutorials
        - astro
      relatedPosts:
        - advanced-astro-patterns
        - astro-vs-next
      ---
      ```
      
      ### Resolving References
      
      ```astro
      ---
      import { getEntry, getCollection } from 'astro:content';
      
      // Get the blog post
      const post = await getEntry('blog', 'getting-started');
      
      // Resolve single reference
      const author = await getEntry(post.data.author);
      // author.data.name, author.data.avatar, etc.
      
      // Resolve optional reference
      const reviewer = post.data.reviewer
        ? await getEntry(post.data.reviewer)
        : null;
      
      // Resolve array of references
      const categories = await Promise.all(
        post.data.categories.map((ref) => getEntry(ref))
      );
      
      // Resolve self-references
      const relatedPosts = await Promise.all(
        post.data.relatedPosts.map((ref) => getEntry(ref))
      );
      ---
      
      <article>
        <h1>{post.data.title}</h1>
        <p>By {author.data.name}</p>
        {reviewer && <p>Reviewed by {reviewer.data.name}</p>}
        <div class="categories">
          {categories.map((cat) => <span>{cat.data.name}</span>)}
        </div>
      </article>
      ```
      
      ## Querying Collections
      
      ### getCollection
      
      ```typescript
      import { getCollection } from 'astro:content';
      
      // Get all entries
      const allPosts = await getCollection('blog');
      
      // Filter with callback (type-safe)
      const publishedPosts = await getCollection('blog', ({ data }) => {
        return !data.draft && data.pubDate <= new Date();
      });
      
      // Sort by date (descending)
      const sortedPosts = (await getCollection('blog'))
        .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
      
      // Filter by tag
      const astroTagged = await getCollection('blog', ({ data }) => {
        return data.tags.includes('astro');
      });
      
      // Paginate
      const pageSize = 10;
      const page = 1;
      const paginatedPosts = sortedPosts.slice(
        (page - 1) * pageSize,
        page * pageSize
      );
      ```
      
      ### getEntry
      
      ```typescript
      import { getEntry } from 'astro:content';
      
      // Get single entry by collection + id
      const post = await getEntry('blog', 'my-first-post');
      
      // Returns null if not found (in Astro 5, throws if not found by default)
      if (!post) {
        return Astro.redirect('/404');
      }
      
      // Access data
      console.log(post.data.title);   // Type-safe frontmatter
      console.log(post.id);           // Entry ID (filename without extension)
      
      // Render content
      const { Content, headings, remarkPluginFrontmatter } = 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', ({ data }) => !data.draft);
        return posts.map((post) => ({
          params: { slug: post.id },
          props: { post },
        }));
      }
      
      const { post } = Astro.props;
      const { Content, headings } = await render(post);
      ---
      
      <article>
        <h1>{post.data.title}</h1>
        <time datetime={post.data.pubDate.toISOString()}>
          {post.data.pubDate.toLocaleDateString()}
        </time>
        <Content />
      </article>
      ```
      
      ### Pagination with Collections
      
      ```astro
      ---
      // src/pages/blog/[...page].astro
      import type { GetStaticPaths } from 'astro';
      import { getCollection } from 'astro:content';
      
      export const getStaticPaths = (async ({ paginate }) => {
        const posts = (await getCollection('blog', ({ data }) => !data.draft))
          .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
      
        return paginate(posts, { pageSize: 10 });
      }) satisfies GetStaticPaths;
      
      const { page } = Astro.props;
      // page.data       - current page entries
      // page.currentPage - current page number
      // page.lastPage   - total pages
      // page.url.prev   - previous page URL
      // page.url.next   - next page URL
      // page.total      - total entries
      ---
      
      {page.data.map((post) => (
        <article>
          <a href={`/blog/${post.id}`}>{post.data.title}</a>
        </article>
      ))}
      
      <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>
      ```
      
      ## MDX Integration
      
      ### Setup
      
      ```bash
      npx astro add mdx
      ```
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import mdx from '@astrojs/mdx';
      
      export default defineConfig({
        integrations: [mdx()],
      });
      ```
      
      ### Custom Components in MDX
      
      ```mdx
      ---
      title: Interactive Tutorial
      ---
      
      import Counter from '../../components/Counter.tsx';
      import Callout from '../../components/Callout.astro';
      import { Code } from 'astro:components';
      
      # {frontmatter.title}
      
      Here's a live counter:
      
      <Counter client:visible initialCount={5} />
      
      <Callout type="warning">
        Remember to hydrate interactive components with a `client:*` directive!
      </Callout>
      ```
      
      ### Passing Components to Rendered Content
      
      ```astro
      ---
      import { getEntry, render } from 'astro:content';
      import Callout from '../components/Callout.astro';
      import CodeBlock from '../components/CodeBlock.astro';
      
      const post = await getEntry('blog', 'my-post');
      const { Content } = await render(post);
      ---
      
      <!-- Override default HTML elements with custom components -->
      <Content components={{
        h1: 'h2',                    <!-- Remap h1 to h2 -->
        blockquote: Callout,         <!-- Replace blockquotes with Callout -->
        pre: CodeBlock,              <!-- Replace code blocks -->
      }} />
      ```
      
      ### Remark and Rehype Plugins
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import mdx from '@astrojs/mdx';
      import remarkToc from 'remark-toc';
      import remarkMath from 'remark-math';
      import rehypeKatex from 'rehype-katex';
      import rehypeSlug from 'rehype-slug';
      import rehypeAutolinkHeadings from 'rehype-autolink-headings';
      
      export default defineConfig({
        integrations: [mdx()],
        markdown: {
          remarkPlugins: [
            remarkToc,
            remarkMath,
          ],
          rehypePlugins: [
            rehypeSlug,
            [rehypeAutolinkHeadings, { behavior: 'wrap' }],
            rehypeKatex,
          ],
          // Syntax highlighting
          shikiConfig: {
            theme: 'github-dark',
            wrap: true,
          },
        },
      });
      ```
      
      ## Content Layer API (Astro 5)
      
      The Content Layer API replaces the filesystem-coupled collection system with a flexible loader-based approach.
      
      ### Built-in Loaders
      
      ```typescript
      // src/content.config.ts
      import { defineCollection, z } from 'astro:content';
      import { glob, file } from 'astro/loaders';
      
      // Glob loader - load from filesystem with glob patterns
      const blog = defineCollection({
        loader: glob({
          pattern: '**/*.{md,mdx}',
          base: './src/content/blog',
          // Optional: generate ID from filename
          generateId: ({ entry, base, data }) => {
            return entry.replace(/\.mdx?$/, '');
          },
        }),
        schema: z.object({
          title: z.string(),
          pubDate: z.coerce.date(),
        }),
      });
      
      // File loader - load from a single JSON/YAML file
      const navigation = defineCollection({
        loader: file('src/data/navigation.json'),
        schema: z.object({
          label: z.string(),
          href: z.string(),
          order: z.number(),
        }),
      });
      
      // File loader with nested data
      const settings = defineCollection({
        loader: file('src/data/settings.yaml', {
          // Extract array from nested path
          parser: (text) => {
            const yaml = parseYaml(text);
            return yaml.site.menuItems;
          },
        }),
        schema: z.object({
          label: z.string(),
          url: z.string(),
        }),
      });
      
      export const collections = { blog, navigation, settings };
      ```
      
      ### Custom Loaders
      
      ```typescript
      // src/loaders/api-loader.ts
      import type { Loader } from 'astro/loaders';
      
      export function apiLoader(options: { url: string; apiKey: string }): Loader {
        return {
          name: 'api-loader',
          load: async ({ store, logger, parseData, generateDigest }) => {
            logger.info('Fetching data from API...');
      
            const response = await fetch(options.url, {
              headers: { Authorization: `Bearer ${options.apiKey}` },
            });
            const items = await response.json();
      
            // Clear previous data
            store.clear();
      
            for (const item of items) {
              const digest = generateDigest(item);
      
              // Parse and validate data against schema
              const data = await parseData({
                id: String(item.id),
                data: item,
              });
      
              store.set({
                id: String(item.id),
                data,
                digest,
                // Optional: rendered HTML content
                rendered: {
                  html: item.content_html ?? '',
                },
              });
            }
      
            logger.info(`Loaded ${items.length} items`);
          },
        };
      }
      ```
      
      ```typescript
      // src/content.config.ts
      import { defineCollection, z } from 'astro:content';
      import { apiLoader } from '../loaders/api-loader';
      
      const products = defineCollection({
        loader: apiLoader({
          url: 'https://api.example.com/products',
          apiKey: import.meta.env.API_KEY,
        }),
        schema: z.object({
          name: z.string(),
          price: z.number(),
          description: z.string(),
          inStock: z.boolean(),
        }),
      });
      
      export const collections = { products };
      ```
      
      ### CMS Integration Loaders
      
      ```typescript
      // Example: Notion loader (community package)
      import { notionLoader } from '@notionhq/astro-loader';
      
      const docs = defineCollection({
        loader: notionLoader({
          databaseId: import.meta.env.NOTION_DB_ID,
          auth: import.meta.env.NOTION_API_KEY,
        }),
        schema: z.object({
          title: z.string(),
          status: z.enum(['Draft', 'Published']),
          lastEdited: z.coerce.date(),
        }),
      });
      ```
      
      ### Incremental Builds
      
      ```typescript
      // Custom loader with incremental update support
      export function incrementalLoader(options: { url: string }): Loader {
        return {
          name: 'incremental-loader',
          load: async ({ store, logger, parseData, meta }) => {
            // meta.store persists between builds
            const lastSync = meta.get('lastSync');
      
            const url = lastSync
              ? `${options.url}?since=${lastSync}`
              : options.url;
      
            const response = await fetch(url);
            const items = await response.json();
      
            // Only update changed items (don't clear store)
            for (const item of items) {
              if (item.deleted) {
                store.delete(String(item.id));
              } else {
                const data = await parseData({
                  id: String(item.id),
                  data: item,
                });
                store.set({ id: String(item.id), data });
              }
            }
      
            meta.set('lastSync', new Date().toISOString());
          },
        };
      }
      ```
      
      ## Type Generation and InferEntrySchema
      
      ### Generating Types
      
      ```bash
      # Manually regenerate types after schema changes
      npx astro sync
      ```
      
      ### Using InferEntrySchema
      
      ```typescript
      // src/lib/types.ts
      import type { InferEntrySchema, CollectionEntry } from 'astro:content';
      
      // Infer the schema type for a collection
      type BlogPost = InferEntrySchema<'blog'>;
      // { title: string; description: string; pubDate: Date; ... }
      
      // Full collection entry type (includes id, data, render, etc.)
      type BlogEntry = CollectionEntry<'blog'>;
      
      // Use in utility functions
      function formatPost(post: CollectionEntry<'blog'>) {
        return {
          title: post.data.title,
          url: `/blog/${post.id}`,
          date: post.data.pubDate.toLocaleDateString(),
        };
      }
      
      // Use in component props
      interface PostListProps {
        posts: CollectionEntry<'blog'>[];
        showDrafts?: boolean;
      }
      ```
      
      ### Type-safe Frontmatter in Layouts
      
      ```astro
      ---
      // src/layouts/BlogPost.astro
      import type { CollectionEntry } from 'astro:content';
      
      interface Props {
        post: CollectionEntry<'blog'>;
      }
      
      const { post } = Astro.props;
      const { title, description, pubDate, heroImage, author } = post.data;
      ---
      
      <html>
        <head>
          <title>{title}</title>
          <meta name="description" content={description} />
        </head>
        <body>
          <article>
            <h1>{title}</h1>
            <time datetime={pubDate.toISOString()}>
              {pubDate.toLocaleDateString('en-US', {
                year: 'numeric', month: 'long', day: 'numeric'
              })}
            </time>
            <slot />
          </article>
        </body>
      </html>
      ```
      
      ## Migration Guide: Astro 2/3/4 to Astro 5
      
      ### Collection Config Location
      
      ```
      # Astro 4 (legacy)
      src/content/config.ts
      
      # Astro 5 (Content Layer API)
      src/content.config.ts        # Note: moved to src root
      ```
      
      ### Adding Loaders (Required in Astro 5)
      
      ```typescript
      // Astro 4 - implicit filesystem loading
      const blog = defineCollection({
        type: 'content',               // Remove this
        schema: z.object({ ... }),
      });
      
      // Astro 5 - explicit loaders
      import { glob, file } from 'astro/loaders';
      
      const blog = defineCollection({
        loader: glob({                  // Add loader
          pattern: '**/*.{md,mdx}',
          base: './src/content/blog',
        }),
        schema: z.object({ ... }),
      });
      ```
      
      ### Data Collections Migration
      
      ```typescript
      // Astro 4 - data collections
      const authors = defineCollection({
        type: 'data',                   // Remove this
        schema: z.object({ ... }),
      });
      
      // Astro 5 - use file loader
      import { file } from 'astro/loaders';
      
      const authors = defineCollection({
        loader: file('src/data/authors.json'),  // Or glob for multiple files
        schema: z.object({ ... }),
      });
      ```
      
      ### Entry ID Changes
      
      ```typescript
      // Astro 4
      post.slug;           // Used for routing
      post.id;             // Included file extension: "my-post.md"
      
      // Astro 5
      post.id;             // Slug-like, no extension: "my-post"
      // post.slug removed - use post.id instead
      ```
      
      ### Rendering Changes
      
      ```typescript
      // Astro 4
      const { Content } = await post.render();
      
      // Astro 5
      import { render } from 'astro:content';
      const { Content } = await render(post);
      ```
      
      ### Checklist for Migration
      
      1. Move `src/content/config.ts` to `src/content.config.ts`
      2. Add `loader` property to every collection (use `glob()` or `file()`)
      3. Remove `type: 'content'` and `type: 'data'` from collections
      4. Replace `post.slug` with `post.id` in routing
      5. Replace `post.render()` with `render(post)` from `astro:content`
      6. Run `npx astro sync` to regenerate types
      7. Update `getStaticPaths()` to use `post.id` instead of `post.slug`
      8. Test all content pages and dynamic routes
      
    • deployment.md 21.4 KB
      # Deployment Reference
      
      Comprehensive guide to deploying Astro applications across platforms: Cloudflare, Vercel, Netlify, Node.js, and static hosting.
      
      ## Cloudflare Workers / Pages
      
      ### Setup
      
      ```bash
      npx astro add cloudflare
      ```
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import cloudflare from '@astrojs/cloudflare';
      
      export default defineConfig({
        output: 'server',           // or 'hybrid'
        adapter: cloudflare({
          imageService: 'cloudflare', // Use Cloudflare Image Resizing
          platformProxy: {
            enabled: true,           // Enable local bindings in dev
          },
        }),
        site: 'https://example.com',
      });
      ```
      
      ### Wrangler Configuration
      
      ```toml
      # wrangler.toml
      name = "my-astro-site"
      compatibility_date = "2024-11-01"
      compatibility_flags = ["nodejs_compat"]
      pages_build_output_dir = "./dist"
      
      # KV Namespace binding
      [[kv_namespaces]]
      binding = "CACHE"
      id = "abc123"
      
      # D1 Database binding
      [[d1_databases]]
      binding = "DB"
      database_name = "my-db"
      database_id = "def456"
      
      # R2 Bucket binding
      [[r2_buckets]]
      binding = "ASSETS"
      bucket_name = "my-assets"
      
      # Environment variables
      [vars]
      API_URL = "https://api.example.com"
      
      # Secrets (set via wrangler secret put)
      # SECRET_KEY - set via `wrangler secret put SECRET_KEY`
      ```
      
      ### Accessing Cloudflare Bindings
      
      ```typescript
      // Type definitions for Cloudflare bindings
      // src/env.d.ts
      /// <reference types="astro/client" />
      
      type Runtime = import('@astrojs/cloudflare').Runtime<Env>;
      
      interface Env {
        CACHE: KVNamespace;
        DB: D1Database;
        ASSETS: R2Bucket;
        API_URL: string;
        SECRET_KEY: string;
      }
      
      declare namespace App {
        interface Locals extends Runtime {}
      }
      ```
      
      ```astro
      ---
      // src/pages/api/data.ts
      import type { APIContext } from 'astro';
      
      export async function GET({ locals }: APIContext) {
        const { env } = locals.runtime;
      
        // KV operations
        const cached = await env.CACHE.get('my-key');
        if (cached) {
          return new Response(cached, {
            headers: { 'Content-Type': 'application/json' },
          });
        }
      
        // D1 database query
        const { results } = await env.DB
          .prepare('SELECT * FROM posts WHERE published = ?')
          .bind(true)
          .all();
      
        // R2 object storage
        const object = await env.ASSETS.get('images/hero.jpg');
      
        // Cache the result
        await env.CACHE.put('my-key', JSON.stringify(results), {
          expirationTtl: 3600,
        });
      
        return new Response(JSON.stringify(results), {
          headers: { 'Content-Type': 'application/json' },
        });
      }
      ```
      
      ### Cloudflare Middleware
      
      ```typescript
      // src/middleware.ts
      import { defineMiddleware } from 'astro:middleware';
      
      export const onRequest = defineMiddleware(async ({ locals, request, cookies }, next) => {
        const { env } = locals.runtime;
      
        // Auth check using KV
        const session = cookies.get('session')?.value;
        if (session) {
          const user = await env.CACHE.get(`session:${session}`);
          if (user) {
            locals.user = JSON.parse(user);
          }
        }
      
        // Rate limiting with KV
        const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
        const rateKey = `rate:${ip}`;
        const count = parseInt(await env.CACHE.get(rateKey) ?? '0');
      
        if (count > 100) {
          return new Response('Rate limited', { status: 429 });
        }
      
        await env.CACHE.put(rateKey, String(count + 1), { expirationTtl: 60 });
      
        return next();
      });
      ```
      
      ### Deployment
      
      ```bash
      # Build and deploy to Cloudflare Pages
      npm run build
      npx wrangler pages deploy dist
      
      # Or connect to Git for automatic deploys via Cloudflare Dashboard
      # Settings > Build > Framework preset: Astro
      ```
      
      ## Vercel
      
      ### Setup
      
      ```bash
      npx astro add vercel
      ```
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import vercel from '@astrojs/vercel';
      
      export default defineConfig({
        output: 'server',           // or 'hybrid'
        adapter: vercel({
          imageService: true,        // Use Vercel Image Optimization
          isr: {
            expiration: 60,          // ISR: revalidate every 60 seconds
          },
          webAnalytics: {
            enabled: true,           // Enable Vercel Web Analytics
          },
          maxDuration: 30,           // Serverless function timeout (seconds)
        }),
      });
      ```
      
      ### Serverless vs Edge
      
      ```typescript
      // Default: serverless function
      export default defineConfig({
        output: 'server',
        adapter: vercel(),
      });
      
      // Edge function (faster cold start, limited APIs)
      export default defineConfig({
        output: 'server',
        adapter: vercel({
          edgeMiddleware: true,      // Run middleware at the edge
        }),
      });
      ```
      
      ### ISR (Incremental Static Regeneration)
      
      ```astro
      ---
      // Per-page ISR configuration
      // src/pages/blog/[slug].astro
      export const prerender = false;
      
      // Set ISR headers
      Astro.response.headers.set(
        'Cache-Control',
        's-maxage=60, stale-while-revalidate=600'
      );
      ---
      ```
      
      ### Vercel Environment Variables
      
      ```bash
      # Set via Vercel CLI
      vercel env add PRIVATE_KEY
      vercel env add PUBLIC_API_URL
      
      # Or via vercel.json
      ```
      
      ```json
      // vercel.json
      {
        "framework": "astro",
        "buildCommand": "astro build",
        "outputDirectory": "dist",
        "headers": [
          {
            "source": "/api/(.*)",
            "headers": [
              { "key": "Cache-Control", "value": "s-maxage=60" }
            ]
          }
        ],
        "redirects": [
          { "source": "/old-path", "destination": "/new-path", "permanent": true }
        ]
      }
      ```
      
      ### Deployment
      
      ```bash
      # Deploy to Vercel
      npx vercel
      
      # Production deploy
      npx vercel --prod
      
      # Or connect to Git for automatic deploys
      ```
      
      ## Netlify
      
      ### Setup
      
      ```bash
      npx astro add netlify
      ```
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import netlify from '@astrojs/netlify';
      
      export default defineConfig({
        output: 'server',           // or 'hybrid'
        adapter: netlify({
          edgeMiddleware: true,      // Run middleware at the edge
          imageCDN: true,            // Use Netlify Image CDN
        }),
      });
      ```
      
      ### Netlify Configuration
      
      ```toml
      # netlify.toml
      [build]
        command = "astro build"
        publish = "dist"
      
      [build.environment]
        NODE_VERSION = "20"
      
      # Redirects
      [[redirects]]
        from = "/old-path"
        to = "/new-path"
        status = 301
      
      # Custom headers
      [[headers]]
        for = "/api/*"
        [headers.values]
          Access-Control-Allow-Origin = "*"
          Cache-Control = "public, max-age=60"
      
      # Netlify Forms
      # Forms are auto-detected in static builds
      # For SSR, use Netlify Forms API
      ```
      
      ### Netlify Edge Functions
      
      ```typescript
      // netlify/edge-functions/geolocation.ts
      import type { Context } from '@netlify/edge-functions';
      
      export default async function (request: Request, context: Context) {
        const { country, city } = context.geo;
      
        // Add geo data to request headers for Astro middleware
        request.headers.set('x-country', country?.code ?? 'US');
        request.headers.set('x-city', city ?? 'Unknown');
      
        return context.next();
      }
      
      export const config = { path: '/*' };
      ```
      
      ### Netlify Forms with Astro
      
      ```astro
      ---
      // Static output - Netlify auto-detects forms
      ---
      
      <form name="contact" method="POST" data-netlify="true">
        <input type="hidden" name="form-name" value="contact" />
        <input type="text" name="name" required />
        <input type="email" name="email" required />
        <textarea name="message" required></textarea>
        <button type="submit">Send</button>
      </form>
      ```
      
      ### Deployment
      
      ```bash
      # Deploy to Netlify
      npx netlify deploy
      
      # Production deploy
      npx netlify deploy --prod
      
      # Or connect to Git for automatic deploys
      ```
      
      ## Node.js (Self-hosted)
      
      ### Setup
      
      ```bash
      npx astro add node
      ```
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import node from '@astrojs/node';
      
      export default defineConfig({
        output: 'server',
        adapter: node({
          mode: 'standalone',        // or 'middleware'
        }),
      });
      ```
      
      ### Standalone Mode
      
      ```bash
      # Build
      npm run build
      
      # Run (starts built-in HTTP server)
      HOST=0.0.0.0 PORT=4321 node dist/server/entry.mjs
      ```
      
      ### Middleware Mode (Express/Fastify)
      
      ```typescript
      // astro.config.mjs
      import node from '@astrojs/node';
      
      export default defineConfig({
        output: 'server',
        adapter: node({ mode: 'middleware' }),
      });
      ```
      
      ```typescript
      // server.mjs - Custom Express server
      import express from 'express';
      import { handler as astroHandler } from './dist/server/entry.mjs';
      
      const app = express();
      
      // Custom middleware before Astro
      app.use('/health', (req, res) => {
        res.json({ status: 'ok' });
      });
      
      // Serve static files
      app.use(express.static('dist/client'));
      
      // Astro handles everything else
      app.use(astroHandler);
      
      const port = process.env.PORT || 4321;
      app.listen(port, () => {
        console.log(`Server running on port ${port}`);
      });
      ```
      
      ```typescript
      // server-fastify.mjs - Custom Fastify server
      import Fastify from 'fastify';
      import fastifyStatic from '@fastify/static';
      import { handler as astroHandler } from './dist/server/entry.mjs';
      import { fileURLToPath } from 'url';
      import path from 'path';
      
      const __dirname = path.dirname(fileURLToPath(import.meta.url));
      
      const app = Fastify({ logger: true });
      
      // Static files
      app.register(fastifyStatic, {
        root: path.join(__dirname, 'dist/client'),
      });
      
      // Health check
      app.get('/health', async () => ({ status: 'ok' }));
      
      // Astro handler
      app.use(astroHandler);
      
      app.listen({ port: 4321, host: '0.0.0.0' });
      ```
      
      ### Docker Deployment
      
      ```dockerfile
      # Dockerfile
      FROM node:20-slim AS builder
      
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci
      
      COPY . .
      RUN npm run build
      
      FROM node:20-slim AS runtime
      
      WORKDIR /app
      COPY --from=builder /app/dist ./dist
      COPY --from=builder /app/node_modules ./node_modules
      COPY --from=builder /app/package.json ./
      
      ENV HOST=0.0.0.0
      ENV PORT=4321
      
      EXPOSE 4321
      
      HEALTHCHECK --interval=30s --timeout=3s \
        CMD curl -f http://localhost:4321/health || exit 1
      
      CMD ["node", "dist/server/entry.mjs"]
      ```
      
      ```yaml
      # docker-compose.yml
      services:
        astro:
          build: .
          ports:
            - "4321:4321"
          environment:
            - DATABASE_URL=postgres://db:5432/app
            - SECRET_KEY=${SECRET_KEY}
          restart: unless-stopped
          depends_on:
            - db
        db:
          image: postgres:16-alpine
          volumes:
            - pgdata:/var/lib/postgresql/data
          environment:
            POSTGRES_DB: app
            POSTGRES_PASSWORD: ${DB_PASSWORD}
      
      volumes:
        pgdata:
      ```
      
      ## Static Hosting
      
      ### Configuration
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      
      export default defineConfig({
        output: 'static',           // Default - all pages prerendered
        site: 'https://example.com',
        base: '/my-app',            // If hosted at a subpath
      });
      ```
      
      ### GitHub Pages
      
      ```yaml
      # .github/workflows/deploy.yml
      name: Deploy to GitHub Pages
      
      on:
        push:
          branches: [main]
      
      permissions:
        contents: read
        pages: write
        id-token: write
      
      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version: 20
            - run: npm ci
            - run: npm run build
            - uses: actions/upload-pages-artifact@v3
              with:
                path: dist
      
        deploy:
          needs: build
          runs-on: ubuntu-latest
          environment:
            name: github-pages
            url: ${{ steps.deployment.outputs.page_url }}
          steps:
            - id: deployment
              uses: actions/deploy-pages@v4
      ```
      
      ```typescript
      // astro.config.mjs for GitHub Pages
      export default defineConfig({
        site: 'https://username.github.io',
        base: '/repo-name',         // For project pages (not needed for user pages)
      });
      ```
      
      ### S3 + CloudFront
      
      ```bash
      # Build and sync to S3
      npm run build
      aws s3 sync dist s3://my-bucket --delete
      aws cloudfront create-invalidation --distribution-id DIST_ID --paths "/*"
      ```
      
      ## Environment Variables
      
      ### Astro Environment Variable Rules
      
      ```
      # .env
      # Private (server-only) - NOT available in client-side code
      DATABASE_URL=postgres://localhost:5432/mydb
      API_SECRET=sk-12345
      SESSION_KEY=abc
      
      # Public (available in client-side code) - MUST start with PUBLIC_
      PUBLIC_API_URL=https://api.example.com
      PUBLIC_SITE_NAME=My Site
      PUBLIC_GA_ID=G-12345
      ```
      
      ### Accessing Environment Variables
      
      ```typescript
      // Server-side (pages, middleware, API routes, server islands)
      const dbUrl = import.meta.env.DATABASE_URL;        // Works
      const apiKey = import.meta.env.API_SECRET;          // Works
      const publicUrl = import.meta.env.PUBLIC_API_URL;   // Works
      
      // Client-side (browser, client:* components)
      const publicUrl = import.meta.env.PUBLIC_API_URL;   // Works
      const dbUrl = import.meta.env.DATABASE_URL;         // undefined!
      ```
      
      ### envField Schema Validation (Astro 5)
      
      ```typescript
      // astro.config.mjs
      import { defineConfig, envField } from 'astro/config';
      
      export default defineConfig({
        env: {
          schema: {
            // Server-only variables
            DATABASE_URL: envField.string({
              context: 'server',
              access: 'secret',
              optional: false,
            }),
            API_KEY: envField.string({
              context: 'server',
              access: 'secret',
            }),
            PORT: envField.number({
              context: 'server',
              access: 'public',
              default: 4321,
            }),
      
            // Client-accessible variables
            PUBLIC_API_URL: envField.string({
              context: 'client',
              access: 'public',
            }),
            PUBLIC_FEATURE_FLAG: envField.boolean({
              context: 'client',
              access: 'public',
              default: false,
            }),
          },
        },
      });
      ```
      
      ```typescript
      // Type-safe env access with validation
      import { DATABASE_URL, PORT } from 'astro:env/server';
      import { PUBLIC_API_URL, PUBLIC_FEATURE_FLAG } from 'astro:env/client';
      
      // These are typed and validated at build time
      console.log(DATABASE_URL);     // string (required)
      console.log(PORT);             // number (defaults to 4321)
      console.log(PUBLIC_API_URL);   // string (required)
      ```
      
      ### Platform-specific Environment Variables
      
      ```bash
      # Cloudflare - set in wrangler.toml or dashboard
      wrangler secret put API_KEY
      
      # Vercel
      vercel env add API_KEY production
      
      # Netlify
      netlify env:set API_KEY "value"
      
      # Docker
      docker run -e DATABASE_URL=... my-astro-app
      ```
      
      ## Headers and Redirects
      
      ### Middleware-based Headers
      
      ```typescript
      // src/middleware.ts
      import { defineMiddleware, sequence } from 'astro:middleware';
      
      const securityHeaders = defineMiddleware(async (context, next) => {
        const response = await next();
      
        response.headers.set('X-Content-Type-Options', 'nosniff');
        response.headers.set('X-Frame-Options', 'DENY');
        response.headers.set('X-XSS-Protection', '1; mode=block');
        response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
        response.headers.set(
          'Content-Security-Policy',
          "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
        );
      
        return response;
      });
      
      const cacheHeaders = defineMiddleware(async (context, next) => {
        const response = await next();
      
        // Cache static assets aggressively
        if (context.url.pathname.startsWith('/_astro/')) {
          response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
        }
      
        return response;
      });
      
      export const onRequest = sequence(securityHeaders, cacheHeaders);
      ```
      
      ### Static File Headers
      
      ```
      # public/_headers (Cloudflare Pages / Netlify)
      /*
        X-Content-Type-Options: nosniff
        X-Frame-Options: DENY
      
      /_astro/*
        Cache-Control: public, max-age=31536000, immutable
      
      /api/*
        Cache-Control: no-cache
        Access-Control-Allow-Origin: *
      ```
      
      ### Redirects
      
      ```
      # public/_redirects (Cloudflare Pages / Netlify)
      /old-blog/*    /blog/:splat    301
      /legacy        /               302
      /docs          https://docs.example.com  301
      ```
      
      ```typescript
      // Programmatic redirects in middleware
      import { defineMiddleware } from 'astro:middleware';
      
      const redirects: Record<string, { to: string; status: 301 | 302 }> = {
        '/old-path': { to: '/new-path', status: 301 },
        '/legacy': { to: '/', status: 302 },
      };
      
      export const onRequest = defineMiddleware(async ({ url, redirect }, next) => {
        const rule = redirects[url.pathname];
        if (rule) {
          return redirect(rule.to, rule.status);
        }
        return next();
      });
      ```
      
      ## SSR Streaming
      
      ### Response Streaming
      
      ```astro
      ---
      // Astro streams HTML by default in SSR mode
      // Components render top-to-bottom, streaming chunks to the client
      
      // Slow data fetch - page header already visible while this loads
      const slowData = await fetch('https://slow-api.example.com/data')
        .then(r => r.json());
      ---
      
      <html>
        <body>
          <!-- This streams immediately -->
          <h1>Page Title</h1>
      
          <!-- This streams after slowData resolves -->
          <div>{slowData.content}</div>
        </body>
      </html>
      ```
      
      ### Streaming with Server Islands
      
      ```astro
      ---
      // Combine streaming with server islands for optimal loading
      import SlowWidget from '../components/SlowWidget.astro';
      import UserDashboard from '../components/UserDashboard.astro';
      ---
      
      <!-- Streams immediately -->
      <header>Fast static content</header>
      
      <!-- Server island: page sends without waiting for this -->
      <SlowWidget server:defer>
        <div slot="fallback">Loading widget...</div>
      </SlowWidget>
      
      <!-- This also streams immediately (doesn't wait for SlowWidget) -->
      <main>More fast content</main>
      
      <UserDashboard server:defer>
        <div slot="fallback" class="skeleton">Loading dashboard...</div>
      </UserDashboard>
      ```
      
      ## Build Optimization
      
      ### Bundle Analysis
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      
      export default defineConfig({
        vite: {
          build: {
            // Analyze bundle
            rollupOptions: {
              output: {
                manualChunks: {
                  // Group vendor chunks
                  'react-vendor': ['react', 'react-dom'],
                  'utils': ['date-fns', 'lodash-es'],
                },
              },
            },
          },
        },
      });
      ```
      
      ### Prefetch Strategies
      
      ```typescript
      // astro.config.mjs
      export default defineConfig({
        prefetch: {
          // Prefetch links on hover (default for View Transitions)
          defaultStrategy: 'hover',
      
          // Or be more aggressive
          // defaultStrategy: 'viewport',  // Prefetch when link enters viewport
          // defaultStrategy: 'load',      // Prefetch all links on page load
      
          // Prefetch all same-origin links
          prefetchAll: false,
        },
      });
      ```
      
      ```astro
      <!-- Per-link prefetch control -->
      <a href="/about" data-astro-prefetch="hover">Hover to prefetch</a>
      <a href="/important" data-astro-prefetch="viewport">Prefetch when visible</a>
      <a href="/critical" data-astro-prefetch="load">Prefetch immediately</a>
      <a href="/external" data-astro-prefetch="false">Never prefetch</a>
      ```
      
      ### Image Service Configuration
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      
      export default defineConfig({
        image: {
          // Use Sharp (default, best quality)
          service: { entrypoint: 'astro/assets/services/sharp' },
      
          // Remote image domains
          domains: ['cdn.example.com', 'images.unsplash.com'],
      
          // Remote patterns (more granular)
          remotePatterns: [
            {
              protocol: 'https',
              hostname: '**.example.com',
              pathname: '/images/**',
            },
          ],
        },
      });
      ```
      
      ```astro
      ---
      import { Image, Picture } from 'astro:assets';
      import heroImage from '../assets/hero.jpg';
      ---
      
      <!-- Optimized image with automatic format conversion -->
      <Image
        src={heroImage}
        alt="Hero image"
        width={1200}
        height={600}
        quality={80}
        format="avif"
        loading="eager"             <!-- Above fold: eager, below fold: lazy (default) -->
      />
      
      <!-- Responsive picture with multiple formats -->
      <Picture
        src={heroImage}
        formats={['avif', 'webp']}
        widths={[400, 800, 1200]}
        sizes="(max-width: 768px) 100vw, 1200px"
        alt="Responsive hero"
      />
      
      <!-- Remote image (must be in domains/remotePatterns) -->
      <Image
        src="https://cdn.example.com/photo.jpg"
        alt="Remote image"
        width={800}
        height={400}
        inferSize                   <!-- Auto-detect dimensions -->
      />
      ```
      
      ### Compression and Performance
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import compress from 'astro-compress';
      
      export default defineConfig({
        integrations: [
          compress({
            CSS: true,
            HTML: true,
            Image: true,
            JavaScript: true,
            SVG: true,
          }),
        ],
        compressHTML: true,          // Built-in HTML minification
        build: {
          inlineStylesheets: 'auto', // Inline small CSS (<4KB)
        },
        vite: {
          build: {
            cssMinify: 'lightningcss',  // Faster CSS minification
          },
        },
      });
      ```
      
      ### Sitemap and SEO
      
      ```bash
      npx astro add sitemap
      ```
      
      ```typescript
      // astro.config.mjs
      import sitemap from '@astrojs/sitemap';
      
      export default defineConfig({
        site: 'https://example.com',
        integrations: [
          sitemap({
            filter: (page) => !page.includes('/admin'),
            changefreq: 'weekly',
            priority: 0.7,
            lastmod: new Date(),
            i18n: {
              defaultLocale: 'en',
              locales: {
                en: 'en-US',
                es: 'es-ES',
              },
            },
          }),
        ],
      });
      ```
      
      ```astro
      ---
      // src/layouts/BaseLayout.astro - SEO head tags
      interface Props {
        title: string;
        description: string;
        image?: string;
        canonicalURL?: string;
      }
      
      const {
        title,
        description,
        image = '/og-default.png',
        canonicalURL = Astro.url.href,
      } = Astro.props;
      ---
      
      <html>
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <title>{title}</title>
          <meta name="description" content={description} />
          <link rel="canonical" href={canonicalURL} />
      
          <!-- Open Graph -->
          <meta property="og:title" content={title} />
          <meta property="og:description" content={description} />
          <meta property="og:image" content={new URL(image, Astro.site)} />
          <meta property="og:url" content={canonicalURL} />
          <meta property="og:type" content="website" />
      
          <!-- Twitter -->
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content={title} />
          <meta name="twitter:description" content={description} />
          <meta name="twitter:image" content={new URL(image, Astro.site)} />
      
          <!-- Sitemap -->
          <link rel="sitemap" href="/sitemap-index.xml" />
        </head>
        <body>
          <slot />
        </body>
      </html>
      ```
      
    • islands-rendering.md 20.6 KB
      # Islands Architecture and Rendering Reference
      
      Deep dive into Astro's islands architecture, partial hydration, client directives, framework integration, and server islands.
      
      ## How Islands Architecture Works
      
      Astro renders all components to static HTML on the server by default. Interactive components ("islands") are selectively hydrated on the client, shipping JavaScript only for the parts of the page that need interactivity.
      
      ```
      Traditional SPA:
      ┌──────────────────────────────────────┐
      │           Full JavaScript App         │  ← All JS shipped
      │  ┌────┐ ┌────┐ ┌────┐ ┌────┐       │
      │  │Nav │ │Hero│ │Card│ │Form│       │
      │  └────┘ └────┘ └────┘ └────┘       │
      └──────────────────────────────────────┘
      
      Astro Islands:
      ┌──────────────────────────────────────┐
      │           Static HTML (zero JS)       │
      │  ┌────┐                              │
      │  │Nav │ ← Island (client:load)       │  ← Only island JS shipped
      │  └────┘                              │
      │  ┌────────────┐                      │
      │  │  Hero Text  │ ← Static HTML       │  ← No JS
      │  └────────────┘                      │
      │  ┌────┐ ┌────┐ ┌────┐              │
      │  │Card│ │Card│ │Card│ ← Static      │  ← No JS
      │  └────┘ └────┘ └────┘              │
      │  ┌──────┐                            │
      │  │ Form │ ← Island (client:visible) │  ← JS loaded on scroll
      │  └──────┘                            │
      └──────────────────────────────────────┘
      ```
      
      ### The Hydration Process
      
      1. **Server**: Astro renders ALL components (including React, Vue, Svelte) to HTML
      2. **Client**: Browser receives pure HTML - instant display, zero JS
      3. **Hydration**: Based on client directive, island JS is loaded and components become interactive
      4. **Result**: Page is visible immediately; interactivity loads progressively
      
      ```astro
      ---
      // This component renders to HTML on the server
      // Then hydrates with React on the client
      import SearchBar from '../components/SearchBar.tsx';
      ---
      
      <!-- Static HTML (no JS) -->
      <header>
        <h1>My Site</h1>
        <!-- React island - hydrates when visible -->
        <SearchBar client:visible placeholder="Search docs..." />
      </header>
      ```
      
      ## Client Directives Deep Dive
      
      ### client:load
      
      Hydrates immediately when the page loads. Highest priority.
      
      ```astro
      ---
      import AuthButton from '../components/AuthButton.tsx';
      import NavMenu from '../components/NavMenu.tsx';
      ---
      
      <!-- Use for: above-fold interactive elements that users interact with immediately -->
      <AuthButton client:load />
      <NavMenu client:load />
      ```
      
      **When to use:**
      - Navigation menus that must be interactive on page load
      - Authentication state indicators
      - Critical CTAs above the fold
      - Elements that must respond to first user interaction
      
      **When NOT to use:**
      - Content below the fold (use `client:visible`)
      - Non-critical widgets (use `client:idle`)
      - Large components that aren't immediately needed
      
      ### client:idle
      
      Hydrates after the page has finished loading and `requestIdleCallback` fires.
      
      ```astro
      ---
      import CommentSection from '../components/CommentSection.tsx';
      import NewsletterSignup from '../components/NewsletterSignup.vue';
      import ShareButtons from '../components/ShareButtons.tsx';
      ---
      
      <!-- Use for: important but not immediately critical interactivity -->
      <CommentSection client:idle postId={post.id} />
      <NewsletterSignup client:idle />
      <ShareButtons client:idle url={Astro.url} />
      ```
      
      **When to use:**
      - Comment sections
      - Newsletter signup forms
      - Social share buttons
      - Chat widgets
      - Analytics dashboards below hero
      
      **Behavior:**
      - Waits for `requestIdleCallback` (or `setTimeout` fallback after 200ms)
      - Doesn't block initial page render or first paint
      - Loads before user scrolls (unlike `client:visible`)
      
      ### client:visible
      
      Hydrates when the element enters the viewport (IntersectionObserver).
      
      ```astro
      ---
      import ImageCarousel from '../components/ImageCarousel.svelte';
      import InteractiveChart from '../components/InteractiveChart.tsx';
      import Testimonials from '../components/Testimonials.vue';
      ---
      
      <!-- Use for: below-fold content that's only needed when scrolled to -->
      <ImageCarousel client:visible images={gallery} />
      <InteractiveChart client:visible data={chartData} />
      <Testimonials client:visible />
      
      <!-- With rootMargin - preload 200px before visible -->
      <InteractiveChart client:visible={{rootMargin: "200px"}} data={chartData} />
      ```
      
      **When to use:**
      - Image carousels/galleries far down the page
      - Interactive charts and data visualizations
      - Testimonial sliders
      - Footer widgets
      - Any interactive content below the fold
      
      **Behavior:**
      - Uses IntersectionObserver to detect visibility
      - Zero JS loaded until element is about to enter viewport
      - Supports `rootMargin` option for preloading
      
      ### client:media
      
      Hydrates only when a CSS media query matches.
      
      ```astro
      ---
      import MobileMenu from '../components/MobileMenu.tsx';
      import DesktopSidebar from '../components/DesktopSidebar.tsx';
      import DarkModeToggle from '../components/DarkModeToggle.tsx';
      ---
      
      <!-- Only hydrate on mobile -->
      <MobileMenu client:media="(max-width: 768px)" />
      
      <!-- Only hydrate on desktop -->
      <DesktopSidebar client:media="(min-width: 1024px)" />
      
      <!-- Hydrate based on user preference -->
      <DarkModeToggle client:media="(prefers-color-scheme: dark)" />
      ```
      
      **When to use:**
      - Mobile-only navigation (hamburger menus)
      - Desktop-only sidebars with interactivity
      - Responsive components that differ dramatically by viewport
      - Reduced motion alternatives
      
      **Behavior:**
      - Checks media query on load; hydrates if matched
      - Also watches for changes (e.g., viewport resize triggers hydration)
      - If media query never matches, JS is never loaded
      
      ### client:only
      
      Skips server rendering entirely. Component renders ONLY on the client.
      
      ```astro
      ---
      import ThreeScene from '../components/ThreeScene.tsx';
      import MapComponent from '../components/Map.tsx';
      import CanvasEditor from '../components/CanvasEditor.svelte';
      ---
      
      <!-- MUST specify the framework -->
      <ThreeScene client:only="react" />
      <MapComponent client:only="react" />
      <CanvasEditor client:only="svelte" />
      
      <!-- Valid framework values: -->
      <!-- client:only="react" -->
      <!-- client:only="preact" -->
      <!-- client:only="vue" -->
      <!-- client:only="svelte" -->
      <!-- client:only="solid-js" -->
      <!-- client:only="lit" -->
      ```
      
      **When to use:**
      - WebGL / Three.js / Canvas components
      - Map libraries (Leaflet, Mapbox) that access `window`
      - Browser-only APIs (Web Audio, WebRTC, etc.)
      - Components that crash during SSR
      
      **Behavior:**
      - No HTML rendered on server (shows nothing until JS loads)
      - No hydration mismatch possible (no server HTML to diff)
      - Framework string is required so Astro knows which renderer to use
      
      ### No Directive (Static)
      
      Component renders to HTML only. Zero client-side JavaScript.
      
      ```astro
      ---
      import Card from '../components/Card.astro';
      import Footer from '../components/Footer.astro';
      import BlogPostPreview from '../components/BlogPostPreview.astro';
      ---
      
      <!-- Pure HTML, no JS ever -->
      <Card title="Hello" description="World" />
      <Footer />
      <BlogPostPreview post={post} />
      ```
      
      **When to use:**
      - Content display (cards, headers, footers)
      - Anything that doesn't need user interaction
      - Layout components
      - Most of your page (aim for 80%+ static)
      
      ## Framework Integration
      
      ### Multi-framework Setup
      
      ```typescript
      // astro.config.mjs
      import { defineConfig } from 'astro/config';
      import react from '@astrojs/react';
      import vue from '@astrojs/vue';
      import svelte from '@astrojs/svelte';
      import solid from '@astrojs/solid-js';
      import preact from '@astrojs/preact';
      
      export default defineConfig({
        integrations: [
          react({
            include: ['**/react/**'],    // Only process files in react/ dirs
          }),
          preact({
            include: ['**/preact/**'],   // Disambiguate from React
          }),
          vue(),
          svelte(),
          solid({
            include: ['**/solid/**'],
          }),
        ],
      });
      ```
      
      ### File Organization for Multi-framework
      
      ```
      src/components/
      ├── react/              # React components (.tsx/.jsx)
      │   ├── Counter.tsx
      │   └── SearchBar.tsx
      ├── vue/                # Vue components (.vue)
      │   ├── TodoList.vue
      │   └── Modal.vue
      ├── svelte/             # Svelte components (.svelte)
      │   ├── Carousel.svelte
      │   └── Toggle.svelte
      ├── solid/              # Solid components (.tsx)
      │   └── DataGrid.tsx
      ├── Header.astro        # Astro (static)
      └── Footer.astro
      ```
      
      ### Using Multiple Frameworks in One Page
      
      ```astro
      ---
      import ReactNav from '../components/react/NavBar.tsx';
      import VueForm from '../components/vue/ContactForm.vue';
      import SvelteCarousel from '../components/svelte/Carousel.svelte';
      import Footer from '../components/Footer.astro';
      ---
      
      <ReactNav client:load user={user} />
      
      <main>
        <h1>Multi-framework Page</h1>
      
        <SvelteCarousel client:visible items={images} />
      
        <VueForm client:idle endpoint="/api/contact" />
      </main>
      
      <Footer />
      ```
      
      ## Sharing State Between Islands
      
      Islands are isolated by default. Here are patterns for sharing state.
      
      ### Nanostores (Recommended)
      
      ```bash
      npm install nanostores @nanostores/react @nanostores/vue @nanostores/svelte
      ```
      
      ```typescript
      // src/stores/cart.ts
      import { atom, map, computed } from 'nanostores';
      
      // Simple atom
      export const isMenuOpen = atom(false);
      
      // Map (object store)
      export interface CartItem {
        id: string;
        name: string;
        price: number;
        quantity: number;
      }
      
      export const cartItems = map<Record<string, CartItem>>({});
      
      // Computed (derived state)
      export const cartTotal = computed(cartItems, (items) => {
        return Object.values(items).reduce(
          (sum, item) => sum + item.price * item.quantity,
          0
        );
      });
      
      // Actions
      export function addToCart(item: CartItem) {
        const existing = cartItems.get()[item.id];
        if (existing) {
          cartItems.setKey(item.id, {
            ...existing,
            quantity: existing.quantity + 1,
          });
        } else {
          cartItems.setKey(item.id, { ...item, quantity: 1 });
        }
      }
      
      export function removeFromCart(id: string) {
        const items = { ...cartItems.get() };
        delete items[id];
        cartItems.set(items);
      }
      ```
      
      ```tsx
      // React component using the store
      import { useStore } from '@nanostores/react';
      import { cartItems, cartTotal, addToCart } from '../stores/cart';
      
      export function CartButton() {
        const $items = useStore(cartItems);
        const $total = useStore(cartTotal);
        const count = Object.keys($items).length;
      
        return (
          <button>
            Cart ({count}) - ${$total.toFixed(2)}
          </button>
        );
      }
      ```
      
      ```svelte
      <!-- Svelte component using same store -->
      <script>
        import { cartItems, cartTotal } from '../stores/cart';
      </script>
      
      <div>
        {#each Object.values($cartItems) as item}
          <p>{item.name}: ${item.price} x {item.quantity}</p>
        {/each}
        <p>Total: ${$cartTotal.toFixed(2)}</p>
      </div>
      ```
      
      ### Custom Events
      
      ```astro
      ---
      // For simple one-way communication between islands
      ---
      
      <script>
        // Dispatch from any island
        document.dispatchEvent(new CustomEvent('cart:add', {
          detail: { id: '123', name: 'Widget', price: 9.99 }
        }));
      
        // Listen in any island
        document.addEventListener('cart:add', (e: CustomEvent) => {
          console.log('Added to cart:', e.detail);
        });
      </script>
      ```
      
      ### URL State
      
      ```typescript
      // Use URL search params for shareable state
      function updateFilter(key: string, value: string) {
        const url = new URL(window.location.href);
        url.searchParams.set(key, value);
        window.history.pushState({}, '', url);
      
        // Notify other islands
        document.dispatchEvent(new CustomEvent('url:change', {
          detail: Object.fromEntries(url.searchParams),
        }));
      }
      ```
      
      ## Performance Budgets
      
      ### Measuring Island Size
      
      ```typescript
      // astro.config.mjs - analyze bundle
      import { defineConfig } from 'astro/config';
      import { visualizer } from 'rollup-plugin-visualizer';
      
      export default defineConfig({
        vite: {
          plugins: [
            visualizer({
              filename: './dist/stats.html',
              gzipSize: true,
              brotliSize: true,
            }),
          ],
        },
      });
      ```
      
      ### Bundle Size Guidelines
      
      | Component Type | Target Size (gzipped) | Strategy |
      |---------------|----------------------|----------|
      | Critical island (client:load) | < 20 KB | Minimize dependencies |
      | Deferred island (client:idle) | < 50 KB | Acceptable, loads after paint |
      | Lazy island (client:visible) | < 100 KB | OK for rich interactive content |
      | Full SPA island (client:only) | < 200 KB | Consider code splitting |
      
      ### Optimization Strategies
      
      ```typescript
      // 1. Prefer Preact over React for smaller islands
      import preact from '@astrojs/preact';
      
      // 2. Dynamic imports for heavy dependencies
      const Chart = lazy(() => import('./HeavyChart'));
      
      // 3. Tree-shakeable imports
      import { format } from 'date-fns/format';        // Good: specific import
      // import { format } from 'date-fns';             // Bad: imports everything
      
      // 4. Use Astro's built-in Image optimization
      import { Image } from 'astro:assets';
      ```
      
      ## Server Islands (Astro 5)
      
      Server islands allow you to defer rendering of specific components to after the initial page response, enabling personalized content within cached pages.
      
      ### How Server Islands Work
      
      ```
      Request Flow:
      1. Edge/CDN serves cached static HTML instantly
      2. Page displays with fallback content for server islands
      3. Server islands fetch their content via separate requests
      4. Dynamic content streams in and replaces fallbacks
      
      ┌─────────────────────────────────┐
      │  Cached Static Page (CDN)       │
      │                                 │
      │  ┌──────────────────────┐      │
      │  │ Static Header         │      │  ← Cached
      │  └──────────────────────┘      │
      │  ┌──────────────────────┐      │
      │  │ server:defer          │      │  ← Fetched separately
      │  │ (user-specific data)  │      │     after page load
      │  └──────────────────────┘      │
      │  ┌──────────────────────┐      │
      │  │ Static Content        │      │  ← Cached
      │  └──────────────────────┘      │
      └─────────────────────────────────┘
      ```
      
      ### Basic Usage
      
      ```astro
      ---
      // src/components/UserGreeting.astro
      const user = await getUser(Astro.cookies.get('session'));
      ---
      
      <div>
        <p>Welcome back, {user.name}!</p>
        <p>You have {user.notifications} new notifications.</p>
      </div>
      ```
      
      ```astro
      ---
      // src/pages/index.astro
      import UserGreeting from '../components/UserGreeting.astro';
      import ProductRecommendations from '../components/ProductRecommendations.astro';
      ---
      
      <html>
        <body>
          <h1>Welcome to our Store</h1>
      
          <!-- This component renders on the server AFTER the page is sent -->
          <UserGreeting server:defer>
            <!-- Fallback shown while server island loads -->
            <p slot="fallback">Loading your profile...</p>
          </UserGreeting>
      
          <!-- Another server island -->
          <ProductRecommendations server:defer>
            <div slot="fallback" class="skeleton-grid">
              <!-- Skeleton placeholder -->
            </div>
          </ProductRecommendations>
      
          <!-- This is static, served from cache -->
          <footer>Static footer content</footer>
        </body>
      </html>
      ```
      
      ### Server Islands with Props
      
      ```astro
      ---
      // Server islands can receive serializable props
      import PricingTable from '../components/PricingTable.astro';
      ---
      
      <!-- Props are encrypted and sent with the deferred request -->
      <PricingTable
        server:defer
        productId="abc123"
        region={Astro.locals.region}
      >
        <p slot="fallback">Loading pricing...</p>
      </PricingTable>
      ```
      
      ### Caching Strategy with Server Islands
      
      ```typescript
      // astro.config.mjs
      export default defineConfig({
        output: 'server',
        adapter: cloudflare(),
      });
      ```
      
      ```astro
      ---
      // src/pages/product/[id].astro
      // The page itself can be cached aggressively
      Astro.response.headers.set('Cache-Control', 'public, max-age=3600');
      
      import ProductDetails from '../components/ProductDetails.astro';
      import UserReviews from '../components/UserReviews.astro';
      import AddToCart from '../components/AddToCart.astro';
      ---
      
      <!-- Static product info (cached) -->
      <ProductDetails productId={Astro.params.id} />
      
      <!-- Dynamic, personalized (server island) -->
      <AddToCart server:defer productId={Astro.params.id}>
        <button slot="fallback" disabled>Loading...</button>
      </AddToCart>
      
      <!-- Dynamic, frequently updated (server island) -->
      <UserReviews server:defer productId={Astro.params.id}>
        <p slot="fallback">Loading reviews...</p>
      </UserReviews>
      ```
      
      ## Slot Patterns
      
      ### Passing Astro Content into Framework Islands
      
      ```astro
      ---
      import ReactAccordion from '../components/react/Accordion.tsx';
      ---
      
      <!-- Astro content becomes children in React -->
      <ReactAccordion client:visible title="FAQ">
        <p>This HTML is passed as children to the React component.</p>
        <ul>
          <li>Static content rendered by Astro</li>
          <li>Hydrated by React when visible</li>
        </ul>
      </ReactAccordion>
      ```
      
      ```tsx
      // React component receiving Astro slot content
      interface AccordionProps {
        title: string;
        children: React.ReactNode;  // Astro slot content arrives as children
      }
      
      export function Accordion({ title, children }: AccordionProps) {
        const [isOpen, setIsOpen] = useState(false);
      
        return (
          <div>
            <button onClick={() => setIsOpen(!isOpen)}>{title}</button>
            {isOpen && <div>{children}</div>}
          </div>
        );
      }
      ```
      
      ### Named Slots with Framework Components
      
      ```astro
      ---
      import ReactCard from '../components/react/Card.tsx';
      ---
      
      <!-- Named slots map to props in React -->
      <ReactCard client:idle>
        <h2 slot="header">Card Title</h2>
        <p>Default slot content (becomes children)</p>
        <span slot="footer">Card footer</span>
      </ReactCard>
      ```
      
      ```tsx
      // React component with named slots
      interface CardProps {
        header?: React.ReactNode;
        footer?: React.ReactNode;
        children: React.ReactNode;
      }
      
      export function Card({ header, footer, children }: CardProps) {
        return (
          <div className="card">
            {header && <div className="card-header">{header}</div>}
            <div className="card-body">{children}</div>
            {footer && <div className="card-footer">{footer}</div>}
          </div>
        );
      }
      ```
      
      ### Nested Islands
      
      ```astro
      ---
      import ReactWrapper from '../components/react/Wrapper.tsx';
      import SvelteWidget from '../components/svelte/Widget.svelte';
      ---
      
      <!-- Nested islands hydrate independently -->
      <ReactWrapper client:load>
        <!-- This Svelte component hydrates separately from the React wrapper -->
        <SvelteWidget client:visible count={5} />
      </ReactWrapper>
      ```
      
      **Important:** Nested islands are NOT nested in the JavaScript sense. Each island hydrates independently. The React wrapper doesn't "own" the Svelte widget - they just happen to be visually nested in the HTML.
      
      ## Advanced Patterns
      
      ### Conditional Hydration
      
      ```astro
      ---
      import HeavyEditor from '../components/react/Editor.tsx';
      const isEditor = Astro.url.searchParams.has('edit');
      ---
      
      <!-- Only include the island if editing -->
      {isEditor ? (
        <HeavyEditor client:load content={content} />
      ) : (
        <div class="content" set:html={renderedContent} />
      )}
      ```
      
      ### Island with Loading State
      
      ```tsx
      // React island with built-in loading state
      import { useState, useEffect } from 'react';
      
      export function DataWidget({ endpoint }: { endpoint: string }) {
        const [data, setData] = useState(null);
        const [loading, setLoading] = useState(true);
      
        useEffect(() => {
          fetch(endpoint)
            .then((r) => r.json())
            .then((d) => { setData(d); setLoading(false); });
        }, [endpoint]);
      
        if (loading) return <div class="skeleton" />;
        return <div>{/* render data */}</div>;
      }
      ```
      
      ### Transition-aware Islands
      
      ```tsx
      // React island that reinitializes on View Transition navigation
      import { useEffect } from 'react';
      
      export function PageTracker() {
        useEffect(() => {
          // This runs on initial hydration AND after View Transition navigations
          const handler = () => {
            console.log('Page changed:', window.location.pathname);
          };
      
          document.addEventListener('astro:page-load', handler);
          return () => document.removeEventListener('astro:page-load', handler);
        }, []);
      
        return null;
      }
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
  • SKILL.md 15.6 KB
    ---
    name: astro-ops
    description: "Astro framework patterns, islands architecture, content collections, rendering strategies, and deployment. Use for: astro, islands architecture, content collections, astro cloudflare, view transitions, partial hydration, astrojs, SSG, SSR, hybrid rendering, astro adapter."
    when_to_use: "Use when building or deploying an Astro site — e.g. 'which rendering mode: SSG, SSR, or hybrid', 'add a React island with client:load', 'set up content collections', 'deploy Astro to Cloudflare/Vercel'. Covers islands architecture, view transitions, and adapters; not general React/Next.js work."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: typescript-ops, tailwind-ops, javascript-ops
    ---
    
    # Astro Operations
    
    > Facts verified as of 2026-07.
    
    Comprehensive patterns for Astro framework development: islands architecture, content collections, rendering strategies, view transitions, and multi-platform deployment.
    
    ## Rendering Strategy Decision Tree
    
    ```
    Which rendering strategy?
    │
    ├─ Is content mostly static (blog, docs, marketing)?
    │  ├─ YES → Does it change less than daily?
    │  │  ├─ YES → SSG (output: 'static')
    │  │  │        Fastest TTFB, CDN-cacheable, zero runtime cost
    │  │  └─ NO  → Hybrid (output: 'hybrid')
    │  │           Default static + opt-in SSR per route
    │  └─ NO  → Does every page need personalization?
    │     ├─ YES → SSR (output: 'server')
    │     │        Dynamic per-request, auth-aware, real-time data
    │     └─ NO  → Hybrid (output: 'hybrid')
    │              Static shell + server islands for dynamic parts
    │
    ├─ Does the app need real-time interactivity (dashboard, SPA)?
    │  ├─ YES → Is it a full SPA with client-side routing?
    │  │  ├─ YES → Consider React/Vue SPA instead, or Astro + client:only
    │  │  └─ NO  → Hybrid + islands architecture
    │  │           Interactive islands in static pages
    │  └─ NO  → SSG (output: 'static')
    │
    ├─ Build time concerns (>10k pages)?
    │  ├─ YES → Hybrid with on-demand rendering
    │  │        Prerender popular pages, SSR the long tail
    │  └─ NO  → SSG handles it fine
    │
    └─ Need edge computing (low latency globally)?
       ├─ YES → SSR + Cloudflare/Vercel Edge adapter
       └─ NO  → SSR + Node adapter or SSG
    ```
    
    ### Configuration
    
    ```typescript
    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    
    // SSG (default) - all pages prerendered at build time
    export default defineConfig({
      output: 'static',
    });
    
    // SSR - all pages rendered on request
    export default defineConfig({
      output: 'server',
      adapter: cloudflare(), // or vercel(), netlify(), node()
    });
    
    // Hybrid - static default, opt-in SSR per page
    export default defineConfig({
      output: 'hybrid',
      adapter: cloudflare(),
    });
    ```
    
    ```astro
    ---
    // In hybrid mode, opt OUT of prerendering for specific pages:
    export const prerender = false;
    // In SSR mode, opt IN to prerendering:
    export const prerender = true;
    ---
    ```
    
    ## Islands Architecture Quick Reference
    
    | Directive | Hydrates When | JS Shipped | Use Case |
    |-----------|--------------|------------|----------|
    | `client:load` | Immediately on page load | Full bundle | Above-fold interactive (nav, hero CTA) |
    | `client:idle` | After page is idle (`requestIdleCallback`) | Full bundle | Below-fold interactive (comment form, chat) |
    | `client:visible` | When scrolled into viewport | Full bundle | Far-down-page (footer widget, carousel) |
    | `client:media` | When media query matches | Full bundle | Mobile-only nav, responsive components |
    | `client:only="react"` | Immediately, skip SSR entirely | Full bundle | Components that can't SSR (canvas, WebGL) |
    | (none) | Never - static HTML only | Zero JS | Static content, cards, headers |
    
    ```astro
    ---
    import NavBar from '../components/NavBar.tsx';
    import CommentForm from '../components/CommentForm.tsx';
    import ImageCarousel from '../components/ImageCarousel.svelte';
    import MobileMenu from '../components/MobileMenu.vue';
    import ThreeScene from '../components/ThreeScene.tsx';
    ---
    
    <!-- Loads immediately - critical interactivity -->
    <NavBar client:load />
    
    <!-- Loads after page is idle - non-critical -->
    <CommentForm client:idle />
    
    <!-- Loads when scrolled into view - lazy -->
    <ImageCarousel client:visible />
    
    <!-- Loads only on mobile -->
    <MobileMenu client:media="(max-width: 768px)" />
    
    <!-- Client-only, no SSR (WebGL can't run on server) -->
    <ThreeScene client:only="react" />
    ```
    
    ## Content Collections Quick Start
    
    ### Define Schema
    
    ```typescript
    // src/content.config.ts (Astro 5) or src/content/config.ts (Astro 4)
    import { defineCollection, z, reference } from 'astro:content';
    import { glob } from 'astro/loaders';
    
    const blog = defineCollection({
      loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
      schema: z.object({
        title: z.string(),
        description: z.string().max(160),
        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: reference('authors'), // Reference another collection
      }),
    });
    
    const authors = defineCollection({
      loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
      schema: z.object({
        name: z.string(),
        avatar: z.string(),
        bio: z.string(),
        socials: z.object({
          twitter: z.string().optional(),
          github: z.string().optional(),
        }).optional(),
      }),
    });
    
    export const collections = { blog, authors };
    ```
    
    ### Query Collections
    
    ```astro
    ---
    import { getCollection, getEntry } from 'astro:content';
    
    // Get all non-draft blog posts, sorted by date
    const posts = (await getCollection('blog', ({ data }) => !data.draft))
      .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
    
    // Get a single entry
    const post = await getEntry('blog', 'my-first-post');
    
    // Resolve a reference
    const author = await getEntry(post.data.author);
    
    // Render content
    const { Content, headings } = await post.render();
    ---
    
    <Content />
    ```
    
    ### Content Collections vs External CMS
    
    | Criterion | Content Collections | External CMS (Payload, etc.) |
    |-----------|--------------------|-----------------------------|
    | Content type | Local markdown/MDX, docs, blogs | Relational data models |
    | Authors | Developers (version-controlled) | Editors (admin UI, multi-user auth) |
    | Validation | Type-safe via Zod at build time | CMS-side schemas + API contracts |
    | Update cadence | Deploys with the site | Independent of deployments |
    | API needs | None (build-time queries) | REST/GraphQL for other consumers |
    | Workflow | Git PRs, simple review | Editorial workflows, drafts, roles |
    
    Rule of thumb: start with Content Collections; reach for a CMS only when non-developers need to publish without a deploy, or when content is genuinely relational.
    
    ## Project Structure Reference
    
    ```
    project-root/
    ├── astro.config.mjs          # Astro configuration
    ├── tsconfig.json              # TypeScript config (extends astro/tsconfigs)
    ├── package.json
    ├── public/                    # Static assets (copied as-is)
    │   ├── favicon.svg
    │   ├── robots.txt
    │   └── og-image.png
    ├── src/
    │   ├── pages/                 # File-based routing
    │   │   ├── index.astro        # → /
    │   │   ├── about.astro        # → /about
    │   │   ├── blog/
    │   │   │   ├── index.astro    # → /blog
    │   │   │   └── [slug].astro   # → /blog/:slug (dynamic)
    │   │   ├── api/
    │   │   │   └── search.ts      # → /api/search (API endpoint)
    │   │   └── [...slug].astro    # → catch-all/404
    │   ├── layouts/
    │   │   ├── BaseLayout.astro   # HTML shell, <head>, global styles
    │   │   └── BlogPost.astro     # Blog post layout
    │   ├── components/
    │   │   ├── Header.astro       # Static Astro component
    │   │   ├── Footer.astro
    │   │   ├── NavBar.tsx         # React island
    │   │   └── Counter.svelte     # Svelte island
    │   ├── content/               # Content collections source files
    │   │   ├── blog/
    │   │   │   ├── post-one.md
    │   │   │   └── post-two.mdx
    │   │   └── authors/
    │   │       └── jane.json
    │   ├── content.config.ts      # Collection schemas (Astro 5)
    │   ├── middleware.ts           # Request/response middleware
    │   ├── styles/
    │   │   └── global.css
    │   └── lib/                   # Shared utilities
    │       ├── utils.ts
    │       └── constants.ts
    └── .env                       # Environment variables
    ```
    
    ## View Transitions Quick Reference
    
    ```astro
    ---
    // src/layouts/BaseLayout.astro
    import { ViewTransitions } from 'astro:transitions';
    ---
    
    <html>
      <head>
        <ViewTransitions />
      </head>
      <body>
        <slot />
      </body>
    </html>
    ```
    
    ### Transition Directives
    
    ```astro
    <!-- Persist element across pages (keeps state, avoids re-render) -->
    <audio transition:persist id="player">
      <source src="/music.mp3" />
    </audio>
    
    <!-- Named transition for animation pairing -->
    <img transition:name="hero" src={post.heroImage} />
    
    <!-- Custom animation -->
    <div transition:animate="slide">Content</div>
    <div transition:animate="fade">Content</div>
    <div transition:animate="none">No animation</div>
    
    <!-- Persist with name (for multiple persistent elements) -->
    <video transition:persist="media-player" />
    ```
    
    ### Lifecycle Events
    
    ```astro
    <script>
      document.addEventListener('astro:before-preparation', (e) => {
        // Before new page is fetched - cancel navigation, show loading
      });
    
      document.addEventListener('astro:after-preparation', (e) => {
        // New page fetched, before swap
      });
    
      document.addEventListener('astro:before-swap', (e) => {
        // Customize DOM swap behavior
      });
    
      document.addEventListener('astro:after-swap', () => {
        // DOM updated - reinitialize scripts
      });
    
      document.addEventListener('astro:page-load', () => {
        // Page fully loaded (fires on initial + every navigation)
        // Use this instead of DOMContentLoaded with View Transitions
      });
    </script>
    ```
    
    ### Back/Forward Handling
    
    ```typescript
    // astro.config.mjs
    export default defineConfig({
      prefetch: {
        prefetchAll: true,         // Prefetch all links on hover
        defaultStrategy: 'hover',  // 'hover' | 'tap' | 'viewport' | 'load'
      },
    });
    ```
    
    ```astro
    <!-- Per-link prefetch control -->
    <a href="/about" data-astro-prefetch>Prefetch on hover (default)</a>
    <a href="/blog" data-astro-prefetch="viewport">Prefetch when visible</a>
    <a href="/contact" data-astro-prefetch="load">Prefetch immediately</a>
    <a href="/external" data-astro-prefetch="false">No prefetch</a>
    ```
    
    ## Deployment Decision Tree
    
    ```
    Where to deploy?
    │
    ├─ Need edge computing + Cloudflare ecosystem (KV, D1, R2)?
    │  └─ Cloudflare Pages/Workers
    │     Adapter: @astrojs/cloudflare
    │     Best for: Global edge, Workers bindings, cost-effective
    │
    ├─ Need serverless + Vercel ecosystem (ISR, analytics)?
    │  └─ Vercel
    │     Adapter: @astrojs/vercel
    │     Best for: Next.js migration, image optimization, ISR
    │
    ├─ Need serverless + Netlify ecosystem (forms, identity)?
    │  └─ Netlify
    │     Adapter: @astrojs/netlify
    │     Best for: JAMstack, built-in forms, split testing
    │
    ├─ Need full server control (Docker, custom runtime)?
    │  └─ Node.js (standalone or Express/Fastify)
    │     Adapter: @astrojs/node
    │     Best for: Self-hosted, WebSocket, long-running processes
    │
    └─ Pure static site (no SSR needed)?
       └─ Any static host (GitHub Pages, S3, Cloudflare Pages)
          No adapter needed, output: 'static'
          Best for: Blogs, docs, marketing sites
    ```
    
    ### Adapter Installation
    
    ```bash
    # Cloudflare
    npx astro add cloudflare
    
    # Vercel
    npx astro add vercel
    
    # Netlify
    npx astro add netlify
    
    # Node.js
    npx astro add node
    ```
    
    ## Common Gotchas
    
    | Gotcha | Why | Fix |
    |--------|-----|-----|
    | Hydration mismatch errors | Server HTML differs from client render (dates, random IDs, browser APIs) | Use `client:only` for browser-dependent components, or ensure deterministic rendering |
    | `import.meta.env` undefined in client | Only `PUBLIC_` prefixed vars are exposed to client-side code | Rename to `PUBLIC_MY_VAR` or pass via props from server |
    | Dynamic routes 404 in SSG | `getStaticPaths()` not returning all possible params | Ensure `getStaticPaths()` returns every valid path, or switch to hybrid/SSR |
    | Images not optimizing | Using `<img>` instead of Astro's `<Image />` component | Import from `astro:assets`: `import { Image } from 'astro:assets'` and use local imports for src |
    | SSR fails without adapter | `output: 'server'` or `'hybrid'` requires a deployment adapter | Install adapter: `npx astro add cloudflare` (or vercel, netlify, node) |
    | MDX components not rendering | Custom components not passed to MDX content | Pass components via `<Content components={{ MyComponent }} />` or use `astro.config.mjs` MDX config |
    | Content collection schema changes not reflected | Type generation is cached, stale `.astro` types | Run `astro sync` to regenerate types, restart dev server |
    | `client:*` on Astro components | Client directives only work on framework components (React, Vue, Svelte) | Astro components are static-only; extract interactive parts to a framework component |
    | `document` / `window` is not defined | Server-side code cannot access browser globals | Guard with `if (typeof window !== 'undefined')` or move to `client:only` |
    | Styles leaking between components | Using global CSS instead of scoped styles | Use `<style>` (scoped by default in .astro) or `<style is:global>` intentionally |
    | View Transitions break scripts | `DOMContentLoaded` only fires once with View Transitions | Use `astro:page-load` event instead, which fires on every navigation |
    | Env vars missing in production | `.env` not loaded or platform env vars not configured | Use `envField` in astro.config.mjs for validation; set vars in platform dashboard |
    
    ## Production Security Checklist
    
    For every production deployment, address:
    
    - **CSP headers** - configure a restrictive `Content-Security-Policy` (see middleware patterns in `references/deployment.md`)
    - **Remote image restrictions** - enforce explicit `image.domains` / `remotePatterns` allow-lists; never derive image URLs from user input (SSRF risk)
    - **Host header validation** - verify the request host matches expected domains in middleware (SSR/hybrid only)
    - **Secrets management** - on Cloudflare, use Workers Bindings (`wrangler secret put`), not env vars baked into code; elsewhere use platform secret stores
    - **HTTPS only** - ensure all external resources (scripts, images, fonts) load over HTTPS
    - **Input validation** - sanitize all user input in SSR contexts (query params, form bodies, cookies)
    
    ## Reference Files
    
    | File | Contents | Lines |
    |------|----------|-------|
    | `references/content-collections.md` | Schema patterns, Zod types, querying, MDX, content layer API, migrations | ~500 |
    | `references/islands-rendering.md` | Islands deep dive, client directives, framework integration, server islands | ~550 |
    | `references/deployment.md` | Cloudflare/Vercel/Netlify/Node adapters, env vars, optimization | ~500 |
    
    ## See Also
    
    - **typescript-ops** - TypeScript patterns used throughout Astro projects
    - **tailwind-ops** - Tailwind CSS integration with Astro (`@astrojs/tailwind`)
    - **javascript-ops** - Core JS patterns for client-side island code
    - **container-orchestration** - Docker patterns for self-hosted Astro (Node adapter)
    - [Astro Documentation](https://docs.astro.build)
    - [Astro Integration Guide](https://docs.astro.build/en/guides/integrations-guide/)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related