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
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.
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)
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