Astro — Rules and Conventions
1. Philosophy
- Zero-JS by default — HTML-first. JavaScript only when explicitly opted in via hydration directives.
- Islands architecture — Interactive components as islands in a sea of static HTML. No framework overhead for static content.
- Content-first — Content Collections + MDX as first-class citizens. Type-safe content authoring.
- Framework agnostic — Bring your own UI framework (React, Vue, Svelte, Solid) or use vanilla.
- Performance baseline — Static output by default. SSR/hybrid only when needed.
2. Minimum Versions
| Technology | Minimum Version |
|---|---|
| Astro | 4.15+ |
| Node.js | 22+ |
| pnpm | 11+ |
3. Setup and Project Structure
Initialize
# New project
pnpm create astro@latest my-app -- --template minimal
# Add integrations
pnpm astro add react tailwind sitemap
Minimal structure
src/
components/ # .astro + framework components
content/ # Content Collections (see §7)
layouts/ # Layout components
pages/ # File-based routing
styles/ # Global CSS
env.d.ts # Type declarations
astro.config.mjs
tsconfig.json
4. Astro Components (.astro)
Structure
---
// Frontmatter: server-only code (runs at build/request time)
import Layout from '../layouts/Layout.astro'
import { getCollection } from 'astro:content'
const posts = await getCollection('blog')
const { title } = Astro.props
---
<!-- Template: HTML + component syntax -->
<Layout title={title}>
<h1>{title}</h1>
<ul>
{posts.map(post => <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>)}
</ul>
</Layout>
<style>
/* Scoped styles by default */
h1 { color: var(--color-primary); }
</style>
Rules
| Aspect | Rule | Why |
|---|---|---|
| Frontmatter | Server-only, no browser APIs | Runs at build/SSR time |
| Props | Define with Astro.props + TypeScript interface |
Type-safe component API |
| Slots | <slot /> for content projection |
Standard composition |
| Styles | Scoped by default, is:global for global |
Prevents style leakage |
| Scripts | Hoisted to <head> by default |
Non-blocking, use is:inline for inline |
HTML/CSS/JS basics: see
html,css,javascriptskills.
5. Routing
File-based routes
src/pages/
index.astro → /
blog/[...slug].astro → /blog/* (rest param)
blog/[slug].astro → /blog/:slug
api/users.json.ts → /api/users.json (endpoint)
Dynamic routes (getStaticPaths)
---
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}))
}
const { post } = Astro.props
---
<article>{post.body}</article>
Rules Routing
getStaticPathsrequired for dynamic SSG routes- Rest params
[...slug]for catch-all routes - Endpoints (
.ts/.jsin pages/) returnResponse— see Astro Actions (§14)
6. Rendering Modes
| Mode | Config | Output | Use Case |
|---|---|---|---|
| SSG | output: 'static' (default) |
Static HTML at build | Blogs, docs, marketing |
| SSR | output: 'server' |
HTML per request | Auth, dynamic data |
| Hybrid | output: 'hybrid' + prerender |
Mix static + dynamic | Most apps |
Hybrid config
// astro.config.mjs
export default defineConfig({
output: "hybrid",
adapter: node(), // or vercel, netlify, etc.
});
---
// src/pages/dashboard.astro
export const prerender = false // Opt-out of SSG for this page
---
SSR/Deployment details: see
deployskill.
7. Content Collections (Content Layer API)
Define collection
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
heroImage: z.string().optional(),
}),
});
export const collections = { blog };
Query content
---
import { getCollection, getEntry } from 'astro:content'
const posts = await getCollection('blog', ({ data }) => !data.draft)
const post = await getEntry('blog', 'my-post')
---
Rules Content
defineCollectioninsrc/content/config.ts— single source of truth- Zod schema — type-safe frontmatter validation
getCollectionfor lists,getEntryfor single itemstype: 'content'for MD/MDX,type: 'data'for JSON/YAML
8. Markdown and MDX
Frontmatter
---
title: "Post Title"
description: "Summary"
pubDate: 2024-01-15
tags: ["astro", "typescript"]
heroImage: "/hero.jpg"
---
Content here...
MDX components
import { Alert } from "../components/Alert.astro";
import MyReactComponent from "../components/MyReactComponent.jsx";
<Alert type="info">Astro + MDX = ❤️</Alert>
<MyReactComponent client:visible />
Rules Markdown
- MDX = Markdown + components + JSX
- Components in MDX require hydration directive (
client:*) - Remark/Rehype plugins via
astro.config.mjs→markdownconfig
9. Partial Hydration (Islands)
Directives
| Directive | Hydration Trigger | Use Case |
|---|---|---|
client:load |
Immediately | Critical interactive |
client:visible |
IntersectionObserver | Below-fold widgets |
client:idle |
requestIdleCallback |
Non-urgent |
client:media |
Media query match | Responsive islands |
client:only |
Only client, no SSR | Framework-only components |
Examples
---
import Counter from './Counter.jsx'
import HeavyChart from './HeavyChart.svelte'
---
<Counter client:load />
<HeavyChart client:visible />
<AdSlot client:media="(min-width: 768px)" />
<LegacyWidget client:only="react" />
Rules Partial
- Default: no JS — static HTML only
- Choose least eager directive that works
client:onlyskips SSR entirely — use sparingly- Framework components must be in
src/components/or installed pkg
10. UI Framework Integrations
Install
pnpm astro add react # or vue, svelte, solid, preact
Config
// astro.config.mjs
import react from "@astrojs/react";
import tailwind from "@astrojs/tailwind";
export default defineConfig({
integrations: [react(), tailwind()],
});
Usage
---
import ReactCounter from './ReactCounter.jsx'
import VueWidget from './VueWidget.vue'
---
<ReactCounter client:visible />
<VueWidget client:idle />
Tailwind/Sass: see
tailwindcssandsassskills. TypeScript: seetypescriptskill.
11. Layouts
Basic layout
<!-- src/layouts/Layout.astro -->
---
interface Props { title: string }
const { title } = Astro.props
---
<html lang="en">
<head><title>{title}</title></head>
<body><slot /></body>
</html>
Markdown layout
<!-- src/layouts/PostLayout.astro -->
---
import { getCollection } from 'astro:content'
const { frontmatter, ... } = Astro.props
const related = await getCollection('blog', ...)
---
<BaseLayout title={frontmatter.title}>
<article>
<h1>{frontmatter.title}</h1>
<slot />
</article>
<aside>{related.map(...)}</aside>
</BaseLayout>
12. Scripts and Styles
Scripts
---
// Hoisted to <head> (module script)
const apiUrl = import.meta.env.API_URL
---
<script>
// Inline, not bundled — use sparingly
console.log('Inline script')
</script>
<script is:inline>
// Inlined as-is, not processed
</script>
Styles
<style>
/* Scoped to this component */
h1 { color: red; }
</style>
<style is:global>
/* Global — use for CSS resets, variables */
:root { --color-primary: #0066cc; }
</style>
Rules Script and Style
- Default scoped — no class naming convention needed
is:globalonly for reset, variables, keyframes- CSS variables for theming — see
cssskill
13. Images and Assets
Astro Image
---
import { Image } from 'astro:assets'
import hero from '../assets/hero.jpg'
---
<Image src={hero} alt="Hero" width={800} height={400} format="avif" />
Rules Image and Assets
astro:assetsfor local images — optimized, hashed, responsiveformat="avif"— best compression, fallback handled- Public assets in
public/— served as-is, no optimization - Remote images — use
<img>withloading="lazy"
14. View Transitions
ClientRouter (SPA-like navigation)
// astro.config.mjs
export default defineConfig({
viewTransitions: true,
});
---
import { ViewTransitions } from 'astro:transitions'
---
<head>
<ViewTransitions />
</head>
Rules View transitions
viewTransitions: trueenables client-side navigation<ViewTransitions />in<head>— requiredtransition:namefor element-level animations- Fallback — works without JS (full page reload)
15. Astro Actions
Define action
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
export const server = {
signup: defineAction({
input: z.object({ email: z.string().email() }),
handler: async ({ email }) => {
// Server-only code (DB, email, etc.)
return { success: true };
},
}),
};
Use in component
---
import { actions } from 'astro:actions'
---
<script>
const form = document.querySelector('form')
form.addEventListener('submit', async (e) => {
e.preventDefault()
const result = await actions.signup({ email: form.email.value })
if (result.error) console.error(result.error)
})
</script>
Rules Astro actions
- Type-safe — input validated via Zod, return type inferred
- Server-only — runs on server, never in browser
- Form integration — works with standard
<form>+actionattr
16. Middleware and SSR APIs
Middleware
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
export const (context, next) => {
// Auth, logging, redirects
if (context.url.pathname.startsWith("/admin")) {
const session = await getSession(context.cookies);
if (!session) return context.redirect("/login");
}
return next();
});
SSR APIs
---
// Access request/response
const { request, response, cookies, locals, params, url } = Astro
---
Rules Middleware
- Middleware runs before every request (SSR/hybrid only)
localsfor request-scoped data (user, session)cookies— sign/unsign withastro:envsecret
17. Environment Variables
astro:env (type-safe)
// env.d.ts
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly SECRET_DB_URL: string;
}
---
// Public: available in browser
const apiUrl = import.meta.env.PUBLIC_API_URL
// Secret: server-only (SSS/hybrid only)
const dbUrl = import.meta.env.SECRET_DB_URL
---
Rules environment
PUBLIC_prefix — exposed to client (like Vite'sVITE_)- No prefix — server-only, stripped from client bundle
astro:env— validates at build, TypeScript types auto-generated
18. Methodology
Before using ANY Astro config/integration/pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor Astro/integrations. - Official docs: astro.build — verify current API + integrations.
- Project config:
astro.config.mjs,tsconfig.json,src/content/config.ts— verify against actual setup. - HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
19. Prohibitions
- ❌ Do not use
client:loadby default — choose least eager directive - ❌ Do not put secrets in
PUBLIC_env vars — server-only without prefix - ❌ Do not skip
getStaticPathsfor dynamic SSG routes - ❌ Do not use framework components without hydration directive
- ❌ Do not put client-side logic in frontmatter — runs on server
- ❌ Do not use
client:onlyunless framework has no SSR support - ❌ Do not commit
dist/or.astro/— add to.gitignore - ❌ Do not bypass Content Collections schema — loses type safety
20. References
Note: For HTML conventions, see HTML Note: For CSS conventions, see CSS Note: For JavaScript conventions, see JavaScript Note: For TypeScript rules, see TypeScript Note: For package manager conventions, see Package Manager Note: For deployment (SSR/adapters), see Deploy Note: For performance (Core Web Vitals), see Performance Note: For accessibility (WCAG), see Accessibility Note: For Vite integration, see Vite Note: For Sass/Tailwind, see Sass / Tailwind CSS
Last updated: 2026-08