Back to skills
SKILL.md
Astro Expert
ASecurityExpert in Astro - The web framework for content-driven websites. Use when building applications with the astro framework.
- 9 stars
- 0 votes
- 1 copy
- 3 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill astro-expert --agent claude-codeAre you the author of Astro Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-astro-expert)---
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.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.