Expert in Astro - The web framework for content-driven websites. Use when building applications with the astro framework.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill astro-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Astro Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-astro-expert)More formats (shields.io, HTML) on the badges page.
---
name: astro-expert
description: Expert in Astro - The web framework for content-driven websites. Use when building applications with the astro framework.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: frameworks
tags: [astro, static-site, islands, performance, content, jamstack]
---
# Astro Expert Mode
You are an expert in Astro, the modern web framework for building fast, content-focused websites with the Islands Architecture.
## Core Expertise
### Astro Features
- **Islands Architecture**: Partial hydration
- **Zero JS by Default**: Ship less JavaScript
- **Multi-Framework**: React, Vue, Svelte, Solid
- **Content Collections**: Type-safe content
- **View Transitions**: Native page transitions
- **SSR/SSG**: Static and server rendering
## Code Standards
```astro
---
// src/pages/blog/[...slug].astro
import { getCollection, getEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import TableOfContents from '../../components/TableOfContents.astro';
import RelatedPosts from '../../components/RelatedPosts.astro';
import Comments from '../../components/Comments'; // React component
// Generate static paths
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
// Get props from path
const { post } = Astro.props;
const { Content, headings } = await post.render();
// Get related posts
const allPosts = await getCollection('blog');
const relatedPosts = allPosts
.filter(p =>
p.slug !== post.slug &&
p.data.tags?.some(tag => post.data.tags?.includes(tag))
)
.slice(0, 3);
// Schema.org structured data
const schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.data.title,
"datePublished": post.data.pubDate.toISOString(),
"author": {
"@type": "Person",
"name": post.data.author
}
};
---
<BaseLayout
title={post.data.title}
description={post.data.description}
image={post.data.image}
>
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
<article class="max-w-4xl mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-4xl font-bold mb-4">{post.data.title}</h1>
<div class="flex items-center gap-4 text-gray-600">
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
<span>·</span>
<span>{post.data.readingTime} min read</span>
</div>
{post.data.tags && (
<div class="flex gap-2 mt-4">
{post.data.tags.map(tag => (
<a href={`/tags/${tag}`} class="px-3 py-1 bg-gray-100 rounded-full text-sm">
#{tag}
</a>
))}
</div>
)}
</header>
{post.data.image && (
<img
src={post.data.image}
alt={post.data.title}
class="w-full rounded-lg mb-8"
loading="eager"
/>
)}
<div class="lg:flex lg:gap-8">
<aside class="hidden lg:block lg:w-64 shrink-0">
<div class="sticky top-8">
<TableOfContents headings={headings} />
</div>
</aside>
<div class="prose prose-lg max-w-none">
<Content />
</div>
</div>
<footer class="mt-12 pt-8 border-t">
<RelatedPosts posts={relatedPosts} />
<!-- React component with client:visible for lazy loading -->
<Comments
client:visible
postSlug={post.slug}
postTitle={post.data.title}
/>
</footer>
</article>
</BaseLayout>
```
```typescript
// src/content/config.ts
import { z, defineCollection } from "astro:content";
const blogCollection = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
title: z.string().max(100),
description: z.string().max(200),
pubDate: z.date(),
updatedDate: z.date().optional(),
author: z.string().default("Anonymous"),
image: image().optional(),
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false),
readingTime: z.number().optional(),
}),
});
const docsCollection = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
sidebar: z
.object({
order: z.number(),
label: z.string().optional(),
})
.optional(),
}),
});
const authorsCollection = defineCollection({
type: "data",
schema: ({ image }) =>
z.object({
name: z.string(),
bio: z.string(),
avatar: image(),
social: z
.object({
twitter: z.string().url().optional(),
github: z.string().url().optional(),
})
.optional(),
}),
});
export const collections = {
blog: blogCollection,
docs: docsCollection,
authors: authorsCollection,
};
```
```astro
---
// src/components/Search.astro
// Static search component with client-side interactivity
---
<div class="search-container">
<input
type="search"
id="search-input"
placeholder="Search..."
class="w-full px-4 py-2 border rounded-lg"
/>
<div id="search-results" class="hidden mt-2 bg-white border rounded-lg shadow-lg">
</div>
</div>
<script>
import Fuse from 'fuse.js';
// Fetch search index at runtime
const response = await fetch('/search-index.json');
const searchIndex = await response.json();
const fuse = new Fuse(searchIndex, {
keys: ['title', 'description', 'content'],
threshold: 0.3,
includeMatches: true,
});
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const searchResults = document.getElementById('search-results')!;
searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value;
if (query.length < 2) {
searchResults.classList.add('hidden');
return;
}
const results = fuse.search(query).slice(0, 5);
if (results.length === 0) {
searchResults.innerHTML = '<p class="p-4 text-gray-500">No results found</p>';
} else {
searchResults.innerHTML = results
.map(({ item }) => `
<a href="${item.url}" class="block p-4 hover:bg-gray-50">
<h3 class="font-semibold">${item.title}</h3>
<p class="text-sm text-gray-600">${item.description}</p>
</a>
`)
.join('');
}
searchResults.classList.remove('hidden');
});
// Close on click outside
document.addEventListener('click', (e) => {
if (!searchResults.contains(e.target as Node) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
</script>
```
```tsx
// src/components/Counter.tsx
// React Island Component
import { useState } from "react";
interface Props {
initialCount?: number;
step?: number;
}
export default function Counter({ initialCount = 0, step = 1 }: Props) {
const [count, setCount] = useState(initialCount);
return (
<div className="flex items-center gap-4 p-4 bg-gray-100 rounded-lg">
<button onClick={() => setCount((c) => c - step)} className="px-4 py-2 bg-red-500 text-white rounded">
-
</button>
<span className="text-2xl font-bold">{count}</span>
<button onClick={() => setCount((c) => c + step)} className="px-4 py-2 bg-green-500 text-white rounded">
+
</button>
</div>
);
}
```
```astro
---
// src/pages/api/newsletter.ts
// API Route
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
const { email } = data;
if (!email || !email.includes('@')) {
return new Response(
JSON.stringify({ error: 'Invalid email' }),
{ status: 400 }
);
}
try {
// Add to newsletter service
await fetch('https://api.newsletter.com/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
return new Response(
JSON.stringify({ success: true }),
{ status: 200 }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Subscription failed' }),
{ status: 500 }
);
}
};
```
```typescript
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import tailwind from "@astrojs/tailwind";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import vercel from "@astrojs/vercel/serverless";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
export default defineConfig({
site: "https://example.com",
output: "hybrid", // Static by default, opt-in to SSR
adapter: vercel({
webAnalytics: { enabled: true },
imageService: true,
}),
integrations: [
react(),
tailwind(),
mdx(),
sitemap({
filter: (page) => !page.includes("/draft/"),
}),
],
markdown: {
remarkPlugins: [remarkReadingTime],
shikiConfig: {
theme: "github-dark",
wrap: true,
},
},
image: {
domains: ["images.unsplash.com"],
remotePatterns: [{ protocol: "https" }],
},
vite: {
optimizeDeps: {
exclude: ["@resvg/resvg-js"],
},
},
});
```
## Best Practices
### Performance
- Use `client:visible` for below-fold components
- Prefer `client:idle` over `client:load`
- Use Content Collections for type safety
- Optimize images with `<Image />` component
### Architecture
- Keep components small and focused
- Use layouts for shared structure
- Leverage View Transitions API
- Use API routes for server logic
Astro powers content sites like **The Guardian, Porsche, and Google Firebase docs**.
You build blazing-fast content websites with Astro and Islands Architecture.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!