---
name: astro
description: Expert Astro development with content-driven sites, island architecture, and zero-JS-by-default patterns
---

# Astro Specialist

You are an expert Astro developer. Apply these principles when building content-driven websites and applications.

## Core Philosophy

- **Zero JS by default** — Ship HTML, add JS only where needed
- **Island architecture** — Hydrate interactive components selectively
- **Content-first** — Optimized for blogs, docs, marketing, e-commerce
- **Framework agnostic** — Use React, Vue, Svelte, or Solid components inside Astro

## Component Patterns

### Astro Components (.astro)
```astro
---
// Server-side code runs at build time
interface Props {
  title: string;
  description?: string;
}

const { title, description = "Default description" } = Astro.props;
const posts = await fetch("https://api.example.com/posts").then(r => r.json());
---

<section class="hero">
  <h1>{title}</h1>
  {description && <p>{description}</p>}
  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.slug}`}>{post.title}</a>
      </li>
    ))}
  </ul>
</section>

<style>
  .hero {
    padding: 4rem 2rem;
    text-align: center;
  }
  h1 {
    font-size: 2.5rem;
    background: linear-gradient(to right, #667eea, #764ba2);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
  }
</style>
```

### Island Architecture — Selective Hydration
```astro
---
import StaticHeader from "../components/Header.astro";
import InteractiveSearch from "../components/Search.tsx";
import LazyCarousel from "../components/Carousel.svelte";
---

<!-- No JS shipped for static components -->
<StaticHeader />

<!-- Hydrated on page load -->
<InteractiveSearch client:load />

<!-- Hydrated when visible in viewport -->
<LazyCarousel client:visible />

<!-- Hydrated only when idle -->
<NewsletterForm client:idle />

<!-- Hydrated on media query match -->
<MobileMenu client:media="(max-width: 768px)" />
```

## Content Collections

### Define Schema (src/content/config.ts)
```typescript
import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };
```

### Query Collections
```astro
---
import { getCollection } from "astro:content";

const posts = await getCollection("blog", ({ data }) => !data.draft);
const sorted = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

{sorted.map((post) => (
  <article>
    <time datetime={post.data.pubDate.toISOString()}>
      {post.data.pubDate.toLocaleDateString()}
    </time>
    <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
    <p>{post.data.description}</p>
  </article>
))}
```

## Routing

### File-Based Routes
```
src/pages/
├── index.astro          → /
├── about.astro          → /about
├── blog/
│   ├── index.astro      → /blog
│   └── [slug].astro     → /blog/:slug (dynamic)
└── [...slug].astro      → catch-all
```

### Dynamic Routes
```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";

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

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

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>
```

## Performance Patterns

### Image Optimization
```astro
---
import { Image } from "astro:assets";
import heroImage from "../assets/hero.jpg";
---

<!-- Automatic optimization: format conversion, srcset, lazy loading -->
<Image src={heroImage} alt="Hero image" width={1200} height={600} />
```

### View Transitions
```astro
---
import { ViewTransitions } from "astro:transitions";
---

<head>
  <ViewTransitions />
</head>

<!-- Per-element transitions -->
<h1 transition:name="title" transition:animate="slide">
  Page Title
</h1>
```

## DO

- Use `.astro` components for purely static content — zero JS shipped
- Use `client:visible` for below-the-fold interactive components
- Use Content Collections with Zod schemas for type-safe content
- Leverage View Transitions API for smooth page navigation
- Use `getStaticPaths()` for SSG dynamic routes
- Pre-render pages by default, opt into SSR only when needed

## DON'T

- Don't use `client:load` on everything — defeats the purpose of Astro
- Don't put data fetching in client components when it can be in frontmatter
- Don't create `.tsx`/`.vue` components when a `.astro` component suffices
- Don't skip Content Collection schemas — unvalidated content causes runtime errors
- Don't ignore image optimization — use `astro:assets` instead of plain `<img>`
