Astro ウェブサイトの公開
Astro のゼロランタイム SSG アプローチ、パーシャルハイドレーション、および広範な Markdown サポートを使用して、高速でコンテンツ駆動型の静的ウェブサイトを構築します。
目次
- クイックスタート
- 使用すべきでない場合
- プロジェクト構造
- SSG vs SSR vs ハイブリッド
- コンテンツコレクション — コンテンツレイヤーAPI、カスタムローダー
- シンタックスハイライト — Shiki、トランスフォーマー、Expressive Code
- 図の統合 — Mermaid、PlantUML、ダークモードテーマ
- クライアントサイド検索 — Pagefind(コントロール、重み付け)、Fuse.js
- バージョン管理ドキュメント — マルチバージョン
- 国際化 (i18n) — ルーティング、フォールバック
- 一般的なパターン — ページネーション、タグ、RSS、フォーム
- パフォーマンスのベストプラクティス — プリフェッチ、クリティカルCSS
- デプロイメント — Netlify
- デプロイ前チェックリスト
- テストと品質 — Vitest、Playwright、リンクチェック
- トラブルシューティング
クイックスタート
# 新規プロジェクトを作成(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 が必要)。
ダークモードテーマ戦略:
- CSS変数 - ブラウザが実行時に色を解決:
// mermaid 設定
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
- Picture要素 - 両方のテーマを生成し、メディアクエリで切り替え:
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="フロー図">
</picture>
- インライン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
| 機能 | Pagefind | Fuse.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);
...