Loading...
Loading...
Comprehensive Astro web framework development guidance for 2026. Use when building, configuring, or troubleshooting Astro projects; creating components; setting up routing; implementing islands architecture; working with React, Tailwind CSS, and Node.js integrations; testing; performance optimization; or deployment strategies. Includes TypeScript patterns, state management, API routes, and common pitfalls solutions.
npx skill4agent add mokbhai/claude astro-developer# Basic Astro project
npm create astro@latest my-project
# With specific integrations
npm create astro@latest -- --add react --add tailwind --add mdx
# From template
npm create astro@latest -- --template blogsrc/
├── components/ # Reusable Astro/UI framework components
├── layouts/ # Page layout templates
├── pages/ # File-based routing (REQUIRED)
├── styles/ # CSS/Sass files
├── content/ # Content collections (Markdown/MDX)
└── env.d.ts # TypeScript environment types
public/ # Static assets (robots.txt, favicon, etc)
astro.config.mjs # Astro configuration
tsconfig.json # TypeScript configuration
package.json # Project dependencies and scripts---
// Component frontmatter - runs on server only
import { SITE_TITLE } from '@/config';
export interface Props {
title?: string;
published?: Date;
variant?: 'default' | 'primary' | 'secondary';
}
const { title, published, variant = 'default' } = Astro.props;
const variantClasses = {
default: 'bg-white dark:bg-gray-800',
primary: 'bg-blue-500 dark:bg-blue-600 text-white',
secondary: 'bg-gray-100 dark:bg-gray-700'
};
---
<!-- Component template - HTML with special syntax -->
<html lang="en" class="h-full">
<head>
<title>{title || SITE_TITLE}</title>
</head>
<body class="h-full m-0 font-sans leading-relaxed text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-900">
<main class={`${variantClasses[variant]} p-6 rounded-lg shadow-sm`}>
{title && <h1 class="text-2xl font-bold mb-4">{title}</h1>}
{published && (
<time class="text-sm text-gray-600 dark:text-gray-400">
{published.toLocaleDateString()}
</time>
)}
<slot /> <!-- Children content -->
</main>
</body>
</html>---
import ReactCounter from '@/components/ReactCounter.jsx';
import VueComponent from '@/components/VueComponent.vue';
import SvelteButton from '@/components/SvelteButton.svelte';
---
<!-- Load immediately -->
<ReactCounter client:load />
<!-- Load when visible in viewport -->
<SvelteButton client:visible />
<!-- Load when browser is idle -->
<VueComponent client:idle />
<!-- Load only on mobile -->
<ReactCounter client:load media="(max-width: 768px)" />client:loadclient:idleclient:visibleclient:media---
import InteractiveHeader from '@/components/InteractiveHeader.astro';
import ImageCarousel from '@/components/ImageCarousel.jsx';
import NewsletterForm from '@/components/NewsletterForm.jsx';
import SocialShare from '@/components/SocialShare.jsx';
---
<!-- Above fold, critical interactivity -->
<InteractiveHeader client:load />
<!-- Content heavy, load when visible -->
<ImageCarousel client:visible />
<!-- Secondary feature, load when idle -->
<NewsletterForm client:idle />
<!-- Mobile-only interactivity -->
<SocialShare client:load media="(max-width: 768px)" />server:defer---
import UserProfile from '@/components/UserProfile.astro';
import RecommendedPosts from '@/components/RecommendedPosts.astro';
---
<!-- Static content loads immediately -->
<main>
<h1>Welcome to our blog</h1>
<p>Explore our latest articles...</p>
</main>
<!-- Dynamic content loads in parallel -->
<aside>
<!-- User's profile with personalized data -->
<UserProfile server:defer />
<!-- Recommended posts based on history -->
<RecommendedPosts server:defer />
</aside>src/pages/
├── index.astro # → /
├── about.astro # → /about
├── blog/
│ ├── index.astro # → /blog
│ ├── [slug].astro # → /blog/post-title
│ └── [...page].astro # → /blog/2, /blog/3
└── api/
└── posts.json.js # → /api/posts (API endpoint)---
// 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 { Content, frontmatter } = Astro.props;
---
<h1>{frontmatter.title}</h1>
<p>Published: {frontmatter.pubDate.toLocaleDateString()}</p>
<Content />// src/pages/api/posts.json.js
export async function GET() {
return Response.json({
posts: [
{ id: 1, title: "First post" },
{ id: 2, title: "Second post" },
],
});
}
export async function POST({ request }) {
const data = await request.json();
// Process form submission
return Response.json({ success: true });
}// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
description: z.string(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
const projects = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
startDate: z.date(),
endDate: z.date().optional(),
technologies: z.array(z.string()),
demoUrl: z.string().url().optional(),
repoUrl: z.string().url().optional(),
}),
});
export const collections = { blog, projects };---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BlogLayout from '@/layouts/BlogLayout.astro';
import BlogPost from '@/components/BlogPost.astro';
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sortedPosts = posts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<BlogLayout title="Blog">
{sortedPosts.map((post) => (
<BlogPost post={post} />
))}
</BlogLayout>// astro.config.mjs
import tailwind from "@astrojs/tailwind";
export default defineConfig({
integrations: [tailwind()],
});// tailwind.config.js
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
darkMode: "class", // Enables dark mode with .dark class
theme: {
extend: {
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
colors: {
// Define brand colors
primary: {
50: "#eff6ff",
500: "#3b82f6",
600: "#2563eb",
900: "#1e3a8a",
},
},
},
},
plugins: [],
};/* src/styles/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Custom base styles */
}
@layer components {
/* Custom component classes */
}global.css<!-- ✅ Use brand colors -->
<div class="bg-mitra-blue text-warm-cream">
<div class="border-lush-green bg-warm-orange/10">
<!-- ❌ Avoid generic colors -->
<div class="bg-blue-500 text-yellow-100">
<div class="border-green-500 bg-orange-100">mitra-bluemitra-yellowlush-greenwarm-orangecharcoal-greywarm-creambg-magic-gradientbg-linear-to-brconst buttonVariants = {
primary: "bg-mitra-blue hover:bg-mitra-blue-dark text-white",
secondary: "bg-warm-cream hover:bg-grey-100 text-charcoal-grey",
accent: "bg-warm-orange hover:bg-warm-orange-dark text-white",
};<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">dark:<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"><!-- ✅ Using brand colors -->
<div class="flex flex-col items-center justify-center p-8 bg-linear-to-br from-mitra-blue/10 to-lush-green/10 rounded-2xl shadow-xl">
<div class="p-6 border border-warm-orange/20 bg-magic-gradient"><div style="--delay: 100ms" class="animate-pulse [--delay:200ms]">bg-gradient-to-*bg-linear-to-*bg-gradient-to-rbg-linear-to-rflex-shrink-0shrink-0aspect-[3/2]aspect-3/2grayscale-[30%]grayscale-30<!-- Correct (Tailwind v4) -->
<div class="bg-linear-to-r from-blue-500 to-purple-600 shrink-0 aspect-3/2 grayscale-30">
<!-- Incorrect (deprecated) -->
<!-- These will trigger linter warnings: -->
<!-- bg-gradient-to-r → should be bg-linear-to-r -->
<!-- flex-shrink-0 → should be shrink-0 -->
<!-- aspect-[3/2] → should be aspect-3/2 -->
<!-- grayscale-[30%] → should be grayscale-30 -->// astro.config.mjs
import react from "@astrojs/react";
export default defineConfig({
integrations: [react()],
});// astro.config.mjs
import mdx from "@astrojs/mdx";
export default defineConfig({
integrations: [mdx()],
});---
import { Image } from 'astro:assets';
import heroImage from '@/images/hero.jpg';
---
<!-- Optimized, responsive images -->
<Image
src={heroImage}
alt="Hero section background"
widths={[400, 800, 1200]}
formats={['avif', 'webp', 'jpg']}
loading="eager"
/>---
// src/layouts/MainLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<header>
<nav>
<a href="/" transition:name="home">Home</a>
<a href="/about" transition:name="about">About</a>
</nav>
</header>
<main transition:name="main-content">
<slot />
</main>
</body>
</html>import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
export default defineConfig({
// Site metadata for SEO
site: "https://yoursite.com",
base: "/subpath", // If deploying to subdirectory
// Integrations
integrations: [react(), tailwind(), sitemap()],
// Build optimizations
build: {
format: "directory", // Clean URLs
assets: "_assets", // Custom assets path
},
// Vite configurations
vite: {
optimizeDeps: {
exclude: ["some-large-package"],
},
},
// Server options for SSR
output: "hybrid", // or 'server' or 'static'
// Security headers
security: {
allowedHosts: ["yoursite.com"],
},
});// tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/layouts/*": ["./src/layouts/*"]
},
"types": ["@astrojs/image/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}npm run build # Creates static files in dist/// astro.config.mjs
export default defineConfig({
output: 'hybrid',
});
// Individual pages can opt-in to SSR
---
export const prerender = false;
---// astro.config.mjs
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone",
}),
});npm install -D vitest @vitest/ui jsdom// vitest.config.ts
import { getViteConfig } from 'astro/config';
export default getViteConfig({
test: {
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],
},
});npm init playwright@latest// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run preview',
url: 'http://localhost:4321/',
},
});import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';
test('Card renders with props', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
props: { title: 'Test' },
});
expect(result).toContain('Test');
});// tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/layouts/*": ["./src/layouts/*"]
}
}
}---
export interface Props {
title: string;
count: number;
isActive?: boolean;
tags: string[];
}
const { title, count, isActive = false, tags } = Astro.props;
---// src/store/useStore.ts
import { create } from 'zustand';
export const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));// Component 1
import { useStore } from '@/store/useStore';
function Counter() {
const { increment } = useStore();
return <button onClick={increment}>+</button>;
}
// Component 2
import { useStore } from '@/store/useStore';
function Display() {
const count = useStore((state) => state.count);
return <div>{count}</div>;
}// Create .env file
PUBLIC_API_URL=https://api.example.com
SECRET_KEY=your-secret-key
// Use in components
const apiUrl = import.meta.env.PUBLIC_API_URL;---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description: string;
image?: string;
url?: string;
}
const { title, description, image, url } = Astro.props;
const siteUrl = Astro.site;
const canonicalUrl = url ? new URL(url, siteUrl) : siteUrl;
---
<html>
<head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
{image && (
<meta property="og:image" content={new URL(image, siteUrl)} />
)}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta name="twitter:card" content="summary_large_image" />
</head>
</html>---
// src/pages/404.astro
import Layout from '@/layouts/Layout.astro';
---
<Layout title="Not Found">
<h1>404 - Page not found</h1>
<p>Sorry, we couldn't find that page.</p>
</Layout>// src/pages/error/[...code].astro
export function getStaticPaths() {
return [{ params: { code: "403" } }, { params: { code: "500" } }];
}
const { code } = Astro.params;
const errorMessages = {
403: "Forbidden",
500: "Internal Server Error",
};client:*---
import ReactComponent from '@/components/ReactComponent.jsx';
---
<!-- ❌ Wrong: No client directive -->
<ReactComponent />
<!-- ✅ Correct: Add appropriate client directive -->
<ReactComponent client:load />documentwindow<script>---
// ❌ Wrong: Browser API in frontmatter
const width = window.innerWidth;
---
<!-- ✅ Correct: Move to script tag -->
<script>
const width = window.innerWidth;
</script>// ❌ Incorrect - TypeScript error
<div ref={(el) => (cardsRef.current[index] = el)}>
// ✅ Correct - Wrap the assignment
<div ref={(el) => {
cardsRef.current[index] = el;
}}>bg-linear-to-*bg-gradient-to-*aspect-3/2aspect-[3/2]shrink-0flex-shrink-0grayscale-30grayscale-[30%]create-component.jsoptimize-images.jsgenerate-sitemap.jsnode scripts/create-component.js --name MyComponent --type astrocomponent-patterns.mdintegration-guide.mdtesting-guide.mdcommon-pitfalls.mdperformance-optimization.mdstate-management.mdRead references/testing-guide.md when setting up testing infrastructure
Read references/common-pitfalls.md when troubleshooting hydration or directive issues
Read references/performance-optimization.md when optimizing build size or load times
Read references/state-management.md when implementing cross-island state sharingcomponent-templates/layout-templates/page-templates/