.agents/skills/astro/SKILL.md
Astro content-focused web framework. Covers islands architecture, content collections, and multi-framework support. Use when building content-heavy or static websites. USE WHEN: user mentions "Astro", asks about "islands architecture", "content collections", "Astro components", "client directives", "Astro.glob", "static site generation with Astro", "multi-framework in Astro" DO NOT USE FOR: Next.js - use `nextjs-app-router` instead; Nuxt - use `nuxt3` instead; SvelteKit - use `sveltekit` instead; Gatsby - use Astro as modern alternative; pure React/Vue/Svelte - use respective framework skills
npx skillsauth add d-subrahmanyam/deno-fresh-microservices astroInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:astrofor comprehensive documentation.
---
// Component script (runs at build time)
import Header from '../components/Header.astro';
import ReactCounter from '../components/Counter.tsx';
const { title } = Astro.props;
const posts = await Astro.glob('./posts/*.md');
---
<!-- Component template -->
<html>
<head><title>{title}</title></head>
<body>
<Header />
<main>
<slot />
</main>
<!-- Island: hydrates on client -->
<ReactCounter client:load />
</body>
</html>
<style>
main { max-width: 800px; }
</style>
| Directive | Behavior |
|-----------|----------|
| client:load | Hydrate immediately |
| client:idle | Hydrate when idle |
| client:visible | Hydrate when visible |
| client:media | Hydrate on media query |
| client:only | Skip SSR, client only |
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
---
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
security: {
checkOrigin: true, // CSRF protection for SSR
},
vite: {
define: {
// Never expose secrets to client
'import.meta.env.SECRET_KEY': 'undefined',
},
},
});
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
return response;
});
// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: ({ image }) =>
z.object({
title: z.string().max(100),
description: z.string().max(200),
date: z.date(),
author: reference('authors'),
cover: image().refine((img) => img.width >= 800, {
message: 'Cover image must be at least 800px wide',
}),
tags: z.array(z.string()).max(5),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
// astro.config.mjs
import { defineConfig } from 'astro/config';
import compress from 'astro-compress';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [
sitemap(),
compress({
CSS: true,
HTML: true,
Image: true,
JavaScript: true,
SVG: true,
}),
],
build: {
inlineStylesheets: 'auto',
},
prefetch: {
prefetchAll: true,
defaultStrategy: 'viewport',
},
});
---
// Image optimization
import { Image, getImage } from 'astro:assets';
import heroImage from '../assets/hero.png';
const optimizedBackground = await getImage({ src: heroImage, format: 'webp' });
---
<Image
src={heroImage}
alt="Hero"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px"
loading="eager"
/>
<!-- Lazy hydration for islands -->
<ReactWidget client:visible />
<!-- View Transitions -->
<ViewTransitions />
---
// src/pages/404.astro
import Layout from '../layouts/Layout.astro';
---
<Layout title="Page Not Found">
<div class="error-page">
<h1>404</h1>
<p>Page not found</p>
<a href="/">Go home</a>
</div>
</Layout>
---
// src/pages/500.astro
import Layout from '../layouts/Layout.astro';
---
<Layout title="Server Error">
<div class="error-page">
<h1>500</h1>
<p>Something went wrong</p>
<a href="/">Go home</a>
</div>
</Layout>
// src/pages/api/data.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ request }) => {
try {
const data = await fetchData();
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('API error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
// tests/e2e/blog.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Blog', () => {
test('lists published posts', async ({ page }) => {
await page.goto('/blog');
const posts = page.locator('article');
await expect(posts).toHaveCount(await posts.count());
await expect(posts.first()).toBeVisible();
});
test('navigates to post', async ({ page }) => {
await page.goto('/blog');
await page.click('article a');
await expect(page.locator('h1')).toBeVisible();
await expect(page).toHaveURL(/\/blog\/.+/);
});
});
// Component testing with container queries
test('island hydrates on visibility', async ({ page }) => {
await page.goto('/');
const counter = page.locator('[data-testid="counter"]');
await expect(counter).not.toBeVisible();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await expect(counter).toBeVisible();
});
# Vercel - vercel.json
{
"buildCommand": "astro build",
"outputDirectory": "dist",
"framework": "astro"
}
# Netlify - netlify.toml
[build]
command = "astro build"
publish = "dist"
# Docker
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
| Metric | Target | |--------|--------| | Lighthouse Score | > 95 | | First Contentful Paint | < 1s | | Time to Interactive | < 1.5s | | Total Blocking Time | < 50ms | | Bundle size (JS) | < 50KB |
This skill is for Astro (content-focused, islands architecture). DO NOT use for:
nextjs-app-router skill insteadnuxt3 skill insteadsveltekit skill insteadremix skill insteadfrontend-react skill insteadfrontend-vue skill insteadfrontend-svelte skill instead| Anti-Pattern | Why It's Wrong | Correct Approach | |--------------|----------------|------------------| | Using client:load everywhere | Defeats zero-JS philosophy, large bundles | Use client:idle or client:visible for deferred hydration | | Not using content collections | Unvalidated content, no type safety | Define collections in src/content/config.ts | | Mixing frameworks unnecessarily | Increases bundle size, complexity | Use one framework per project, or Astro components | | Ignoring image optimization | Poor performance, large assets | Use <Image> from astro:assets | | Not setting alt text on images | Accessibility issue, SEO penalty | Always provide meaningful alt text | | Using client:only for all content | No SSR, poor SEO | Use client:only only for browser-only components | | Hardcoding data in components | Unmaintainable, no CMS integration | Use content collections or API fetching | | No ViewTransitions | Choppy navigation UX | Add <ViewTransitions /> to layout |
| Issue | Possible Cause | Solution | |-------|----------------|----------| | "Cannot use import.meta.env in client" | Accessing server-only env var | Prefix with PUBLIC_ for client access | | Island not hydrating | Wrong client directive | Check client:load/idle/visible directive is set | | Content collection not found | Schema not defined or wrong path | Define in src/content/config.ts, check src/content/{collection} | | Images not optimized | Using <img> instead of <Image> | Import and use <Image> from astro:assets | | "getCollection is not defined" | Wrong import | Import from 'astro:content' | | Build fails with type errors | Content schema mismatch | Check frontmatter matches Zod schema | | 404 page not showing | Missing src/pages/404.astro | Create 404.astro in src/pages/ | | CSS not scoped | Missing <style> in component | Add <style> block to Astro component |
development
Guidelines for building high-performance APIs with Fastify and TypeScript, covering validation, Prisma integration, and testing best practices
development
FastAPI modern Python web framework. Covers routing, Pydantic models, dependency injection, and async support. Use when building Python APIs. USE WHEN: user mentions "fastapi", "pydantic", "async python api", "python rest api", asks about "dependency injection python", "python openapi", "python swagger", "async endpoints", "python api validation", "fastapi middleware" DO NOT USE FOR: Django apps - use `django` instead, Flask apps - use `flask` instead, synchronous Python APIs without type hints, GraphQL-only APIs
tools
FastAPI integration testing specialist. Covers synchronous TestClient, async httpx AsyncClient, dependency injection overrides, auth testing (JWT, OAuth2, API keys), WebSocket testing, file uploads, background tasks, middleware testing, and HTTP mocking with respx, responses, and pytest-httpserver. USE WHEN: user mentions "FastAPI test", "TestClient", "httpx async test", "dependency override test", "respx mock", asks about testing FastAPI endpoints, authentication in tests, or HTTP client mocking. DO NOT USE FOR: Django - use `pytest-django`; pytest internals - use `pytest`; Container infrastructure - use `testcontainers-python`
development
Expert in FastAPI Python development with best practices for APIs and async operations