publishing-astro-websites について
Astroでコンテンツ中心の静的サイトを構築するためのスキルです。コンテンツ管理、検索、ドキュメント、多言語対応、デプロイの手法を扱います。
主な機能
- 01Astroの構成とレンダリング方式
- 02Markdownコレクションとローダー
- 03コードハイライトと図表の統合
- 04PagefindまたはFuse.jsによる検索
- 05ドキュメント、多言語、ページ設計
- 06ビルド、デプロイ、テストの指針
活用シーン
- 01ブログやドキュメントサイトの構築
- 02静的サイトへの検索機能の追加
- 03多言語または複数バージョンの文書公開
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spillwavesolutions/ publishing-astro-websites-agentic-skill --skill publishing-astro-websitesインストール後、Claude Code で/publishing-astro-websitesを入力して読み込まれたか確認します。
同梱ファイルreferences/deployment-platforms.mdreferences/markdown-deep-dive.md
詳細
Astro ウェブサイトの公開
Astro のランタイム不要の SSG アプローチ、部分的なハイドレーション、豊富な Markdown サポートを活用して、高速でコンテンツ主導型の静的ウェブサイトを構築します。
目次
- クイックスタート
- 使用しないケース
- プロジェクト構成
- SSG と SSR とハイブリッド
- コンテンツコレクション — レガシー、Content Layer API、カスタムローダー
- 構文ハイライト — Shiki、トランスフォーマー、Expressive Code
- 図表の統合 — Mermaid、PlantUML、ダークモードのテーマ設定
- クライアントサイド検索 — Pagefind(制御、重み付け)、Fuse.js
- バージョン管理されたドキュメント — Starlight、複数バージョン
- 国際化 — ルーティング、フォールバック
- 一般的なパターン — ページネーション、タグ、RSS、フォーム
- パフォーマンスのベストプラクティス — プリフェッチ、クリティカル CSS
- デプロイ — Firebase URL 設定、GitHub Pages
- デプロイ前チェックリスト
- テストと品質 — Vitest、Playwright、リンクチェック
- トラブルシューティング
クイックスタート
# Create new project (use Blog template for Markdown sites)
npm create astro@latest
# Development
npm run dev # Local server at http://localhost:4321
npm run build # Generate static files in dist/
npm run preview # Preview production build
使用しないケース
このスキルでは**静的サイト生成(SSG)**に重点を置いています。次のような場合は、別のアプローチを検討してください。
- リアルタイムデータアプリケーション - データベース接続を使用する SSR モードを使います
- ユーザー認証フロー - サーバーサイドでのセッション処理が必要です
- 在庫が動的に変化する E コマース - ハイブリッドモードまたは完全な SSR を使います
- シングルページアプリケーション(SPA) - React/Vue のフレームワークを直接検討します
ハイブリッド SSG+SSR パターンについては、Astro のアダプターのドキュメントを参照してください。
プロジェクト構成
src/
components/ # Astro, React, Vue, Svelte components
content/ # Content Collections (Markdown/MDX)
config.ts # Collection schemas
docs/ # Example collection
layouts/ # Page wrappers with slots
pages/ # File-based routing
public/ # Static assets (images, fonts, favicons)
astro.config.mjs # Framework configuration
SSG と SSR とハイブリッド
| モード | ページのレンダリング時点 | 用途 |
|---|---|---|
| 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)
})
})
};
Content Layer API(Astro 5.0+)
glob() ローダーを使う新しいパターンです。大規模サイトではビルドが最大 75% 高速になります。
// src/content.config.ts (note: different filename)
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(), // Validates image exists
author: reference('authors'), // Cross-collection reference
})
});
export const collections = { blog };
高度なスキーマパターン
schema: ({ image }) => z.object({
cover: image(), // Validates image in src/
category: z.enum(['tech', 'news']),
author: reference('authors'), // Cross-collection ref
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 release notes
}
});
}
}
};
}
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; // highlighted
const c = 3;
console.log(a + b + c); // highlighted
```
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 config
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="Flow diagram">
</picture>
- インライン SVG - CSS で SVG クラスを対象にします(リスク:スタイルの衝突)。
.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">Page Title</h1>
<p data-pagefind-weight="10">Important intro text</p>
<nav data-pagefind-ignore>
<a href="/related">Related Posts</a>
</nav>
</main>
| 属性 | 目的 |
|---|---|
data-pagefind-body | インデックス作成の対象をこの要素のみに限定 |
data-pagefind-ignore | 要素をインデックスから除外 |
data-pagefind-meta="key" | メタデータフィールドを定義 |
data-pagefind-weight="10" | 関連性を高める(デフォルト:1) |
Pagefind と 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="Search..." />
<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');
// Clear previous results safely
resultsEl.replaceChildren();
// Build results using safe DOM methods
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)をご検討ください。
バージョン管理されたドキュメント
Starlight(ドキュメントサイトにおすすめ)
Astro 上に構築された、ドキュメント専用フレームワークです。
npm create astro@latest -- --template starlight
主な機能:組み込み検索(Pagefind)、i18n、サイドバーナビゲーション、ダークモード、コンポーネントのオーバーライド。
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
export default defineConfig({
integrations: [
starlight({
title: 'My Docs',
sidebar: [
{ label: 'Guides', autogenerate: { directory: 'guides' } },
{ label: 'Reference', autogenerate: { directory: 'reference' } },
],
}),
],
});
複数バージョンのドキュメント構成パターン
フォルダベースの構成を使用します。
src/content/docs/
v1/
getting-started.md
api-reference.md
v2/
getting-started.md
api-reference.md
new-feature.md
Starlight でバージョン管理するには、starlight-utils プラグインを使用します。
npm install starlight-utils
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightUtils from 'starlight-utils';
export default defineConfig({
integrations: [
starlight({
plugins: [starlightUtils({ multiSidebar: { switcherStyle: 'dropdown' } })],
sidebar: [
{ label: 'v2', items: [{ label: 'Guides', autogenerate: { directory: 'v2' } }] },
{ label: 'v1', items: [{ label: 'Guides', autogenerate: { directory: 'v1' } }] },
],
}),
],
});
国際化(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, // Fallback to English
isFallback: !localizedDoc
}
});
}
}
return paths;
}
const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---
{isFallback && (
<div class="translation-notice">
This page is not yet available in your language.
</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);
return Array.from({ length: totalPages }, (_, i) => ({
params: { page: String(i + 1) },
}));
}
const { page } = Astro.params;
const pageNum = parseInt(page);
const allPosts = await getCollection("blog");
const sortedPosts = allPosts.sort((a, b) =>
b.data.pubDate.getTime() - a.data.pubDate.getTime()
);
const start = (pageNum - 1) * POSTS_PER_PAGE;
const posts = sortedPosts.slice(start, start + POSTS_PER_PAGE);
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
---
{posts.map(post => <article>{post.data.title}</article>)}
<nav>
{pageNum > 1 && <a href={`/blog/page/${pageNum - 1}`}>← Previous</a>}
<span>Page {pageNum} of {totalPages}</span>
{pageNum < totalPages && <a href={`/blog/page/${pageNum + 1}`}>Next →</a>}
</nav>
タグ/カテゴリーのアーカイブ
タグごとにページを生成します。
---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const tags = new Set();
allPosts.forEach(post => post.data.tags?.forEach(tag => tags.add(tag)));
return Array.from(tags).map(tag => ({
params: { tag },
props: { tag },
}));
}
const { tag } = Astro.props;
const allPosts = await getCollection("blog");
const postsWithTag = allPosts.filter(post => post.data.tags?.includes(tag));
---
<h1>Posts tagged: {tag}</h1>
{postsWithTag.map(post => <a href={`/blog/${post.slug}`}>{post.data.title}</a>)}
RSS フィード
// src/pages/rss.xml.js
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
export async function GET(context) {
const blog = await getCollection("blog");
return rss({
title: "My Blog",
description: "A blog about Astro",
site: context.site,
items: blog.map(post => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDate,
link: `/blog/${post.slug}/`,
})),
});
}
静的フォーム
SSG サイトでは、サードパーティのフォームハンドラーを使用します。
Formspree(最も簡単):
<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
Netlify Forms:
<form name="contact" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact" />
<input type="text" name="name" required />
<button type="submit">Send</button>
</form>
JSON-LD 構造化データ
SEO 向けにリッチスニペットを追加します。
---
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: entry.data.title,
description: entry.data.description,
datePublished: entry.data.pubDate?.toISOString(),
author: { "@type": "Person", name: entry.data.author },
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
ダークモードの切り替え
<button id="theme-toggle">🌙</button>
<script>
const toggle = document.getElementById("theme-toggle");
const html = document.documentElement;
// Load saved preference or detect system preference
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
html.classList.add("dark");
}
toggle?.addEventListener("click", () => {
html.classList.toggle("dark");
localStorage.setItem("theme", html.classList.contains("dark") ? "dark" : "light");
});
</script>
Tailwind では、設定で darkMode: "class" を有効にします。
パフォーマンスのベストプラクティス
-
部分的なハイドレーション:必要な箇所でのみ
client:*ディレクティブを使用しますclient:load- 直ちにハイドレーションclient:idle- ブラウザーがアイドル状態になったときにハイドレーションclient:visible- ビューポートに入ったときにハイドレーションclient:media- メディアクエリが一致したときにハイドレーションclient:only="react"- サーバーレンダリングをスキップし、クライアント側のみで処理
-
画像の最適化:Astro の
<Image />コンポーネントを使用します -
可能な限り静的な状態を維持:アイランドアーキテクチャにより、ほとんどのコンテンツは静的 HTML のままです
-
アセットのフィンガープリント:本番ビルドで自動的に設定されます
-
プリフェッチ:ユーザーがクリックする前にリンクを自動読み込みします
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true, // Prefetch all links
defaultStrategy: 'viewport' // When links enter viewport
}
});
オプション:'tap'(ホバー/フォーカス時)、'viewport'(表示されたとき)、'load'(ページ読み込み時)。
- クリティカル CSS:astro-critters でファーストビューの CSS をインライン化します
npm install astro-critters
import critters from 'astro-critters';
export default defineConfig({
integrations: [critters()]
});
クリティカル CSS を抽出してインライン化し、残りの CSS の読み込みを遅らせることで、初回描画を高速化します。
デプロイ
デプロイのワークフロー
- ビルド:
npm run buildを実行し、dist/の出力を確認します - プレビュー:localhost:4321 で
npm run previewを使ってテストします - 設定:astro.config.mjs で
site、base、trailingSlashを設定します - プラットフォームのセットアップ:ホスティングを初期化します(例:
firebase init hosting) - デプロイ:プラットフォームにプッシュします(
firebase deploy、vercel、または git push) - 確認:公開 URL を確認し、404 ページをテストして、アセットが読み込まれることを検証します
クイックデプロイコマンド
# Build for production
npm run build
# Preview before deploy
npm run preview
プラットフォーム固有
Netlify/Vercel/Cloudflare Pages: Git リポジトリを接続すると、プッシュ時に自動デプロイされます。
GitHub Pages:
// astro.config.mjs
export default defineConfig({
site: 'https://username.github.io',
base: '/repo-name'
});
Firebase Hosting:
npm install -g firebase-tools
firebase login
firebase init hosting # Set public to 'dist'
npm run build
firebase deploy
firebase.json(推奨設定):
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*"],
"cleanUrls": true,
"trailingSlash": false,
"headers": [
{
"source": "/_astro/**",
"headers": [{"key": "Cache-Control", "value": "public, max-age=31536000, immutable"}]
}
]
}
}
リダイレクトループを防ぐため、Astro の設定を合わせます:
// astro.config.mjs - match Firebase settings
export default defineConfig({
trailingSlash: 'never', // Must match Firebase trailingSlash: false
build: {
format: 'directory' // Default - generates /about/index.html
}
});
| Firebase の設定 | Astro の設定 | 結果 |
|---|---|---|
trailingSlash: false | trailingSlash: 'never' | /about(スラッシュなし) |
trailingSlash: true | trailingSlash: 'always' | /about/(スラッシュあり) |
| 不一致 | 不一致 | リダイレクトループが発生します! |
よくあるデプロイ時の注意点
| 問題 | 解決策 |
|---|---|
| 末尾のスラッシュに関する問題 | trailingSlash: 'always' または 'never' を設定します |
| サブパスでアセットが読み込まれない | astro.config.mjs で base を設定します |
| 404 が機能しない | カスタム 404.astro ページを作成します |
| デプロイ時にビルドが失敗する | Node のバージョンがローカルと一致しているか確認します |
デプロイ前チェックリスト
デプロイする前に、次の項目を確認してください。
-
npm run buildがエラーなしで完了する -
npm run previewで localhost:4321 にサイトが正しく表示される - すべての Content Collection スキーマが検証される(
astro check) - 画像に
<Image />コンポーネントを使うか、public/に配置している - すべてのページに SEO メタデータ(title、description、og:*)がある
- 404.astro ページが存在し、正しく表示される
- サブディレクトリにデプロイする場合、
baseパスが設定されている - デプロイ先プラットフォームで環境変数が設定されている
-
trailingSlashの設定がホスティングプラットフォームの要件に合っている - RSS フィードが機能する(
/rss.xml) - サイトマップが生成される(
/sitemap-index.xml) - Lighthouse スコアが 90 を超えている
- コンポーネントテストが成功する(
npm run test) - E2E テストが成功する(
npm run test:e2e) - リンクチェッカーでリンク切れが見つからない(
npx linkinator dist)
テストと品質
静的解析
# Type checking and validation
npx astro check
# Linting (with ESLint)
npm install -D eslint
npx eslint .
# Preview production build
npm run build && npm run preview
ビルド時の検証は Content Collections によって自動的に行われ、スキーマエラーがあるとビルドは失敗します。
Vitest によるコンポーネントテスト
npm install -D vitest @vitest/ui @astrojs/testing
// vitest.config.ts
import { getViteConfig } from 'astro/config';
export default getViteConfig({
test: {
include: ['src/**/*.test.ts'],
},
});
// src/components/Button.test.ts
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Button from './Button.astro';
test('Button renders with text', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Button, {
props: { text: 'Click me' }
});
expect(result).toContain('Click me');
});
Playwright による E2E テスト
npm install -D @playwright/test
npx playwright install
// tests/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My Site/);
await expect(page.locator('h1')).toBeVisible();
});
test('navigation works', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/about"]');
await expect(page).toHaveURL(/about/);
});
// package.json
{
"scripts": {
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
リンクチェック
内部リンクが壊れていないことを検証します。
npm install -D linkinator
npx linkinator dist --recurse
CI に追加します。
# .github/workflows/links.yml
- run: npm run build
- run: npx linkinator dist --recurse --skip "^(?!http://localhost)"
.astro ファイルの構成
---
// Frontmatter: JavaScript/TypeScript runs at build time
import Layout from '../layouts/Layout.astro';
import { getCollection } from 'astro:content';
const { title } = Astro.props;
const posts = await getCollection('blog');
---
<Layout title={title}>
<h1>{title}</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
))}
</ul>
</Layout>
<style>
/* Scoped to this component */
h1 { color: navy; }
</style>
<script>
// Client-side JavaScript
console.log('Runs in browser');
</script>
ファイルベースのルーティング
| ファイル | ルート |
|---|---|
src/pages/index.astro | / |
src/pages/about.astro | /about |
src/pages/blog/index.astro | /blog |
src/pages/blog/[slug].astro | /blog/:slug(動的) |
src/pages/[...path].astro | キャッチオール |
動的ルートで SSG を使用するには、getStaticPaths() が必要です。
---
export function getStaticPaths() {
return [
{ params: { slug: 'post-1' } },
{ params: { slug: 'post-2' } }
];
}
---
SEO の基本
手動で設定する方法
---
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</head>
astro-seo(簡易設定)
npm install astro-seo
---
import { SEO } from 'astro-seo';
---
<head>
<SEO
title="Page Title"
description="Page description"
openGraph={{
basic: {
title: "OG Title",
type: "website",
image: "/og-image.png",
}
}}
twitter={{ creator: "@handle" }}
/>
</head>
メタタグ、Open Graph、Twitter Cards、canonical URL を自動的に処理します。
必須のインテグレーション
# Add integrations
npx astro add react # React components
npx astro add tailwind # Tailwind CSS
npx astro add mdx # MDX support
npx astro add sitemap # Auto-generate sitemap
# RSS feed
npm install @astrojs/rss
トラブルシューティング
「ローカルでは動くのに、デプロイすると動かない」
- ホストで環境変数が設定されているか確認します
baseパスの設定を確認します- Node のバージョンが一致していることを確認します(v18 以降を推奨)
動的ルートのページが見つからない
getStaticPaths()が必要なパスをすべて返しているか確認します- params に誤字がないか確認します
Content Collection のスキーマエラー
astro checkを実行して検証の詳細を確認します- frontmatter が Zod スキーマと正確に一致していることを確認します
アセットが読み込まれない
- 処理対象のアセットには
importを使用します - 未処理の静的ファイルには
public/を使用します
参考資料
特定のトピックについて詳しくは、以下を参照してください。
references/markdown-deep-dive.md- 高度な Markdown/MDX パターンreferences/deployment-platforms.md- プラットフォーム固有のデプロイの詳細