mcpField
A
astro

astro

github.com/claude-dev-suite開発4639 回インストール3 日前に更新
リポジトリ

astro について

Astro 開発者向けのリファレンススキルです。コンポーネント、コンテンツコレクション、アイランド設計に加え、本番運用の指針も扱います。

主な機能

  1. 01Astro コンポーネントとクライアントディレクティブ
  2. 02型安全なコンテンツコレクション
  3. 03Markdown と MDX のサポート
  4. 04パフォーマンスとセキュリティの指針
  5. 05一般的な問題のトラブルシューティング

活用シーン

  1. 01コンテンツ中心のサイト構築
  2. 02アイランドの遅延ハイドレーション設定
  3. 03ブログのコンテンツスキーマ検証
  4. 04Astro のビルド問題の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill astro

インストール後、Claude Code で/astroを入力して読み込まれたか確認します。

同梱ファイルquick-ref/content.mdquick-ref/islands.md

詳細

Astro コアナレッジ

深い知識:包括的なドキュメントを参照するには、mcp__documentation__fetch_docs を technology: astro で使用してください。

コンポーネント構造

---
// 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');
---


<html>
  <head><title>{title}</title></head>
  <body>
    <Header />
    <main>
      <slot />
    </main>
    
    <ReactCounter client:load />
  </body>
</html>

<style>
  main { max-width: 800px; }
</style>

クライアントディレクティブ(アイランド)

ディレクティブ動作
client:load直ちにハイドレーションします
client:idleアイドル状態になったときにハイドレーションします
client:visible表示されたときにハイドレーションします
client:mediaメディアクエリに一致したときにハイドレーションします
client:onlySSR をスキップし、クライアントのみで実行します

コンテンツコレクション

// 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);
---

主な機能

  • デフォルトでは JS ゼロ(HTML を配信)
  • React、Vue、Svelte を組み合わせて使用可能
  • 型安全性を備えたコンテンツコレクション
  • Markdown/MDX の組み込みサポート

本番環境への対応

セキュリティ設定
// 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"
/>


<ReactWidget client:visible />


<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
モニタリング指標
指標目標
Lighthouse スコア> 95
First Contentful Paint< 1s
Time to Interactive< 1.5s
Total Blocking Time< 50ms
バンドルサイズ(JS)< 50KB
チェックリスト
  • ミドルウェアにセキュリティヘッダーを設定
  • SSR で checkOrigin を有効化
  • Zod スキーマを使用したコンテンツコレクション
  • astro:assets による画像最適化
  • 遅延ハイドレーション(client:visible/idle)
  • View Transitions を有効化
  • 404/500 エラーページ
  • サイトマップの生成
  • アセットの圧縮
  • Playwright による E2E テスト
  • パイプラインに Lighthouse CI を導入

このスキルを使用しない場合

このスキルは Astro(コンテンツ重視、アイランドアーキテクチャ)向けです。次の場合は使用しないでください。

  • Next.js(React メタフレームワーク):代わりに nextjs-app-router スキルを使用してください
  • Nuxt(Vue メタフレームワーク):代わりに nuxt3 スキルを使用してください
  • SvelteKit(Svelte メタフレームワーク):代わりに sveltekit スキルを使用してください
  • Remix(React メタフレームワーク):代わりに remix スキルを使用してください
  • 純粋な React アプリケーション:代わりに frontend-react スキルを使用してください
  • 純粋な Vue アプリケーション:代わりに frontend-vue スキルを使用してください
  • 純粋な Svelte アプリケーション:代わりに frontend-svelte スキルを使用してください
  • Gatsby:Astro は最新の代替手段ですが、移行方法は異なります

アンチパターン

アンチパターン問題点正しい方法
どこでも client:load を使用するJS ゼロの理念に反し、バンドルが大きくなりますハイドレーションを遅らせるには client:idle または client:visible を使用します
コンテンツコレクションを使用しないコンテンツが検証されず、型安全性もありませんsrc/content/config.ts でコレクションを定義します
フレームワークを不必要に混在させるバンドルサイズと複雑さが増しますプロジェクトごとにフレームワークを1つ使用するか、Astro コンポーネントを使用します
画像最適化を無視するパフォーマンスが低下し、アセットが大きくなりますastro:assets の README の画像 を使用します
画像に alt テキストを設定しないアクセシビリティ上の問題が生じ、SEO に悪影響があります常に意味のある alt テキストを指定します
すべてのコンテンツに client:only を使用するSSR が行われず、SEO に悪影響がありますブラウザー専用コンポーネントに限り client:only を使用します
コンポーネントにデータをハードコードする保守が困難になり、CMS と連携できませんコンテンツコレクションまたは API からの取得を使用します
ViewTransitions を使用しないナビゲーションの UX がぎこちなくなりますレイアウトに を追加します

クイックトラブルシューティング

問題考えられる原因解決策
"Cannot use import.meta.env in client"サーバー専用の環境変数にアクセスしているクライアントからアクセスできるよう、PUBLIC_ を接頭辞として付けます
アイランドがハイドレーションされないクライアントディレクティブが正しくないclient:load/client:idle/client:visible ディレクティブが設定されていることを確認します
コンテンツコレクションが見つからないスキーマが定義されていない、またはパスが正しくないsrc/content/config.ts で定義し、src/content/{collection} を確認します
画像が最適化されない<Image> の代わりに <img> を使用しているastro:assets から <Image> をインポートして使用します
"getCollection is not defined"インポート先が正しくない'astro:content' からインポートします
型エラーでビルドが失敗するコンテンツのスキーマが一致していないフロントマターが Zod スキーマと一致することを確認します
404 ページが表示されないsrc/pages/404.astro がないsrc/pages/ に 404.astro を作成します
CSS がスコープ化されないコンポーネントに <style> がないAstro コンポーネントに <style> ブロックを追加します

リファレンスドキュメント

コメント