Communitygithub.com

Astro サイト公開スキル

Astroフレームワークを使用した静的Webサイトの構築とデプロイに関する包括的なガイダンスを提供します。

Astro サイト公開スキル とは?

このスキルは、Astroフレームワークを使用した高速でコンテンツ駆動型の静的Webサイトの構築とデプロイに関する包括的なガイダンスを提供します。主に静的サイト生成(SSG)に焦点を当てており、Content Collections、Markdown/MDXサポート、パーシャルハイドレーション、アイランドアーキテクチャなどの主要機能をカバーします。また、シンタックスハイライト(Shiki、Expressive Code)、図の統合(Mermaid、PlantUML)、クライアントサイド検索(Pagefind、Fuse.js)、多言語対応(i18n)、バージョン管理されたドキュメント、パフォーマンスベストプラクティスなど、実践的なトピックも網羅しています。デプロイに関しては、Netlify、Vercel、GitHub Pages、GCP/Firebaseへのデプロイ手順を提供し、静的サイトのテストやトラブルシューティングに関する情報も含まれています。このスキルは、「astroサイトを作成して」、「astroをfirebaseにデプロイして」といった開発者のリクエストに応えるために設計されています。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/qalimero/qalim-portfolio/tree/HEAD/.claude/skills/publishing-astro-websites

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

プレビュー

スキル README より

Astro サイト公開スキル preview

ドキュメント

Astro ウェブサイトの公開

Astro のゼロランタイム SSG アプローチ、パーシャルハイドレーション、および広範な Markdown サポートを使用して、高速でコンテンツ駆動型の静的ウェブサイトを構築します。

目次

クイックスタート

# 新規プロジェクトを作成(MarkdownサイトにはBlogテンプレートを使用)
npm create astro@latest

# 開発
npm run dev          # ローカルサーバー: http://localhost:4321
npm run build        # dist/ に静的ファイルを生成
npm run preview      # プロダクションビルドをプレビュー

使用すべきでない場合

このスキルは静的サイト生成 (SSG) に焦点を当てています。以下の場合は他のアプローチを検討してください。

  • リアルタイムデータアプリケーション - データベース接続を伴うSSRモードを使用
  • ユーザー認証フロー - サーバーサイドのセッション処理が必要
  • 動的在庫管理のあるEコマース - ハイブリッドモードまたはフルSSRを使用
  • シングルページアプリケーション (SPA) - React/Vueフレームワークを直接検討

ハイブリッドSSG+SSRパターンについては、Astroのアダプタードキュメントを参照してください。

プロジェクト構造

src/
  components/     # Astro、React、Vue、Svelteコンポーネント
  content/        # コンテンツコレクション (Markdown/MDX)
    config.ts     # コレクションスキーマ
    docs/         # コレクションの例
  layouts/        # スロット付きページラッパー
  pages/          # ファイルベースのルーティング
public/           # 静的アセット(画像、フォント、ファビコン)
astro.config.mjs  # フレームワーク設定

SSG vs SSR vs ハイブリッド

モードページレンダリングタイミングユースケース
SSG (デフォルト)ビルド時ブログ、ドキュメント、マーケティングサイト
SSRリクエストごと動的データ、パーソナライゼーション
ハイブリッド両方の混合静的ページ + 動的エンドポイント

純粋な静的サイトの場合は、デフォルトの output: 'static' を使用します。アダプターは不要です。

コンテンツコレクション

レガシーパターン (Astro 4.x)

src/content/config.ts` でスキーマを定義:

import { defineCollection, z } from "astro:content";

export const collections = {
  docs: defineCollection({
    schema: z.object({
      title: z.string(),
      description: z.string().optional(),
      tags: z.array(z.string()).optional(),
      order: z.number().optional(),
      draft: z.boolean().default(false)
    })
  })
};

コンテンツレイヤーAPI (Astro 5.0+)

glob() ローダーを使用した新しいパターン - 大規模サイトでは最大75%高速なビルド:

// src/content.config.ts (ファイル名が異なることに注意)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: ({ image }) => z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    draft: z.boolean().default(false),
    cover: image(),  // 画像が存在することを検証
    author: reference('authors'),  // コレクション間参照
  })
});

export const collections = { blog };

高度なスキーマパターン

schema: ({ image }) => z.object({
  cover: image(),                    // src/ 内の画像を検証
  category: z.enum(['tech', 'news']),
  author: reference('authors'),      // コレクション間参照
  relatedPosts: z.array(reference('blog')).optional(),
})

カスタムローダー (リモートコンテンツ)

外部API(GitHubリリース、CMSなど)からコンテンツを取得:

// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';

export function githubReleasesLoader(repo: string): Loader {
  return {
    name: 'github-releases',
    load: async ({ store, logger }) => {
      logger.info(`Fetching releases for ${repo}`);
      const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
      const releases = await response.json();

      for (const release of releases) {
        store.set({
          id: release.tag_name,
          data: {
            version: release.tag_name,
            published_at: release.published_at,
            body: release.body  // Markdown リリースノート
          }
        });
      }
    }
  };
}

content.config.ts で登録:

import { githubReleasesLoader } from './loaders/github-releases';

const releases = defineCollection({
  loader: githubReleasesLoader('owner/repo'),
  schema: z.object({
    version: z.string(),
    published_at: z.string(),
    body: z.string(),
  })
});

コレクションをクエリしてレンダリング:

---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const docs = await getCollection("docs");
  return docs.map(doc => ({
    params: { slug: doc.slug },
    props: { doc }
  }));
}

const { doc } = Astro.props;
const { Content } = await doc.render();
---

<article>
  <h1>{doc.data.title}</h1>
  <Content />
</article>

シンタックスハイライト

基本的なShiki設定

import { defineConfig } from "astro/config";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "github-dark",
      wrap: true
    }
  }
});

デュアルライト/ダークテーマ

shikiConfig: {
  themes: {
    light: 'github-light',
    dark: 'github-dark',
  },
}

テーマを切り替えるCSSを追加:

@media (prefers-color-scheme: dark) {
  .astro-code, .astro-code span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
  }
}

行ハイライトとトランスフォーマー

```typescript {2,4}
const a = 1;
const b = 2;  // ハイライト
const c = 3;
console.log(a + b + c);  // ハイライト
```

Shikiトランスフォーマー (Astro 4.14+):

import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';

shikiConfig: {
  transformers: [transformerNotationFocus(), transformerNotationDiff()],
}

コード内で表記コメントを使用:

  • // [!code focus] - この行をフォーカス
  • // [!code ++] - 追加としてマーク (緑)
  • // [!code --] - 削除としてマーク (赤)

Expressive Code (ドキュメントにおすすめ)

コピーボタン、ファイル名、差分ハイライトを備えたリッチなコードブロック:

npm install astro-expressive-code
import expressiveCode from 'astro-expressive-code';

export default defineConfig({
  integrations: [expressiveCode()],
});

機能: コピーボタン、ファイルタブ、行マーカー、ターミナルフレーム、テキストマーカー。

図の統合

Mermaid (推奨)

Astro統合をインストール:

npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';

export default defineConfig({
  integrations: [mermaid({ theme: 'default' })]
});

Markdown で使用:

```mermaid
graph TD;
    A-->B;
    B-->C;
```

機能: クライアントサイドレンダリング、自動テーマ切り替え、オフライン対応、Playwright不要。

代替 (ビルド時静的SVG): プリレンダリングされた図には rehype-mermaid をPlaywrightと共に使用(npx playwright install --with-deps が必要)。

ダークモードテーマ戦略:

  1. CSS変数 - ブラウザが実行時に色を解決:
// mermaid 設定
mermaid.initialize({
  theme: 'base',
  themeVariables: {
    primaryColor: 'var(--diagram-primary)',
    lineColor: 'var(--diagram-line)'
  }
});
  1. Picture要素 - 両方のテーマを生成し、メディアクエリで切り替え:
<picture>
  <source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="/diagrams/flow-light.svg" alt="フロー図">
</picture>
  1. インラインSVG - CSSでSVGクラスをターゲット(リスク: スタイルの競合) - role='image' または role="presentation" を持つアクセシビリティ基準を追加:
.dark .mermaid-svg .node rect {
  fill: var(--bg-dark);
}

PlantUML

npx astro add plantuml

Markdown で使用:

```plantuml
@startuml
Alice -> Bob: Hello
Bob --> Alice: Hi!
@enduml
```

クライアントサイド検索

Pagefind (大規模サイトにおすすめ)

ビルド時にインデックスを作成するゼロ設定の静的検索:

npm install pagefind

package.json に追加:

{
  "scripts": {
    "build": "astro build && npx pagefind --site dist",
    "postbuild": "pagefind --site dist"
  }
}

コンポーネントで使用:

<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>

<div id="search"></div>
<script>
  window.addEventListener('DOMContentLoaded', () => {
    new PagefindUI({ element: '#search', showSubResults: true });
  });
</script>

機能: 外部サービス不要、オフライン動作、自動インデックス作成、小バンドル (~10KB)。

きめ細かいインデックス制御:

<!-- ヘッダー/サイドバーではなくメインコンテンツのみをインデックス -->
<main data-pagefind-body>
  <h1 data-pagefind-meta="title">ページタイトル</h1>
  <p data-pagefind-weight="10">重要なイントロテキスト</p>

  <!-- 検索スニペットから除外 -->
  <nav data-pagefind-ignore>
    <a href="/related">関連記事</a>
  </nav>
</main>
属性目的
data-pagefind-bodyこの要素のみにインデックスを制限
data-pagefind-ignore要素をインデックスから除外
data-pagefind-meta="key"メタデータフィールドを定義
data-pagefind-weight="10"関連性を向上(デフォルト: 1)

Pagefind vs Fuse.js

機能PagefindFuse.js
アーキテクチャプリビルドされたバイナリチャンクランタイムインメモリ
帯域幅低い(必要なチャンクのみロード)高い(全文インデックスをダウンロード)
スケーラビリティ10,000ページ以上500ページ未満
多言語ネイティブステミング手動設定
ユースケースグローバルサイト検索小規模リストフィルタリング

Fuse.js (軽量代替)

カスタムUIが必要な小規模サイト向け:

---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
  title: post.data.title,
  slug: post.slug,
  body: post.body.slice(0, 500)
})));
---

<input type="search" id="search" placeholder="検索..." />
<ul id="results"></ul>

<script define:vars={{ searchIndex }}>
  import Fuse from 'fuse.js';

  const fuse = new Fuse(JSON.parse(searchIndex), {
    keys: ['title', 'body'],
    threshold: 0.3
  });

  document.getElementById('search').addEventListener('input', (e) => {
    const results = fuse.search(e.target.value);
    const resultsEl = document.getElementById('results');

    // 安全に以前の結果をクリア
    resultsEl.replaceChildren();

    // 安全なDOMメソッドを使用して結果を構築
    results.forEach(r => {
      const li = document.createElement('li');
      const link = document.createElement('a');
      link.href = `/blog/${r.item.slug}`;
      link.textContent = r.item.title;
      li.appendChild(link);
      resultsEl.appendChild(li);
    });
  });
</script>

エンタープライズのニーズには、Algolia(ホスト型検索API)を検討してください。

バージョン管理ドキュメント

マルチバージョンドキュメントパターン

フォルダベースの構造を使用:

src/content/docs/
  v1/
    getting-started.md
    api-reference.md
  v2/
    getting-started.md
    api-reference.md
    new-feature.md

国際化 (i18n)

astro.config.mjs で設定:

export default defineConfig({
  i18n: {
    defaultLocale: "en",
    locales: ["en", "fr", "es"],
    routing: {
      prefixDefaultLocale: false
    }
  }
});

ロケールごとにコンテンツを構成:

src/content/docs/
  en/
    getting-started.md
  fr/
    getting-started.md

コンポーネントでロケールを検出:

---
const locale = Astro.currentLocale || 'en';
---

翻訳がない場合のフォールバック

翻訳がない場合にバナーを表示して英語コンテンツを表示:

---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";

const languages = ['en', 'es', 'fr'];
const defaultLang = 'en';

export async function getStaticPaths() {
  const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/'));
  const paths = [];

  for (const doc of englishDocs) {
    const slug = doc.id.replace(/^en\//, '');

    for (const lang of languages) {
      const localizedId = `${lang}/${slug}`;
      const localizedDoc = await getEntry('docs', localizedId);

      paths.push({
        params: { lang, slug },
        props: {
          entry: localizedDoc || doc,  // 英語にフォールバック
          isFallback: !localizedDoc
        }
      });
    }
  }
  return paths;
}

const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---

{isFallback && (
  <div class="translation-notice">
    このページはまだあなたの言語で利用できません。
  </div>
)}
<Content />

一般的なパターン

ページネーションリスト

50件以上の投稿があるサイトでは、ページ分割:

---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";

const POSTS_PER_PAGE = 10;

export async function getStaticPaths() {
  const allPosts = await getCollection("blog");
  const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
  ...

関連スキル