astro について
Astro 開発者向けのリファレンススキルです。コンポーネント、コンテンツコレクション、アイランド設計に加え、本番運用の指針も扱います。
主な機能
- 01Astro コンポーネントとクライアントディレクティブ
- 02型安全なコンテンツコレクション
- 03Markdown と MDX のサポート
- 04パフォーマンスとセキュリティの指針
- 05一般的な問題のトラブルシューティング
活用シーン
- 01コンテンツ中心のサイト構築
- 02アイランドの遅延ハイドレーション設定
- 03ブログのコンテンツスキーマ検証
- 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:only | SSR をスキップし、クライアントのみで実行します |
コンテンツコレクション
// 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 の |
| 画像に 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> ブロックを追加します |
リファレンスドキュメント
コメント
Markdown 対応