Skill: encryptshawn/astro-advanced (version 1.0.0)

--- name: astro-advanced description: > Comprehensive skill for building, configuring, and troubleshooting Astro projects. Use this skill whenever the user mentions Astro, .astro files, Astro config, Astro islands, partial hydration, client directives (client:load, client:visible, client:idle), Astro SSR, Astro adapters (Node, Vercel, Cloudflare, Netlify), Astro content collections, Astro layouts, hybrid rendering, astro.config.mjs, or any framework integration with Astro (Vue, React, Svelte). Also trigger when the user is debugging broken Astro builds, hydration mismatches, routing issues, base path problems, or deployment failures on static/serverless hosts. Even if the user doesn't say "Astro" explicitly, trigger if they reference .astro file syntax, frontmatter fences (---), or island architecture concepts. This skill covers project setup, rendering modes (SSG/SSR/hybrid), SSR caching, SEO, layouts, templates, Vue island integration, content collections, data fetching, deployment, auth, performance, and troubleshooting. --- # Astro Advanced Skill This skill provides production-grade guidance for Astro projects — from initial scaffolding through deployment, caching, and performance tuning. It covers the patterns that actually matter in real projects and the mistakes that actually happen. ## How to use this skill 1. **Read this file first** for the core workflow and decision tree. 2. **Consult the reference files** in `references/` for deep dives on specific topics: - `references/setup-and-structure.md` — Project creation, file structure, config, adapters - `references/rendering-modes.md` — SSG vs SSR vs Hybrid, when to use each, caching strategies - `references/seo.md` — Meta tags, Open Graph, JSON-LD, sitemaps, canonical URLs - `references/islands-and-vue.md` — Island architecture, client directives, Vue/React/Svelte integration - `references/content-and-data.md` — Content collections, data fetching, dynamic routes - `references/deployment.md` — Adapters, static hosts, serverless, environment variables - `references/performance.md` — Image optimization, bundle analysis, hydration control - `references/troubleshooting.md` — Common errors and fixes organized by symptom ## Core decision tree When helping with an Astro project, follow this sequence: ### 1. Identify the rendering strategy first This is the single most important decision in any Astro project. Everything else flows from it. - **Pure static site (blog, docs, marketing)?** → SSG (default). No adapter needed. - **Needs user-specific data, auth, or real-time content?** → SSR with an adapter. - **Mostly static but a few dynamic pages?** → Hybrid mode. Set `output: 'static'` in config and use `export const prerender = false` on dynamic pages. ### 2. Pick the right adapter Only needed for SSR or hybrid: - **Vercel** → `@astrojs/vercel` (serverless or edge) - **Netlify** → `@astrojs/netlify` - **Cloudflare Pages** → `@astrojs/cloudflare` - **Self-hosted Node** → `@astrojs/node` (standalone or middleware) ### 3. Set up integrations Add only what you need. Each integration is a potential build-time dependency: ```bash # Common additions npx astro add vue # Vue islands npx astro add tailwind # Tailwind CSS npx astro add mdx # MDX support npx astro add sitemap # Auto sitemap generation ``` ### 4. Establish content strategy - **Few pages, hand-authored** → Regular `.astro` pages in `/src/pages` - **Blog/docs with structured content** → Content collections with Zod schemas - **CMS-driven** → Fetch at build time (SSG) or runtime (SSR) ### 5. Apply SEO from the start Don't bolt it on later. See `references/seo.md` for the full pattern, but at minimum: - Create a reusable `<SEO>` component for head tags - Set up canonical URLs - Add structured data (JSON-LD) for key pages - Generate sitemap via `@astrojs/sitemap` ## Key patterns to always follow ### Layout pattern Every page should use a layout. Layouts handle `<html>`, `<head>`, and shared chrome: ```astro --- // src/layouts/Base.astro import SEO from '../components/SEO.astro'; const { title, description } = Astro.props; --- <html lang="en"> <head> <SEO title={title} description={description} /> </head> <body> <nav><!-- shared nav --></nav> <slot /> <footer><!-- shared footer --></footer> </body> </html> ``` ### Island pattern Static by default. Only hydrate what needs interactivity: ```astro <!-- Static: renders HTML, ships zero JS --> <Card title="Hello" /> <!-- Interactive: hydrates on load --> <Counter client:load /> <!-- Interactive: hydrates when visible (lazy) --> <ImageGallery client:visible /> ``` **The #1 Astro mistake**: forgetting `client:*` on a component that needs interactivity, then wondering why click handlers don't work. ### SSR caching pattern SSR without caching is just a slow website. Always pair SSR with a caching strategy: ```ts // In an SSR endpoint or page return new Response(JSON.stringify(data), { headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", "Content-Type": "application/json" } }); ``` ## When things go wrong Read `references/troubleshooting.md` for a symptom-based guide. The top 5 issues: 1. **"Component doesn't do anything"** → Missing `client:*` directive 2. **Build fails after adding integration** → Version mismatch, check `package.json` 3. **SSR returns 500** → Missing adapter or wrong `output` mode in config 4. **Broken links after deploy** → Base path not set, or trailing slash mismatch 5. **Hydration mismatch errors** → Server/client HTML differs (conditional rendering, dates, randomness) ## File output conventions When generating Astro project files: - Always include the frontmatter fence (`---`) even if empty - Use `.astro` extension for Astro components and pages - Place pages in `src/pages/`, components in `src/components/`, layouts in `src/layouts/` - Use TypeScript in frontmatter when the project uses TS - Include `astro.config.mjs` with only the integrations and settings actually needed

path: references/troubleshooting.md
content: # Troubleshooting Guide

Organized by symptom. Find what's broken, then follow the fix.

## Table of Contents
1. [Component doesn't do anything (no interactivity)](#no-interactivity)
2. [Hydration mismatch errors](#hydration-mismatch)
3. [Build fails after adding integration](#integration-build-fail)
4. [SSR returns 500 or blank page](#ssr-500)
5. [Broken links / 404 after deploy](#broken-links)
6. ["window is not defined" / "document is not defined"](#window-undefined)
7. [Content collection errors](#content-errors)
8. [Styles not applying](#styles-broken)
9. [Dynamic routes not working](#dynamic-routes)
10. [Environment variables undefined](#env-undefined)
11. [Slow builds](#slow-builds)
12. [CORS errors from API calls](#cors)
13. [Images not loading](#images-broken)
14. [TypeScript errors](#typescript)
15. [Dev server issues](#dev-server)

---

## 1. Component doesn't do anything (no interactivity) {#no-interactivity}

**Symptom**: Component renders HTML but buttons, inputs, event handlers don't work.

**Cause**: Missing `client:*` directive. Without it, Astro renders static HTML and ships zero JS.

**Fix**:
```astro
<!-- Before: static HTML only -->
<Counter />

<!-- After: hydrated and interactive -->
<Counter client:load />
```

**Checklist if you already have a client directive**:
- Is the directive on the right component? (Check parent vs child)
- Is the component a `.astro` file? (Astro components can't be hydrated — only framework components like `.vue`, `.tsx`, `.svelte`)
- Is the integration installed? (`npx astro add vue`)

---

## 2. Hydration mismatch errors {#hydration-mismatch}

**Symptom**: Console warns about hydration mismatch. UI might flicker or revert after load.

**Cause**: HTML rendered on the server differs from what the client renders.

**Common triggers**:
- Using `Date.now()`, `Math.random()`, or `new Date()` in the render template
- Reading `localStorage` or `window.innerWidth` during render
- Conditional rendering based on browser-only values
- Different timezone between server and client

**Fix pattern**:
```vue
<script setup>
import { ref, onMounted } from 'vue';

// Use a safe default for SSR
const width = ref(1024);

// Update with real value after mount (client only)
onMounted(() => {
  width.value = window.innerWidth;
});
</script>
```

**Nuclear option**: Use `client:only="vue"` to skip SSR entirely for that component. Only do this if you can't fix the mismatch.

---

## 3. Build fails after adding integration {#integration-build-fail}

**Symptom**: `npm run build` or `npm run dev` crashes after installing a new integration.

**Diagnosis**:
```bash
# Check for version conflicts
npm ls --depth=0

# Clear caches
rm -rf node_modules/.astro
rm -rf node_modules/.vite

# Reinstall
rm -rf node_modules
npm install
```

**Common causes**:
- **Integration version incompatible with Astro version**: Check the integration's docs for supported Astro versions
- **Conflicting peer dependencies**: Two integrations requiring different versions of a shared dependency
- **Config not updated**: `npx astro add` updates config automatically; manual install doesn't

**Fix**: Use `npx astro add <integration>` instead of `npm install`. It handles both package.json and astro.config.mjs.

---

## 4. SSR returns 500 or blank page {#ssr-500}

**Symptom**: SSR pages return HTTP 500 or render nothing.

**Checklist**:
1. **Is `output` set correctly?**
   ```js
   // astro.config.mjs
   export default defineConfig({
     output: 'server', // Must be 'server' for full SSR
   });
   ```

2. **Is an adapter installed?**
   ```bash
   npx astro add node  # or vercel, netlify, cloudflare
   ```

3. **Check server logs** — the actual error is in the terminal/server output, not the browser.

4. **Does the page have `export const prerender = false`?** If you're using hybrid mode (output: 'static'), SSR pages need this.

5. **Are environment variables available?** SSR runs on the server — check server-side env vars, not just build-time ones.

6. **Is `Astro.redirect()` returning correctly?**
   ```astro
   ---
   // Must RETURN the redirect
   if (!user) return Astro.redirect('/login');
   // Without return, execution continues
   ---
   ```

---

## 5. Broken links / 404 after deploy {#broken-links}

**Symptom**: Site works locally but links 404 in production.

**Check these in order**:

1. **Base path**:
   ```js
   // If hosting at https://example.com/docs/
   export default defineConfig({
     base: '/docs/',
   });
   ```
   All internal links must use the base. Use `import.meta.env.BASE_URL` for dynamic base.

2. **Trailing slash mismatch**:
   ```js
   export default defineConfig({
     trailingSlash: 'always', // Match your host's behavior
   });
   ```
   Netlify adds trailing slashes by default. Vercel doesn't. Mismatch = redirect loops or 404s.

3. **Case sensitivity**: macOS doesn't care about case; Linux does. `About.astro` creates `/About` on Linux, which is different from `/about`.

4. **SSR pages on static host**: If you deployed SSR pages to a static host (GitHub Pages, S3), they won't exist. Either prerender them or use an SSR-capable host.

---

## 6. "window is not defined" / "document is not defined" {#window-undefined}

**Symptom**: Build or SSR crashes with ReferenceError about browser globals.

**Cause**: Code that runs on the server is trying to use browser-only APIs.

**Where this happens**:
- Astro frontmatter (always runs on server)
- Framework component render functions (run on server during SSR)
- Third-party libraries that assume a browser environment

**Fixes**:

1. **Move to `onMounted` / `useEffect`** (only runs in browser):
   ```vue
   <script setup>
   import { onMounted } from 'vue';
   onMounted(() => {
     // Safe: only runs in browser
     const el = document.getElementById('my-element');
   });
   </script>
   ```

2. **Guard with typeof check**:
   ```ts
   if (typeof window !== 'undefined') {
     window.addEventListener('scroll', handler);
   }
   ```

3. **Use `client:only`** to skip SSR entirely:
   ```astro
   <MapComponent client:only="vue" />
   ```

4. **Lazy import browser-only libraries**:
   ```ts
   let lib;
   if (typeof window !== 'undefined') {
     lib = await import('browser-only-lib');
   }
   ```

---

## 7. Content collection errors {#content-errors}

### "Collection X not found"
- Does `src/content/config.ts` exist?
- Does it export the collection? `export const collections = { blog };`
- Does the folder name match? `src/content/blog/` for collection `blog`
- Run `npx astro sync` to regenerate types

### Schema validation error
```
[ERROR] blog/my-post.md frontmatter does not match collection schema
```
- Check the error details — it tells you which field failed
- Common: using `z.date()` instead of `z.coerce.date()` for frontmatter dates
- Common: missing required fields in frontmatter
- Common: wrong type (string where number expected)

### Content not updating in dev
- Restart the dev server after changing `config.ts`
- Run `npx astro sync` after schema changes
- Clear `.astro` cache: `rm -rf node_modules/.astro`

---

## 8. Styles not applying {#styles-broken}

### Scoped styles not working on child components
Astro scoped styles only apply to HTML in the current `.astro` file, not children.

```astro
<style>
  /* Only affects <h1> directly in THIS file, not in child components */
  h1 { color: red; }
</style>
```

**Fix**: Use `:global()` to target child HTML, or use `is:global` on the style tag:
```astro
<style>
  :global(.child-class) { color: red; }
</style>

<!-- Or make the entire block global -->
<style is:global>
  .applies-everywhere { color: red; }
</style>
```

### Tailwind not working
- Is the integration installed? `npx astro add tailwind`
- Is `@tailwind` imported in a global CSS file or layout?
- Check `tailwind.config.mjs` content paths include `.astro` files:
  ```js
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']
  ```

### Styles flash or disappear
- FOUC (Flash of Unstyled Content): Make sure CSS is in `<head>`, not dynamically injected
- Check that scoped styles aren't conflicting with global styles

---

## 9. Dynamic routes not working {#dynamic-routes}

### SSG: "getStaticPaths() is required"
```
[ERROR] [GetStaticPaths] `getStaticPaths()` function is required for dynamic routes.
```
Static mode requires all paths to be known at build time:
```astro
---
export async function getStaticPaths() {
  return [
    { params: { slug: 'post-1' } },
    { params: { slug: 'post-2' } },
  ];
}
---
```

### SSR: Params are undefined
Check that the file naming matches:
- `[slug].astro` → `Astro.params.slug`
- `[...path].astro` → `Astro.params.path`
- `[category]/[id].astro` → `Astro.params.category`, `Astro.params.id`

---

## 10. Environment variables undefined {#env-undefined}

### In frontmatter (server):
```ts
// All variables available
console.log(import.meta.env.SECRET_KEY);     // ✓
console.log(import.meta.env.PUBLIC_API_URL); // ✓
```

### In client-side island:
```ts
// ONLY PUBLIC_ prefixed variables
console.log(import.meta.env.PUBLIC_API_URL); // ✓
console.log(import.meta.env.SECRET_KEY);     // undefined (by design)
```

### In production (SSR):
- Set variables on the hosting platform, not just in `.env`
- `.env` files are used during build. SSR runtime needs actual environment variables.
- On Vercel/Netlify: set in dashboard
- On Node: set in shell or use dotenv

---

## 11. Slow builds {#slow-builds}

**Diagnosis**: Run with timing:
```bash
time npm run build
```

**Common causes**:
- **Too many pages**: 10,000+ SSG pages = long builds. Consider hybrid (SSR for long-tail pages).
- **Heavy data fetching in getStaticPaths**: API calls during build. Cache or parallelize.
- **Unoptimized images**: Large images being processed at build time. Pre-optimize before adding to project.
- **Expensive integrations**: Some integrations add build steps. Test by removing one at a time.

**Quick fixes**:
- Limit pages in dev: `if (import.meta.env.DEV) return posts.slice(0, 10);`
- Use incremental builds if your host supports them (Vercel, Netlify)
- Pre-optimize images before import

---

## 12. CORS errors from API calls {#cors}

**Symptom**: Browser console shows CORS errors when fetching from an API.

**Key insight**: CORS only applies to browser-to-server requests. Server-to-server requests (Astro frontmatter, endpoints) are never blocked by CORS.

**Fix**: Move the API call from the client island to an Astro server endpoint:

```ts
// src/pages/api/proxy.ts — Astro endpoint (server-side, no CORS)
export const GET: APIRoute = async ({ url }) => {
  const res = await fetch(`https://external-api.com/data?q=${url.searchParams.get('q')}`);
  return new Response(await res.text(), {
    headers: { 'Content-Type': 'application/json' },
  });
};
```

Then call your own endpoint from the island:
```ts
// In your Vue/React island
const res = await fetch('/api/proxy?q=search');
```

---

## 13. Images not loading {#images-broken}

### Images from `src/assets/` not found
```astro
---
// Must use import for processed images
import hero from '../assets/hero.jpg';
---
<Image src={hero} alt="Hero" />
```

### Remote images not loading
Add the domain to allowed list:
```js
// astro.config.mjs
export default defineConfig({
  image: {
    domains: ['cdn.example.com'],
  },
});
```

### Images 404 after deploy
- Check `base` path in config
- Ensure images in `/public/` are deployed (some hosts ignore certain directories)
- For processed images, check that the build output includes `_astro/` directory

---

## 14. TypeScript errors {#typescript}

### "Cannot find module 'astro:content'"
Run `npx astro sync` to generate type definitions.

### Props type mismatch
```astro
---
// Define strict props interface
interface Props {
  title: string;
  count: number;  // If passing from parent, ensure it's a number, not string
}
const { title, count } = Astro.props;
---
```

### Integration types missing
```bash
# Regenerate all types
npx astro sync
```

---

## 15. Dev server issues {#dev-server}

### Port already in use
```bash
# Find what's using port 4321
lsof -i :4321
# Kill it, or use a different port:
npx astro dev --port 3000
```

### HMR (Hot Module Replacement) not working
- Some changes require a full restart (config changes, new content collections)
- Clear the Vite cache: `rm -rf node_modules/.vite`
- Check for circular imports

### Dev server crashes on save
- Usually a syntax error in `.astro` file — check terminal output
- Malformed frontmatter (missing closing `---`)
- Invalid TypeScript in frontmatter

---

## General debugging strategy

1. **Read the terminal error** — Astro's error messages are usually clear and point to the exact file/line
2. **Check `View Source`** — See the actual HTML output (not DevTools, which shows post-hydration DOM)
3. **Build locally first** — `npm run build && npm run preview` catches issues before deploying
4. **Isolate the problem** — Comment out components until you find which one causes the issue
5. **Check the Astro docs** — `docs.astro.build` has excellent troubleshooting sections
6. **Check GitHub issues** — `github.com/withastro/astro/issues` for known bugs


path: references/content-and-data.md
content: # Content Collections & Data Fetching

## Table of Contents
1. [Content collections overview](#overview)
2. [Defining collections](#defining)
3. [Querying collections](#querying)
4. [Data fetching patterns](#data-fetching)
5. [Dynamic routes with data](#dynamic-routes)
6. [Common problems](#problems)

---

## Content collections overview

Content collections are Astro's system for organizing and validating structured content (blog posts, docs, products, etc.). They live in `src/content/` and use Zod schemas for type safety.

Why use them instead of raw markdown in `/src/pages`:
- Schema validation catches content errors at build time
- TypeScript types are auto-generated from schemas
- Query APIs for filtering, sorting, pagination
- Separates content from presentation

---

## Defining collections

### Directory structure
```
src/content/
├── config.ts          # Schema definitions (REQUIRED)
├── blog/
│   ├── first-post.md
│   ├── second-post.mdx
│   └── third-post.md
├── authors/
│   ├── alice.json
│   └── bob.json
└── products/
    └── widget.yaml
```

### Schema definition
```ts
// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';

const blog = defineCollection({
  type: 'content', // Markdown/MDX (has a body to render)
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.coerce.date(), // Coerces string to Date
    updatedDate: z.coerce.date().optional(),
    author: reference('authors'), // Reference to another collection
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    image: z.string().optional(),
  }),
});

const authors = defineCollection({
  type: 'data', // JSON/YAML (structured data, no body)
  schema: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url(),
    bio: z.string().optional(),
  }),
});

export const collections = { blog, authors };
```

### Content file format
```md
---
title: "My First Post"
description: "An introduction to Astro"
publishDate: 2024-01-15
author: alice
tags: ["astro", "webdev"]
draft: false
---

# Hello World

This is the body content rendered as HTML.
```

---

## Querying collections

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

// Get all published posts, sorted by date
const posts = (await getCollection('blog', ({ data }) => {
  return !data.draft; // Filter out drafts
})).sort((a, b) =>
  b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);

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

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

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

<h1>{post.data.title}</h1>
<p>By {author.data.name}</p>
<Content />
```

### Useful query patterns

**Pagination**:
```astro
---
const allPosts = await getCollection('blog');
const pageSize = 10;
const page = Number(Astro.params.page) || 1;
const totalPages = Math.ceil(allPosts.length / pageSize);
const posts = allPosts.slice((page - 1) * pageSize, page * pageSize);
---
```

**Filter by tag**:
```astro
---
const tag = Astro.params.tag;
const posts = await getCollection('blog', ({ data }) =>
  data.tags.includes(tag)
);
---
```

**Group by year**:
```ts
const postsByYear = posts.reduce((acc, post) => {
  const year = post.data.publishDate.getFullYear();
  (acc[year] ||= []).push(post);
  return acc;
}, {} as Record<number, typeof posts>);
```

---

## Data fetching patterns

### SSG: Fetch at build time

In static mode, all fetching happens during the build. Data is baked into HTML.

```astro
---
// This runs at BUILD TIME, not in the browser
const res = await fetch('https://api.example.com/products');
const products = await res.json();
---

{products.map(p => <ProductCard product={p} />)}
```

**Important**: API rate limits can bite you during builds. If you have 1000 pages each fetching from an API, that's 1000 requests during build.

Mitigations:
- Batch API calls where possible
- Use content collections for local data
- Cache API responses during build (Astro caches `fetch` by default in SSG)

### SSR: Fetch at request time

In SSR mode, fetching happens on every request.

```astro
---
// This runs on EVERY REQUEST
const query = Astro.url.searchParams.get('q') || '';
const res = await fetch(`https://api.example.com/search?q=${query}`);
const results = await res.json();
---

<SearchResults results={results} />
```

### Fetch in API endpoints

```ts
// src/pages/api/products.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ url }) => {
  const category = url.searchParams.get('category');
  const products = await db.getProducts({ category });

  return new Response(JSON.stringify(products), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=120',
    },
  });
};
```

### Client-side fetching from islands

For interactive data loading (search, infinite scroll), fetch from within the island:

```vue
<script setup>
import { ref, onMounted } from 'vue';

const data = ref(null);
const loading = ref(true);

onMounted(async () => {
  const res = await fetch('/api/products');
  data.value = await res.json();
  loading.value = false;
});
</script>
```

---

## Dynamic routes with data

### SSG dynamic routes
Must provide all paths at build time:

```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<Layout title={post.data.title}>
  <Content />
</Layout>
```

### SSR dynamic routes
Read params at request time:

```astro
---
// src/pages/blog/[slug].astro (SSR)
import { getEntry } from 'astro:content';

const { slug } = Astro.params;
const post = await getEntry('blog', slug);

if (!post) {
  return Astro.redirect('/404');
}

const { Content } = await post.render();
---
```

### Catch-all routes
```astro
---
// src/pages/[...path].astro
const { path } = Astro.params;
// path = "a/b/c" for URL /a/b/c
---
```

---

## Common problems

### "Collection not found"
- Check that `src/content/config.ts` exists and exports the collection
- Make sure the collection name in `getCollection('name')` matches the folder name
- Run `astro sync` to regenerate types after changing schemas

### Schema validation errors
```
[ERROR] blog/my-post.md: "publishDate" Expected date, received string
```
Fix: Use `z.coerce.date()` instead of `z.date()` for frontmatter dates.

### Content not updating
- Content collections are cached. Restart the dev server after structural changes
- Run `astro sync` after modifying `config.ts`

### Slow builds with large collections
- Avoid fetching external data inside `getStaticPaths()` if possible
- Use pagination to limit pages generated per build
- Consider switching data-heavy pages to SSR

### MDX components not rendering
- Make sure `@astrojs/mdx` is installed and in `astro.config.mjs`
- Import components in the MDX file or pass them via the layout

### Reference resolution
```ts
// Get the referenced entry
const author = await getEntry(post.data.author);
// author.data.name, author.data.email, etc.
```
References only store the collection name and slug. You must call `getEntry()` to resolve them.


path: references/seo.md
content: # SEO Best Practices for Astro

## Table of Contents
1. [Why Astro is great for SEO](#why-astro)
2. [Reusable SEO component](#seo-component)
3. [Open Graph & social meta](#open-graph)
4. [Structured data (JSON-LD)](#structured-data)
5. [Sitemap & robots.txt](#sitemap)
6. [Canonical URLs](#canonical-urls)
7. [Performance signals](#performance)
8. [Common SEO mistakes in Astro](#mistakes)

---

## Why Astro is great for SEO

Astro outputs static HTML by default — no JavaScript required for content rendering. This means:
- Search engines see full content on first crawl (no hydration needed)
- Core Web Vitals are excellent out of the box (minimal JS = fast LCP, low TBT)
- No flash of unstyled/empty content

The main risk is undoing these advantages by shipping too much client-side JS through islands.

---

## Reusable SEO component

Create a single component that handles all head meta. Use it in every layout.

```astro
---
// src/components/SEO.astro
interface Props {
  title: string;
  description: string;
  image?: string;
  canonicalURL?: string;
  type?: 'website' | 'article';
  publishedDate?: string;
  noindex?: boolean;
}

const {
  title,
  description,
  image = '/og-default.png',
  canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
  type = 'website',
  publishedDate,
  noindex = false,
} = Astro.props;

const siteName = 'Your Site Name';
const fullTitle = `${title} | ${siteName}`;
const absoluteImage = new URL(image, Astro.site).href;
---

<!-- Primary meta -->
<title>{fullTitle}</title>
<meta name="description" content={description} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<link rel="canonical" href={canonicalURL} />

<!-- Open Graph -->
<meta property="og:type" content={type} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={absoluteImage} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:site_name" content={siteName} />
{publishedDate && <meta property="article:published_time" content={publishedDate} />}

<!-- 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={absoluteImage} />
```

Usage in a layout:
```astro
---
import SEO from '../components/SEO.astro';
const { title, description } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <SEO title={title} description={description} />
  </head>
  <body>
    <slot />
  </body>
</html>
```

---

## Open Graph & social meta

### Dynamic OG images

For blog posts, generate unique OG images. Options:
1. **Static images per post** — Place in `/public/og/` and reference by slug
2. **Dynamic generation** — Use `@vercel/og`, Satori, or a similar library in an API route

```ts
// src/pages/og/[slug].png.ts — Dynamic OG image endpoint
import type { APIRoute } from 'astro';
import { getEntry } from 'astro:content';
// Use satori + sharp or @resvg/resvg-js to render HTML to PNG

export const GET: APIRoute = async ({ params }) => {
  const post = await getEntry('blog', params.slug!);
  // Generate and return PNG...
};
```

### Social preview checklist
- OG image is exactly 1200×630px
- Title is under 60 characters (truncation varies by platform)
- Description is under 155 characters
- Test with: Facebook Sharing Debugger, Twitter Card Validator, LinkedIn Post Inspector

---

## Structured data (JSON-LD)

Add structured data to help search engines understand your content.

### Article schema
```astro
---
const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": title,
  "description": description,
  "image": image,
  "datePublished": publishedDate,
  "dateModified": modifiedDate,
  "author": {
    "@type": "Person",
    "name": authorName,
    "url": authorURL
  },
  "publisher": {
    "@type": "Organization",
    "name": "Your Site",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png"
    }
  }
};
---
<script type="application/ld+json" set:html={JSON.stringify(articleSchema)} />
```

### Breadcrumb schema
```astro
---
const breadcrumbs = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com" },
    { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog" },
    { "@type": "ListItem", "position": 3, "name": title },
  ]
};
---
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbs)} />
```

### FAQ schema
```astro
---
const faqSchema = {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": faqs.map(faq => ({
    "@type": "Question",
    "name": faq.question,
    "acceptedAnswer": {
      "@type": "Answer",
      "text": faq.answer
    }
  }))
};
---
<script type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
```

---

## Sitemap & robots.txt

### Sitemap
```bash
npx astro add sitemap
```

```js
// astro.config.mjs
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://example.com', // REQUIRED for sitemap
  integrations: [
    sitemap({
      filter: (page) => !page.includes('/admin/'), // Exclude pages
      changefreq: 'weekly',
      priority: 0.7,
      lastmod: new Date(),
    }),
  ],
});
```

### robots.txt
Place in `/public/robots.txt`:
```
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/

Sitemap: https://example.com/sitemap-index.xml
```

---

## Canonical URLs

Every page needs a canonical URL to prevent duplicate content issues.

**The pattern**: Use `Astro.site` + `Astro.url.pathname` to build canonical URLs.

```astro
---
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<link rel="canonical" href={canonicalURL.href} />
```

**Watch out for**:
- Trailing slash inconsistency (`/about` vs `/about/` are different URLs)
- Query parameters (canonical should not include `?page=2` etc.)
- `www` vs non-`www` (pick one and redirect the other)
- HTTP vs HTTPS (always canonical to HTTPS)

Set `trailingSlash` in config to match your host's behavior:
```js
export default defineConfig({
  trailingSlash: 'always', // or 'never'
});
```

---

## Performance signals (SEO-relevant)

Google uses Core Web Vitals as ranking signals. Astro makes these easy to ace.

**LCP (Largest Contentful Paint)**:
- Use `<Image />` from `astro:assets` for optimized images
- Preload the LCP image: `<link rel="preload" as="image" href={heroImage} />`
- Avoid lazy-loading above-the-fold images

**CLS (Cumulative Layout Shift)**:
- Always set `width` and `height` on images
- Avoid injecting content above existing content after load
- Reserve space for dynamic islands

**INP (Interaction to Next Paint)**:
- Ship minimal JS (Astro's default behavior)
- Use `client:visible` or `client:idle` instead of `client:load` where possible
- Avoid heavy computation in event handlers

---

## Common SEO mistakes in Astro

1. **Forgetting `site` in config** → `Astro.site` is undefined, canonical URLs and sitemap break silently.

2. **No canonical URLs** → Duplicate content across routes (with/without trailing slash, www/non-www).

3. **Same meta on every page** → Every page should have a unique `<title>` and `<meta name="description">`.

4. **Missing OG image** → Social shares look broken. Always provide a fallback default.

5. **Over-hydrating destroys performance** → Adding `client:load` to everything ships tons of JS, killing Core Web Vitals. Use the lightest directive possible.

6. **Dynamic SSR pages without meta** → SSR pages still need proper `<title>` and meta tags. Fetch the data first, then render the head.

7. **Not testing with `View Source`** → Islands that render client-side only (missing SSR support) may show empty HTML to crawlers. Always check the raw HTML output.


path: references/rendering-modes.md
content: # Rendering Modes & SSR Caching

## Table of Contents
1. [SSG (Static Site Generation)](#ssg)
2. [SSR (Server-Side Rendering)](#ssr)
3. [Hybrid mode](#hybrid)
4. [SSR caching strategies](#caching)
5. [Middleware](#middleware)
6. [API routes / endpoints](#endpoints)
7. [Decision matrix](#decision-matrix)

---

## SSG (Static Site Generation) — the default

Every `.astro` page is pre-rendered at build time into static HTML. No server needed at runtime.

```js
// astro.config.mjs
export default defineConfig({
  output: 'static', // This is the default; you can omit it
});
```

**When to use SSG**:
- Blog, docs, marketing sites, portfolios
- Content that changes at deploy time, not per-request
- Maximum performance (every page is a cached HTML file)

**Limitations**:
- No per-request data (user sessions, search results, real-time content)
- Dynamic routes must define all paths at build time via `getStaticPaths()`
- Large sites with thousands of pages = long build times

---

## SSR (Server-Side Rendering)

Pages are rendered on-demand per request. Requires an adapter.

```js
// astro.config.mjs
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
```

**When to use SSR**:
- User-specific content (dashboards, account pages)
- Real-time or frequently changing data
- Auth-gated pages
- Search results, filtered listings
- Content from a CMS that updates without redeploying

**Key behavior differences from SSG**:
- `Astro.request` contains the actual request (headers, cookies, URL)
- `Astro.cookies` is available for reading/writing cookies
- `Astro.redirect()` works for server-side redirects
- No `getStaticPaths()` — params come from the live URL

```astro
---
// SSR page: reads cookies, fetches user-specific data
const [REDACTED_SECRET]'auth_token')?.value;
if (!token) return Astro.redirect('/login');

const user = await fetchUser(token);
---
<h1>Welcome, {user.name}</h1>
```

---

## Hybrid mode

The practical sweet spot for most real projects. Static by default, SSR where needed.

```js
// astro.config.mjs — Astro 4+ approach
export default defineConfig({
  output: 'static',  // Default: pages are static
  adapter: node({ mode: 'standalone' }), // Needed for the SSR pages
});
```

Then opt individual pages OUT of prerendering:

```astro
---
// src/pages/dashboard.astro — this page renders on every request
export const prerender = false;
---
```

Or if you set `output: 'server'` (SSR default), opt individual pages IN to prerendering:

```astro
---
// src/pages/about.astro — this page is pre-rendered at build time
export const prerender = true;
---
```

**Hybrid is the right choice when**:
- Most pages are static (marketing, blog, docs)
- A few pages need dynamic data (login, dashboard, search)
- You want fast static pages without giving up server capabilities

---

## SSR Caching Strategies

SSR without caching is just a slow website. Every SSR page should have a caching plan.

### Level 1: HTTP Cache-Control headers

The simplest and most effective approach. Set headers and let the CDN/browser handle it.

```astro
---
// In an .astro page
Astro.response.headers.set(
  'Cache-Control',
  'public, s-maxage=60, stale-while-revalidate=300'
);

const data = await fetchExpensiveData();
---
<h1>{data.title}</h1>
```

**Header cheat sheet**:

| Header | Meaning |
|--------|---------|
| `public, max-age=3600` | Browser + CDN cache for 1 hour |
| `public, s-maxage=60` | CDN caches 60s, browser doesn't |
| `public, s-maxage=60, stale-while-revalidate=300` | CDN serves stale for 5min while refreshing |
| `private, max-age=0` | No caching (user-specific data) |
| `no-store` | Never cache anywhere |

### Level 2: Middleware-based caching

Use Astro middleware to apply caching logic globally or per-route:

```ts
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async ({ url, request }, next) => {
  const response = await next();

  // Cache static-ish pages for 5 minutes at the CDN
  if (url.pathname.startsWith('/blog/')) {
    response.headers.set(
      'Cache-Control',
      'public, s-maxage=300, stale-while-revalidate=600'
    );
  }

  // Never cache user-specific pages
  if (url.pathname.startsWith('/dashboard')) {
    response.headers.set('Cache-Control', 'private, no-store');
  }

  return response;
});
```

### Level 3: In-memory / external cache for expensive operations

For data that's expensive to fetch but doesn't change often:

```ts
// src/lib/cache.ts
const cache = new Map<string, { data: any; expires: number }>();

export async function cachedFetch<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlSeconds = 60
): Promise<T> {
  const cached = cache.get(key);
  if (cached && cached.expires > Date.now()) {
    return cached.data as T;
  }

  const data = await fetcher();
  cache.set(key, {
    data,
    expires: Date.now() + ttlSeconds * 1000,
  });
  return data;
}
```

Usage in a page:
```astro
---
import { cachedFetch } from '../lib/cache';

const posts = await cachedFetch('recent-posts', async () => {
  return await cms.getPosts({ limit: 10 });
}, 120); // Cache for 2 minutes
---
```

### Level 4: Edge caching (CDN-specific)

When deploying to Vercel, Cloudflare, or Netlify, use their edge caching:

**Vercel**: Uses `s-maxage` headers automatically. Add `stale-while-revalidate` for instant responses.

**Cloudflare**: Use Cache API in Workers for fine-grained control:
```ts
// Works in Cloudflare adapter context
const cache = caches.default;
const cached = await cache.match(request);
if (cached) return cached;
```

### Cache invalidation strategies

- **Time-based** (TTL): Set `max-age` or `s-maxage`. Simple, predictable.
- **On-demand**: Purge CDN cache via API when content changes (webhook from CMS).
- **Stale-while-revalidate**: Serve stale content immediately, refresh in background. Best UX.
- **Cache busting**: Append version/hash to URLs for assets.

---

## Middleware

Middleware runs before every request in SSR mode. Use it for auth, logging, caching headers, redirects.

```ts
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';

const auth = defineMiddleware(async ({ cookies, url, redirect }, next) => {
  const [REDACTED_SECRET]'session')?.value;

  if (url.pathname.startsWith('/admin') && !token) {
    return redirect('/login');
  }

  return next();
});

const timing = defineMiddleware(async (context, next) => {
  const start = performance.now();
  const response = await next();
  const duration = performance.now() - start;
  response.headers.set('X-Response-Time', `${duration.toFixed(0)}ms`);
  return response;
});

// Chain multiple middleware
export const onRequest = sequence(auth, timing);
```

---

## API Routes / Endpoints

Files in `src/pages/api/` that export HTTP method handlers:

```ts
// src/pages/api/search.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ url }) => {
  const query = url.searchParams.get('q');
  const results = await search(query);

  return new Response(JSON.stringify(results), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=30',
    },
  });
};

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  // Process the submission
  return new Response(JSON.stringify({ ok: true }), {
    status: 200,
  });
};
```

---

## Decision Matrix

| Scenario | Mode | Caching |
|----------|------|---------|
| Blog / docs | SSG | N/A (static files) |
| Marketing site with contact form | Hybrid (form endpoint is SSR) | Static pages cached by CDN |
| E-commerce product pages | Hybrid or SSR | `s-maxage=300, stale-while-revalidate` |
| User dashboard | SSR | `private, no-store` |
| API-driven listing with filters | SSR | `s-maxage=60` + in-memory cache |
| CMS preview mode | SSR | `no-store` for preview, cached for published |


path: references/setup-and-structure.md
content: # Setup & Project Structure

## Table of Contents
1. [Creating a project](#creating-a-project)
2. [File structure explained](#file-structure)
3. [astro.config.mjs](#config)
4. [Adapters](#adapters)
5. [TypeScript setup](#typescript)
6. [Common setup mistakes](#common-mistakes)

---

## Creating a project

```bash
# Interactive setup
npm create astro@latest

# With a specific template
npm create astro@latest -- --template blog
npm create astro@latest -- --template docs
npm create astro@latest -- --template minimal

# With a community template
npm create astro@latest -- --template github-user/repo
```

After creation:
```bash
cd my-project
npm install
npm run dev     # Dev server on localhost:4321
npm run build   # Production build to ./dist
npm run preview # Preview production build locally
```

---

## File structure

```
my-project/
├── public/              # Static assets (served as-is, no processing)
│   ├── favicon.svg
│   ├── robots.txt
│   └── og-image.png
├── src/
│   ├── pages/           # File-based routing (REQUIRED for routes)
│   │   ├── index.astro          # → /
│   │   ├── about.astro          # → /about
│   │   ├── blog/
│   │   │   ├── index.astro      # → /blog
│   │   │   └── [slug].astro     # → /blog/:slug (dynamic)
│   │   └── api/
│   │       └── search.ts        # → /api/search (API endpoint)
│   ├── components/      # Reusable UI (Astro, Vue, React, etc.)
│   ├── layouts/         # Page shells with <slot />
│   ├── content/         # Content collections (markdown, MDX, JSON)
│   │   └── config.ts    # Collection schemas
│   ├── styles/          # Global styles
│   ├── lib/             # Utility functions, API clients
│   └── middleware.ts    # Request middleware (SSR only)
├── astro.config.mjs     # Astro configuration
├── tsconfig.json        # TypeScript config
└── package.json
```

### Key distinctions that trip people up

**`/public` vs `src/assets/`**:
- `/public` → Copied verbatim to build output. Use for files that need a stable URL (favicons, robots.txt, downloadable PDFs).
- `src/assets/` → Processed by Astro's build pipeline. Use for images (gets optimized), CSS (gets bundled), etc.

**`/src/pages` vs `/src/components`**:
- Pages create routes. Every `.astro`, `.md`, or `.mdx` file in `/src/pages` becomes a URL.
- Components don't create routes. They're imported and used inside pages or other components.
- A component in `/src/pages` IS a route whether you intended it or not.

**Dynamic routes**:
```
src/pages/blog/[slug].astro     → /blog/my-post
src/pages/[...path].astro       → catch-all (404 page, etc.)
src/pages/[lang]/[...slug].astro → /en/about, /es/about
```

For SSG, dynamic routes need `getStaticPaths()`:
```astro
---
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}
const { post } = Astro.props;
---
```

For SSR, dynamic routes read params at runtime:
```astro
---
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
if (!post) return Astro.redirect('/404');
---
```

---

## astro.config.mjs

Minimal config:
```js
import { defineConfig } from 'astro/config';

export default defineConfig({});
```

Common production config:
```js
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import node from '@astrojs/node';

export default defineConfig({
  site: 'https://example.com',        // Required for sitemap, canonical URLs
  base: '/',                           // Set if hosting at a subpath: '/docs/'
  output: 'static',                    // 'static' | 'server'
  adapter: node({ mode: 'standalone' }), // Only for SSR/hybrid
  integrations: [
    vue(),
    tailwind(),
    sitemap(),
  ],
  vite: {
    // Vite config overrides if needed
  },
  trailingSlash: 'always',            // 'always' | 'never' | 'ignore'
});
```

### Config options that matter most

| Option | What it does | When you need it |
|--------|-------------|-----------------|
| `site` | Sets the canonical base URL | Always in production (sitemap, OG tags) |
| `base` | Subpath prefix for all routes | Hosting at `/docs/` or behind a reverse proxy |
| `output` | Rendering mode | `'server'` for SSR, `'static'` for SSG (default) |
| `adapter` | Server runtime | Required when `output` is `'server'` |
| `trailingSlash` | URL format | Match your host's behavior to avoid redirects |

---

## Adapters

Adapters tell Astro how to run your server code. Only needed for SSR or hybrid.

### Node.js (self-hosted)
```bash
npx astro add node
```
```js
// astro.config.mjs
import node from '@astrojs/node';
export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }), // or 'middleware'
});
```
- `standalone` → Runs its own HTTP server (port 4321 by default)
- `middleware` → Exports an Express/Fastify-compatible handler

### Vercel
```bash
npx astro add vercel
```
```js
import vercel from '@astrojs/vercel';
export default defineConfig({
  output: 'server',
  adapter: vercel(),
});
```

### Netlify
```bash
npx astro add netlify
```

### Cloudflare Pages
```bash
npx astro add cloudflare
```

---

## TypeScript setup

Astro supports TypeScript out of the box. The frontmatter section of `.astro` files is TypeScript by default.

```json
// tsconfig.json (Astro's recommended)
{
  "extends": "astro/tsconfigs/strict"  // or "base" for looser checks
}
```

Type-safe props:
```astro
---
interface Props {
  title: string;
  description?: string;
  tags: string[];
}
const { title, description = 'Default', tags } = Astro.props;
---
```

---

## Common setup mistakes

**Installing integrations manually instead of using `npx astro add`**:
The `astro add` command updates both `package.json` AND `astro.config.mjs`. Manual npm install only does half the job.

**Putting components in `/src/pages` accidentally**:
Everything in pages becomes a route. If you have `src/pages/Button.astro`, it creates a `/Button` page.

**Forgetting `site` in config**:
Without it, `Astro.site` returns undefined, sitemap generation fails, and canonical URLs break.

**Wrong Node.js version**:
Astro requires Node 18+. Check with `node --version`.

**Mixing package managers**:
Pick one (npm, pnpm, yarn) and stick with it. Delete lock files from other managers.


path: references/islands-and-vue.md
content: # Islands Architecture & Vue Integration

## Table of Contents
1. [How islands work](#how-islands-work)
2. [Client directives explained](#client-directives)
3. [Choosing the right directive](#choosing-directives)
4. [Vue integration setup](#vue-setup)
5. [Vue component patterns](#vue-patterns)
6. [React and Svelte islands](#other-frameworks)
7. [Common island mistakes](#mistakes)
8. [Advanced patterns](#advanced)

---

## How islands work

Astro's island architecture is its core differentiator. The mental model:

1. **Every component renders to static HTML on the server** (build time for SSG, request time for SSR).
2. **By default, zero JavaScript is shipped for any component.**
3. **Adding a `client:*` directive tells Astro to also ship the JS and hydrate that component.**

This means a page with 20 components but only 2 `client:*` directives ships JS for only those 2 components. The rest are pure HTML.

```
┌──────────────────────────────────────────────┐
│  Page (static HTML shell)                    │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ Header   │  │ Article  │  │ Sidebar  │   │
│  │ (static) │  │ (static) │  │ (static) │   │
│  └──────────┘  └──────────┘  └──────────┘   │
│                                              │
│  ┌──────────────────┐  ┌─────────────────┐   │
│  │ Search Bar       │  │ Comment Form    │   │
│  │ (island: Vue)    │  │ (island: React) │   │
│  │ client:load      │  │ client:visible  │   │
│  └──────────────────┘  └─────────────────┘   │
│                                              │
└──────────────────────────────────────────────┘
```

---

## Client directives explained

| Directive | When JS loads | When component hydrates | Use case |
|-----------|-------------|----------------------|---------|
| `client:load` | Immediately on page load | As soon as JS loads | Critical interactive UI (nav menus, auth forms) |
| `client:idle` | After page is idle (`requestIdleCallback`) | When browser is idle | Non-critical interactivity (newsletter signup, chat widget) |
| `client:visible` | When component enters viewport | When scrolled into view | Below-fold content (comments, carousels, charts) |
| `client:media="(query)"` | When media query matches | On match | Mobile-only menus, responsive components |
| `client:only="vue"` | Immediately | Client-only (no SSR HTML) | Components that can't SSR (uses `window`, DOM APIs) |

### Hydration flow

```
Server:  Render HTML → Send to browser
Browser: Display HTML immediately (fast!)
         ↓
         Load JS for island (based on directive timing)
         ↓
         Hydrate: Attach event listeners to existing HTML
         ↓
         Component is now interactive
```

**The hydration contract**: The HTML generated on the server must match what the client would render. If they differ, you get hydration mismatch errors.

---

## Choosing the right directive

**Decision flow**:

```
Does the component need interactivity?
├── No  → Don't use any client directive (static HTML)
├── Yes →
│   Is it above the fold / immediately needed?
│   ├── Yes → client:load
│   └── No  →
│       Is it below the fold?
│       ├── Yes → client:visible
│       └── No  →
│           Can it wait until the page is idle?
│           ├── Yes → client:idle
│           └── No  → client:load
│
│   Does it ONLY work in the browser (uses window/document)?
│   └── Yes → client:only="framework"
```

**Performance ranking** (best to worst):
1. No directive (zero JS)
2. `client:visible` (loads only if user scrolls to it)
3. `client:idle` (defers until page is idle)
4. `client:media` (loads only on matching viewport)
5. `client:load` (loads immediately)
6. `client:only` (no SSR HTML + immediate load)

---

## Vue integration setup

### Installation
```bash
npx astro add vue
```

This automatically:
- Installs `@astrojs/vue` and `vue`
- Adds the integration to `astro.config.mjs`

### Manual setup (if needed)
```bash
npm install @astrojs/vue vue
```

```js
// astro.config.mjs
import vue from '@astrojs/vue';

export default defineConfig({
  integrations: [
    vue({
      appEntrypoint: '/src/pages/_app',  // Optional: custom app setup
      jsx: false,                         // Enable JSX in Vue if needed
    }),
  ],
});
```

### Custom app entrypoint (for plugins, stores)
```ts
// src/pages/_app.ts
import type { App } from 'vue';
import { createPinia } from 'pinia';

export default (app: App) => {
  app.use(createPinia());
  // Register global components, directives, etc.
};
```

---

## Vue component patterns

### Basic Vue island
```vue
<!-- src/components/Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue';

interface Props {
  initialCount?: number;
}

const props = withDefaults(defineProps<Props>(), {
  initialCount: 0,
});

const count = ref(props.initialCount);
</script>

<template>
  <div class="counter">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
  </div>
</template>
```

Using it in Astro:
```astro
---
import Counter from '../components/Counter.vue';
---

<!-- Static: renders HTML but buttons won't work -->
<Counter initialCount={5} />

<!-- Interactive: buttons work after hydration -->
<Counter client:load initialCount={5} />
```

### Props serialization rules

Props passed from Astro to framework islands are serialized (converted to JSON). This means:

**Works** (serializable):
- Strings, numbers, booleans
- Arrays, plain objects
- `null`, `undefined`
- Dates (serialized as ISO strings)

**Does NOT work** (not serializable):
- Functions, callbacks
- Class instances
- Symbols
- Circular references
- Reactive Vue refs/computed

```astro
---
import MyVue from '../components/MyVue.vue';

// GOOD: plain data
const items = [{ id: 1, name: 'First' }];

// BAD: functions can't cross the boundary
const onClick = () => console.log('nope');
---

<MyVue client:load items={items} />
<!-- <MyVue client:load onClick={onClick} /> ← This won't work -->
```

### Composables and state management

Vue composables work normally inside hydrated islands:

```vue
<!-- src/components/SearchBar.vue -->
<script setup>
import { ref, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core'; // VueUse works

const query = ref('');
const results = ref([]);

const search = useDebounceFn(async (q) => {
  if (!q) { results.value = []; return; }
  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
  results.value = await res.json();
}, 300);

watch(query, (q) => search(q));
</script>

<template>
  <input v-model="query" placeholder="Search..." />
  <ul v-if="results.length">
    <li v-for="r in results" :key="r.id">{{ r.title }}</li>
  </ul>
</template>
```

### Pinia stores across islands

Each island is an independent Vue app. Pinia stores are NOT shared between islands by default.

If you need shared state between Vue islands, use:
1. **Custom events** (`window.dispatchEvent` / `addEventListener`)
2. **Shared external store** (nanostores — works across any framework)
3. **URL state** (query params, hash)

```ts
// src/stores/cart.ts — using nanostores (framework-agnostic)
import { atom, map } from 'nanostores';

export const $cartItems = map<Record<string, number>>({});
export const $cartCount = atom(0);

export function addToCart(id: string) {
  const items = $cartItems.get();
  $cartItems.setKey(id, (items[id] || 0) + 1);
  $cartCount.set(Object.values($cartItems.get()).reduce((a, b) => a + b, 0));
}
```

---

## React and Svelte islands

The same patterns apply. Install the integration, add `client:*` directives.

```bash
npx astro add react
npx astro add svelte
```

**Mixing frameworks on the same page is fully supported**:
```astro
---
import VueCounter from '../components/Counter.vue';
import ReactChart from '../components/Chart.tsx';
import SvelteToggle from '../components/Toggle.svelte';
---

<VueCounter client:load />
<ReactChart client:visible data={chartData} />
<SvelteToggle client:idle />
```

Each island hydrates independently. They share no runtime state (unless using nanostores or similar).

---

## Common island mistakes

### 1. Forgetting the client directive
```astro
<!-- Bug: renders HTML but nothing is interactive -->
<Counter />

<!-- Fix: add the appropriate directive -->
<Counter client:load />
```
This is the #1 Astro support question. The component renders HTML fine but click handlers, reactive state, and lifecycle hooks don't run.

### 2. Using client:only when you don't need to
```astro
<!-- Avoid: No SSR HTML, shows blank until JS loads -->
<HeroSection client:only="vue" />

<!-- Better: SSR HTML shows immediately, then hydrates -->
<HeroSection client:load />
```
Only use `client:only` when the component truly cannot render on the server (uses `window`, `document`, canvas, WebGL, etc.).

### 3. Hydration mismatch errors
The server HTML must match the client render. Common causes:
- **Dates/timestamps**: Server and client format differently
- **Random values**: `Math.random()` in render
- **Conditional rendering based on `window`**: Server has no `window`
- **Browser-only APIs in template**: `localStorage.getItem()` in template

Fix: Move browser-only logic to `onMounted` (Vue) or `useEffect` (React):
```vue
<script setup>
import { ref, onMounted } from 'vue';

const theme = ref('light'); // Safe default for SSR

onMounted(() => {
  theme.value = localStorage.getItem('theme') || 'light';
});
</script>
```

### 4. Over-hydrating the page
If every component has `client:load`, you've defeated Astro's purpose. Audit:
- Does this component actually need JS? (Maybe it's just displaying data)
- Can it use `client:visible` instead of `client:load`?
- Can the interactive part be extracted into a smaller island?

### 5. Passing non-serializable props
```astro
---
// Bug: functions can't be serialized across the island boundary
const handleClick = () => alert('hi');
---
<MyVue client:load onClick={handleClick} />
```
Move the handler inside the Vue component instead.

---

## Advanced patterns

### Nested islands
An Astro component can contain multiple islands. An island can contain Astro children via slots:

```astro
---
import VueWrapper from '../components/Wrapper.vue';
import StaticContent from '../components/StaticContent.astro';
---

<VueWrapper client:load>
  <!-- This static content is passed as a slot -->
  <StaticContent />
</VueWrapper>
```

But a hydrated island cannot contain another hydrated island from a different framework. Keep islands flat.

### Island communication
Use custom events for cross-island communication:

```vue
<!-- Island A: emits event -->
<script setup>
function notify() {
  window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count: 5 } }));
}
</script>
```

```vue
<!-- Island B: listens for event -->
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

const count = ref(0);
function handler(e) { count.value = e.detail.count; }

onMounted(() => window.addEventListener('cart-updated', handler));
onUnmounted(() => window.removeEventListener('cart-updated', handler));
</script>
```

### Lazy-loading heavy islands
Combine `client:visible` with dynamic imports for code-splitting:

```astro
---
// The Vue component JS only loads when scrolled into view
import HeavyChart from '../components/HeavyChart.vue';
---
<HeavyChart client:visible data={bigDataset} />
```

Astro automatically code-splits each island, so each `client:visible` component only downloads its JS when triggered.


path: references/deployment.md
content: # Deployment & Adapters

## Table of Contents
1. [Deployment decision tree](#decision-tree)
2. [Static deployment](#static)
3. [SSR deployment](#ssr)
4. [Environment variables](#env)
5. [Base path / subpath hosting](#base-path)
6. [Auth & protected pages](#auth)
7. [Common deployment failures](#failures)

---

## Deployment decision tree

```
Is every page static (SSG)?
├── Yes → Deploy to ANY static host
│   ├── Netlify, Vercel, Cloudflare Pages
│   ├── GitHub Pages, AWS S3 + CloudFront
│   └── Any web server (nginx, Apache)
│
└── No (SSR or Hybrid) → Need a server runtime
    ├── Vercel → @astrojs/vercel
    ├── Netlify → @astrojs/netlify
    ├── Cloudflare → @astrojs/cloudflare
    ├── AWS Lambda → @astrojs/node + serverless wrapper
    └── Self-hosted → @astrojs/node (standalone)
```

---

## Static deployment

Build produces a `dist/` folder of HTML, CSS, JS, and assets. Upload it anywhere.

```bash
npm run build
# Output: ./dist/
```

### Netlify
```toml
# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"
```

### Vercel (static)
No adapter needed. Vercel auto-detects Astro:
```json
// vercel.json (optional, for customization)
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist"
}
```

### GitHub Pages
```yaml
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    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
      - uses: actions/deploy-pages@v4
```

For GitHub Pages, set the base path:
```js
// astro.config.mjs
export default defineConfig({
  site: 'https://username.github.io',
  base: '/repo-name/',
});
```

### Cloudflare Pages (static)
Connect your Git repo in the Cloudflare dashboard. Build command: `npm run build`, output: `dist`.

---

## SSR deployment

### Vercel (serverless)
```bash
npx astro add vercel
```

```js
// astro.config.mjs
import vercel from '@astrojs/vercel';

export default defineConfig({
  output: 'server',
  adapter: vercel({
    // Options
    imageService: true,     // Use Vercel's image optimization
    isr: {
      expiration: 60,       // ISR: revalidate every 60 seconds
    },
  }),
});
```

### Netlify (serverless)
```bash
npx astro add netlify
```

```js
import netlify from '@astrojs/netlify';

export default defineConfig({
  output: 'server',
  adapter: netlify(),
});
```

### Cloudflare Workers
```bash
npx astro add cloudflare
```

```js
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    mode: 'directory', // or 'advanced'
  }),
});
```

### Node.js (self-hosted)
```bash
npx astro add node
```

```js
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone', // Runs its own HTTP server
    // mode: 'middleware' // Exports handler for Express/Fastify
  }),
});
```

Running in production:
```bash
npm run build
node ./dist/server/entry.mjs
# Server starts on port 4321
```

With a process manager:
```bash
# PM2
pm2 start ./dist/server/entry.mjs --name my-astro-app

# Or with environment variables
HOST=0.0.0.0 PORT=3000 node ./dist/server/entry.mjs
```

### Docker
```dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./

ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321

CMD ["node", "./dist/server/entry.mjs"]
```

---

## Environment variables

### Defining variables
```bash
# .env (development)
PUBLIC_API_URL=https://api.dev.example.com
SECRET_API_KEY=sk-dev-12345

# .env.production (production build)
PUBLIC_API_URL=https://api.example.com
SECRET_API_KEY=sk-prod-67890
```

### Accessing variables

**Server-side (frontmatter, endpoints, middleware)**:
```ts
// Access ALL variables (public and secret)
const [REDACTED_SECRET];
const apiUrl = import.meta.env.PUBLIC_API_URL;
```

**Client-side (islands, browser JS)**:
```ts
// Only PUBLIC_ prefixed variables are available
const apiUrl = import.meta.env.PUBLIC_API_URL;
// import.meta.env.SECRET_API_KEY → undefined (intentionally hidden)
```

### Platform-specific env setup

**Vercel**: Set in project Settings → Environment Variables
**Netlify**: Set in Site Settings → Environment Variables
**Cloudflare**: Set in Workers & Pages → Settings → Variables
**Node**: Pass via shell or `.env` file

The `PUBLIC_` prefix convention is enforced by Astro. Never put secrets in `PUBLIC_` variables.

---

## Base path / subpath hosting

When hosting at a subpath like `https://example.com/docs/`:

```js
// astro.config.mjs
export default defineConfig({
  base: '/docs/',
  site: 'https://example.com',
});
```

### What `base` affects
- All generated links are prefixed with `/docs/`
- `Astro.url` includes the base
- Static assets are served from `/docs/`
- The dev server mounts at `localhost:4321/docs/`

### What breaks without `base`
- Links point to `/` instead of `/docs/` → 404
- CSS/JS assets 404 because paths are wrong
- Images in `/public/` load from the wrong path

### Using base-aware paths
```astro
---
// Always use Astro's URL utilities for base-aware paths
const homeURL = new URL('/', Astro.url);
---

<!-- DON'T hardcode paths -->
<a href="/">Home</a>  <!-- Wrong if base is /docs/ -->

<!-- DO use Astro's base-aware utilities -->
<a href={`${import.meta.env.BASE_URL}`}>Home</a>
```

### Reverse proxy setup (nginx)
```nginx
location /docs/ {
    proxy_pass http://localhost:4321/docs/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
```

---

## Auth & protected pages

### Middleware-based auth (SSR only)
```ts
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const protectedRoutes = ['/dashboard', '/admin', '/settings'];

export const onRequest = defineMiddleware(async ({ url, cookies, redirect }, next) => {
  const isProtected = protectedRoutes.some(r => url.pathname.startsWith(r));

  if (isProtected) {
    const session = cookies.get('session')?.value;

    if (!session) {
      return redirect(`/login?redirect=${encodeURIComponent(url.pathname)}`);
    }

    // Validate session (check DB, verify JWT, etc.)
    const user = await validateSession(session);
    if (!user) {
      cookies.delete('session');
      return redirect('/login');
    }

    // Make user available to the page
    Astro.locals.user = user;
  }

  return next();
});
```

### Auth API endpoints
```ts
// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request, cookies, redirect }) => {
  const formData = await request.formData();
  const email = formData.get('email') as string;
  const [REDACTED_SECRET]'password') as string;

  const user = await authenticateUser(email, password);

  if (!user) {
    return new Response('Invalid credentials', { status: 401 });
  }

  const session = await createSession(user.id);
  cookies.set('session', session.id, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  });

  return redirect('/dashboard');
};
```

### Auth in static mode (DON'T)

You cannot do server-side auth in static mode. The HTML is pre-built. Options:
1. Switch protected pages to SSR (hybrid mode)
2. Use client-side auth (check token in island, redirect via JS) — but the HTML is still in the build output
3. Use an external auth gateway (Cloudflare Access, Netlify Identity)

---

## Common deployment failures

### "Cannot find adapter"
```
[ERROR] Cannot use `output: 'server'` without an adapter.
```
Fix: Install the correct adapter: `npx astro add vercel` (or node, netlify, etc.)

### Environment variables undefined in production
- Check the variable is set on the hosting platform
- For client-side access, variable must start with `PUBLIC_`
- For SSR, make sure the runtime environment has the variable (not just build time)

### SSR pages return 404 on static host
You deployed an SSR app to a static host (GitHub Pages, S3). Either:
- Switch those pages to `export const prerender = true`
- Or deploy to a host that supports SSR (Vercel, Netlify, Cloudflare Workers, Node)

### Broken assets after deploy
- Check `base` in config matches the deployment path
- Verify `trailingSlash` setting matches the host's behavior
- Clear CDN cache after deploy

### Build succeeds locally but fails on host
- Node version mismatch (Astro needs 18+)
- Missing environment variables
- Different package manager lock file
- Case-sensitivity in file names (macOS is case-insensitive, Linux isn't)

### "window is not defined" during build
A component uses browser APIs during SSR. Fix:
- Wrap in `client:only="framework"` directive
- Move browser code to `onMounted` / `useEffect`
- Guard with `if (typeof window !== 'undefined')`


path: references/performance.md
content: # Performance Optimization

## Table of Contents
1. [Image optimization](#images)
2. [Hydration control](#hydration)
3. [Bundle analysis](#bundle)
4. [Font loading](#fonts)
5. [Prefetching](#prefetching)
6. [Build performance](#build)

---

## Image optimization

Astro has built-in image optimization via `astro:assets`. Use it — raw `<img>` tags are a performance miss.

### The `<Image />` component
```astro
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Optimized: resized, converted to WebP/AVIF, width/height set -->
<Image src={heroImage} alt="Hero" />

<!-- With explicit dimensions -->
<Image src={heroImage} alt="Hero" width={800} height={400} />

<!-- Remote images (must be configured) -->
<Image
  src="https://example.com/photo.jpg"
  alt="Remote"
  width={600}
  height={300}
  inferSize  <!-- Or use inferSize to auto-detect -->
/>
```

### Configure remote image domains
```js
// astro.config.mjs
export default defineConfig({
  image: {
    domains: ['example.com', 'cdn.example.com'],
    // Or allow all remotes (less secure):
    // remotePatterns: [{ protocol: 'https' }],
  },
});
```

### The `<Picture />` component
For responsive art direction:
```astro
---
import { Picture } from 'astro:assets';
import hero from '../assets/hero.jpg';
---

<Picture
  src={hero}
  formats={['avif', 'webp']}
  alt="Hero"
  widths={[400, 800, 1200]}
  sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
/>
```

### Critical image tips
- **Never lazy-load above-the-fold images** — it delays LCP
- **Always set width and height** — prevents layout shift (CLS)
- **Use `loading="eager"` for LCP image**, `loading="lazy"` for everything else
- **Preload the hero image** in the layout `<head>`:
  ```html
  <link rel="preload" as="image" href={heroSrc} />
  ```

---

## Hydration control

The fewer islands you hydrate, the faster your site. Audit every `client:*` directive.

### Hydration audit checklist
For each component with a `client:*` directive, ask:
1. Does this component actually need JavaScript? (Display-only components don't)
2. Is this component above the fold? (If not, use `client:visible`)
3. Is this component critical to the page? (If not, use `client:idle`)
4. Does the entire component need to be interactive? (Extract the interactive part into a smaller island)

### Splitting large components
Instead of hydrating a large component:
```astro
<!-- Bad: hydrates the entire product card just for the "Add to Cart" button -->
<ProductCard client:load product={product} />
```

Split into static + interactive:
```astro
<!-- Good: static HTML for display, tiny island for the button -->
<ProductCard product={product} />
<AddToCartButton client:idle productId={product.id} />
```

### Measuring hydration cost
Check the Network tab in DevTools:
- Filter by JS files
- Look at which islands load the most JS
- Each island creates its own chunk — find the heavy ones

---

## Bundle analysis

### Inspect the build output
```bash
npm run build
# Check dist/ size
du -sh dist/
du -sh dist/_astro/  # JS + CSS bundles
```

### Use Vite's bundle visualizer
```js
// astro.config.mjs
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        plugins: [
          // Add visualizer plugin
        ],
      },
    },
  },
});
```

Or install and use `vite-bundle-visualizer`:
```bash
npx vite-bundle-visualizer
```

### Common bundle bloat sources
- **Large framework islands**: A single Vue/React component pulls in the entire framework runtime
- **Unused library imports**: `import _ from 'lodash'` instead of `import debounce from 'lodash/debounce'`
- **All-in-one component libraries**: Import only the components you use
- **Client-side routing libraries**: Astro doesn't need SPA routing; use standard `<a>` tags

---

## Font loading

Fonts are a common LCP blocker. Load them efficiently.

### Self-host fonts (preferred)
```css
/* src/styles/fonts.css */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Show fallback immediately, swap when loaded */
}
```

### Preload critical fonts
```astro
<!-- In your layout <head> -->
<link
  rel="preload"
  href="/fonts/myfont.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>
```

### Font subsetting
Only include the characters you need:
```bash
# Using glyphhanger
npx glyphhanger --whitelist="US_ASCII" --subset=myfont.ttf
```

### Google Fonts the right way
Don't use the default `<link>` tag. Instead, self-host:
```bash
# Download fonts locally with fontsource
npm install @fontsource/inter
```

```ts
// In your layout frontmatter
import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';
```

---

## Prefetching

Astro has built-in link prefetching to speed up navigation.

```js
// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: false,          // Don't prefetch everything
    defaultStrategy: 'viewport', // Prefetch links when they enter viewport
  },
});
```

### Per-link control
```html
<!-- Prefetch on hover (default) -->
<a href="/about" data-astro-prefetch>About</a>

<!-- Prefetch when visible in viewport -->
<a href="/blog" data-astro-prefetch="viewport">Blog</a>

<!-- Don't prefetch -->
<a href="/heavy-page" data-astro-prefetch="false">Heavy Page</a>
```

---

## Build performance

### Speed up large SSG builds

**Limit getStaticPaths in development**:
```ts
export async function getStaticPaths() {
  const posts = await getCollection('blog');

  // In dev, only build a few pages for speed
  if (import.meta.env.DEV) {
    return posts.slice(0, 5).map(post => ({
      params: { slug: post.slug },
      props: { post },
    }));
  }

  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}
```

**Deduplicate API calls**: Astro caches `fetch()` calls during static builds. Use the same URL to benefit from deduplication:
```ts
// These two calls hit the API only once
const res1 = await fetch('https://api.example.com/posts');
const res2 = await fetch('https://api.example.com/posts');
```

**Parallelize where possible**: Astro builds pages in parallel. But if your data fetching is sequential, that bottlenecks the build.

### Reduce build output size
- Use `<Image />` instead of raw `<img>` (smaller optimized images)
- Remove unused CSS (Astro scopes styles by default, but global CSS can bloat)
- Check for duplicate dependencies: `npm ls | grep duplicated`


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)