mcpField
P
publishing-astro-websites

publishing-astro-websites

github.com/spillwavesolutions開発3161 回インストール43 週間前に更新
リポジトリ

publishing-astro-websites について

Astroでコンテンツ中心の静的サイトを構築するためのスキルです。コンテンツ管理、検索、ドキュメント、多言語対応、デプロイの手法を扱います。

主な機能

  1. 01Astroの構成とレンダリング方式
  2. 02Markdownコレクションとローダー
  3. 03コードハイライトと図表の統合
  4. 04PagefindまたはFuse.jsによる検索
  5. 05ドキュメント、多言語、ページ設計
  6. 06ビルド、デプロイ、テストの指針

活用シーン

  1. 01ブログやドキュメントサイトの構築
  2. 02静的サイトへの検索機能の追加
  3. 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 サポートを活用して、高速でコンテンツ主導型の静的ウェブサイトを構築します。

目次

クイックスタート

# 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 が必要です)。

ダークモードのテーマ設定方法:

  1. CSS 変数 - 実行時にブラウザーで色を解決します。
// mermaid config
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="Flow diagram">
</picture>
  1. インライン 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 の比較
機能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="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" を有効にします。

パフォーマンスのベストプラクティス

  1. 部分的なハイドレーション:必要な箇所でのみ client:* ディレクティブを使用します

    • client:load - 直ちにハイドレーション
    • client:idle - ブラウザーがアイドル状態になったときにハイドレーション
    • client:visible - ビューポートに入ったときにハイドレーション
    • client:media - メディアクエリが一致したときにハイドレーション
    • client:only="react" - サーバーレンダリングをスキップし、クライアント側のみで処理
  2. 画像の最適化:Astro の <Image /> コンポーネントを使用します

  3. 可能な限り静的な状態を維持:アイランドアーキテクチャにより、ほとんどのコンテンツは静的 HTML のままです

  4. アセットのフィンガープリント:本番ビルドで自動的に設定されます

  5. プリフェッチ:ユーザーがクリックする前にリンクを自動読み込みします

// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,           // Prefetch all links
    defaultStrategy: 'viewport'  // When links enter viewport
  }
});

オプション:'tap'(ホバー/フォーカス時)、'viewport'(表示されたとき)、'load'(ページ読み込み時)。

  1. クリティカル CSS:astro-critters でファーストビューの CSS をインライン化します
npm install astro-critters
import critters from 'astro-critters';

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

クリティカル CSS を抽出してインライン化し、残りの CSS の読み込みを遅らせることで、初回描画を高速化します。

デプロイ

デプロイのワークフロー
  1. ビルド:npm run build を実行し、dist/ の出力を確認します
  2. プレビュー:localhost:4321 で npm run preview を使ってテストします
  3. 設定:astro.config.mjs で site、base、trailingSlash を設定します
  4. プラットフォームのセットアップ:ホスティングを初期化します(例:firebase init hosting)
  5. デプロイ:プラットフォームにプッシュします(firebase deploy、vercel、または git push)
  6. 確認:公開 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: falsetrailingSlash: 'never'/about(スラッシュなし)
trailingSlash: truetrailingSlash: '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 - プラットフォーム固有のデプロイの詳細

主なリソース

コメント