Rule Writing

CLAUDE.md for Astro Projects

Astro ships zero JS by default — AI generates client-heavy patterns that defeat the purpose. Rules for islands architecture, content collections, and server-first rendering.

7 min read·March 28, 2025

Astro ships zero JS by default — AI generates SPA patterns that defeat the purpose

Islands architecture, content collections, client: directives, and static-first rendering

Why Astro Needs Specific AI Rules

Astro's core value proposition is shipping zero JavaScript by default — HTML pages are fully rendered on the server, and JavaScript is only added where interactivity is needed (islands). AI assistants don't understand this model. They generate React-style components with client-side state, event handlers, and useEffect calls — shipping kilobytes of JavaScript that Astro was designed to eliminate.

The most common AI failures: making every component interactive (client:load on everything), generating React/Vue SPA patterns instead of Astro components (.astro files), ignoring content collections for blog/docs content, using client-side routing instead of multi-page architecture, and importing heavy UI libraries for static content that needs no interactivity.

Astro is the anti-SPA framework. Its rules must actively prevent the SPA patterns that AI defaults to. Without rules, AI generates an Astro project that ships more JS than a plain React app — the worst of both worlds.

Rule 1: Zero JavaScript by Default

The rule: 'Astro components (.astro files) ship zero JavaScript to the browser. They render HTML on the server and send only the HTML. Every component is an .astro file by default — not React, Vue, or Svelte. Only use framework components (React, Vue, Svelte) when the component genuinely needs client-side interactivity. Static content = .astro. Interactive widgets = framework component with client: directive.'

For the test: 'Before adding a framework component, ask: does this need to respond to user events in the browser? If the answer is no, use an .astro component. A navigation menu that's always the same? .astro. A hero section with static text? .astro. A search input with autocomplete? React/Svelte with client:visible.'

AI defaults to React components for everything because React dominates its training data. In Astro, React components add JavaScript — .astro components add zero. This rule inverts the AI's default, making .astro the first choice and React the exception.

  • .astro for all static content — zero JS shipped, server-rendered HTML
  • Framework components (React/Vue/Svelte) only for interactive widgets
  • Test: does it respond to user events? No → .astro. Yes → framework + client:
  • Never client:load on static content — it ships JS for no reason
  • Measure JS shipped with Astro's built-in bundle analyzer
💡 The .astro Test

Before reaching for React/Vue: does this component respond to user events in the browser? No → .astro (zero JS). Yes → framework component with client: directive. This test keeps your bundle minimal.

Rule 2: Islands Architecture and client: Directives

The rule: 'Use client: directives to hydrate interactive components. client:load for above-the-fold interactive components (loads immediately). client:visible for below-the-fold components (loads when scrolled into view). client:idle for non-critical interactivity (loads when browser is idle). client:media for responsive interactivity (loads at specific viewport widths). Never use client:only unless SSR is impossible for the component.'

For choosing the right directive: 'Default to client:visible — it covers most cases and defers loading until the user actually needs the component. Use client:load only for components that must be interactive immediately on page load (auth forms, critical CTAs). Use client:idle for analytics widgets, chat popups, and other low-priority interactivity.'

Islands are Astro's killer feature. Each interactive component is an independent island — it hydrates independently, loads independently, and errors independently. AI that adds client:load to everything turns the page into a single-island SPA, defeating the architecture entirely.

⚠️ client:load Defeats Astro

Adding client:load to every component turns your Astro site into an SPA with extra steps. Default to client:visible — it defers hydration until the user actually sees the component. client:load only for above-the-fold critical interactivity.

Rule 3: Content Collections for Structured Content

The rule: 'Use Astro content collections for all structured content: blog posts, documentation, product catalogs, team bios. Define schemas in src/content/config.ts with Zod. Place content in src/content/[collection]/ as Markdown or MDX files. Query with getCollection() and getEntry() — never read files manually with fs.'

For schemas: 'Define typed schemas for every collection: defineCollection({ type: "content", schema: z.object({ title: z.string(), date: z.date(), tags: z.array(z.string()), draft: z.boolean().default(false) }) }). Schemas validate frontmatter at build time — typos and missing fields are caught before deployment.'

For rendering: 'Use the render() method on collection entries to get the Content component: const { Content } = await entry.render(). Pass Content as an Astro component: <Content />. Use MDX for content that needs interactive components embedded in markdown.'

  • Content collections for all structured content — blog, docs, products
  • Zod schemas in config.ts — validates frontmatter at build time
  • getCollection() and getEntry() for querying — never raw fs reads
  • MDX for content with embedded interactive components
  • Draft support with schema field: draft: z.boolean().default(false)
Build-Time Validation

Content collection Zod schemas validate frontmatter at build time. A typo in a blog post's date field fails the build instead of rendering a broken page. This catches content errors before they reach production.

Rule 4: Routing and Pages

The rule: 'Use file-based routing in src/pages/. Static routes: src/pages/about.astro → /about. Dynamic routes: src/pages/blog/[slug].astro. Use getStaticPaths for dynamic routes with static generation. Use server rendering (output: "server" or "hybrid") for pages that need request-time data. Default to static generation — server render only when necessary.'

For hybrid rendering: 'Use export const prerender = false on individual pages that need server rendering in a hybrid project. Everything else is statically generated. This gives you the performance of static with the flexibility of server where you need it — authentication pages, user dashboards, personalized content.'

For API endpoints: 'API routes in src/pages/api/[route].ts. Export named functions: export async function GET({ params, request }): Promise<Response>. Return new Response() with appropriate headers. Use these for: form submissions, webhooks, and JSON APIs consumed by islands.'

Rule 5: Integrations and Optimizations

The rule: 'Use official Astro integrations: @astrojs/react for React islands, @astrojs/tailwind for Tailwind, @astrojs/mdx for MDX content, @astrojs/sitemap for SEO sitemaps, @astrojs/image for image optimization. Configure in astro.config.mjs. Never manually configure what an integration handles — let the integration do its job.'

For images: 'Use Astro's built-in <Image /> component for all images. It handles: format conversion (WebP, AVIF), responsive sizing, lazy loading, and dimension specification (prevents layout shift). Import images from src/assets/ — Astro optimizes them at build time. Use public/ only for images that shouldn't be processed.'

For performance: 'Astro sites are fast by default — don't add complexity to optimize. Measure with Lighthouse before optimizing. The biggest performance wins: fewer client: directives (less JS), content collections instead of API calls (data at build time), and <Image /> for all images (optimized formats and sizes).'

Complete Astro Rules Template

Consolidated rules for Astro projects.

  • .astro for static content (zero JS) — framework components only for interactivity
  • client:visible by default — client:load for critical, client:idle for low-priority
  • Content collections with Zod schemas — getCollection()/getEntry() for querying
  • File-based routing — getStaticPaths for dynamic — hybrid for selective SSR
  • Official integrations: @astrojs/react, tailwind, mdx, sitemap, image
  • <Image /> for all images — optimized formats, responsive, lazy loading
  • Static generation by default — server render only when request data is needed
  • Measure with Lighthouse — fewer client: directives = less JS = faster