Skip to content
Back to skills

Astro Expert

ASecurity

Expert 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
developmentjavascripttypescriptgojavareactvuenodegitapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill astro-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Astro Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Astro Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-astro-expert/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-astro-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.