tech-book-labs
ガイド(外部統合) · 2 セクション統合

Astro 実装テクニック

Astro で静的サイトを組む際の実装ノート。Content Collection のリレーション設計、View Transitions(client router)による画面遷移までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · astro 6.3.x

Astro Content Collection で関連記事 / hub ページを組む

Astro Content Collection で複数 collection を関連付け、hub ページや関連記事 aside を自動生成するパターン — glob loader / reference / cross-link / category タクソノミー / カテゴリ別 RSS / pagefind 検索の組み込み方を実装メモ化。

検証日: 2026-05-10

使用バージョン: astro@6.3.x

対象: Content Collection の最小構成は通った、wiki / docs サイトに発展させたい人

入門は別途(MDX 記事を 1 collection で並べる手前まで)。本稿は 複数 collection を関連付けて hub / 関連 aside / search を自動生成する 中級パターン。

1. 複数 collection と reference

src/content.config.ts で複数の collection を定義し、片方が他方を reference で参照する:

import { defineCollection, reference, z } from "astro:content";
import { glob } from "astro/loaders";

const articles = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/articles" }),
  schema: z.object({
    title: z.string(),
    summary: z.string(),
    category: z.string(),
    // 別 collection の id を reference で持つ
    relatedArticles: z.array(reference("articles")).default([]),
    author: reference("authors").optional(),
  }),
});

const authors = defineCollection({
  loader: glob({ pattern: "**/*.json", base: "./src/content/authors" }),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
  }),
});

export const collections = { articles, authors };

ポイント:

  • reference("articles") は collection 名の文字列(articles collection の id のいずれかを許容)
  • schema の zod が拘束するので、存在しない id を frontmatter に書くと build エラー
  • 配列で複数参照 = 関連記事リスト

getEntries(refs) / getEntry(ref) で reference の実体を遅延 fetch。getStaticPaths の中で呼ぶ:

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

const all = await getCollection("articles");
const entry = all.find((a) => a.id === Astro.params.slug)!;

// reference を追跡して実体を取得
const relatedEntries = await getEntries(entry.data.relatedArticles);
const author = entry.data.author ? await getEntry(entry.data.author) : null;
---

<aside>
  <h3>関連記事</h3>
  <ul>
    {relatedEntries.map((r) => <li><a href={`/articles/${r.id}`}>{r.data.title}</a></li>)}
  </ul>
</aside>

getEntries / getEntry で reference の実体を遅延 fetch。手動で全件 load + filter するより型安全 + パフォーマンス良い。

reference は build 時に id 整合性をチェック、runtime に getEntries で遅延解決 (クリックで拡大)

3. category タクソノミーの自動 hub

frontmatter に category を入れて、category 一覧ページを getStaticPaths で自動生成:

// src/lib/categories.ts
export const CATEGORIES = {
  forms: { label: "フォーム", description: "..." },
  visualization: { label: "可視化", description: "..." },
  // ...
};
---
// src/pages/categories/[id].astro
import { getCollection } from "astro:content";
import { CATEGORIES } from "@/lib/categories";

export async function getStaticPaths() {
  return Object.keys(CATEGORIES).map((id) => ({ params: { id } }));
}

const { id } = Astro.params;
const meta = CATEGORIES[id as keyof typeof CATEGORIES];
const articles = (await getCollection("articles", ({ data }) => data.category === id))
  .sort((a, b) => a.data.title.localeCompare(b.data.title));
---

<h1>{meta.label}</h1>
<p>{meta.description}</p>
<ul>
  {articles.map((a) => (
    <li><a href={`/articles/${a.id}`}>{a.data.title}</a></li>
  ))}
</ul>

これで /categories/forms / /categories/visualization などのページが自動生成される。

4. 同じ category 内の他記事を aside で表示

記事ページの [...slug].astro で:

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

const all = await getCollection("articles", ({ data }) => !data.draft);
const { entry } = Astro.props;

const sameCat = all
  .filter((a) => a.data.category === entry.data.category && a.id !== entry.id)
  .sort((a, b) => (a.data.library ?? "").localeCompare(b.data.library ?? ""));
---

<aside>
  <h3>同じカテゴリ</h3>
  <ul>
    {sameCat.map((a) => <li><a href={`/articles/${a.id}`}>{a.data.title}</a></li>)}
  </ul>
</aside>

reference を毎記事で書くのが手間な場合、category の自動グループ化で代替。explicit relation が必要な深掘り記事は relatedArticles で個別指定、という二段使いが現実的。

5. カテゴリ別 RSS を @astrojs/rss で生成

src/pages/<category>/rss.xml.ts を作って rss() に items を流す。getStaticPaths で category 別の endpoint を生成可能:

// src/pages/feed/[category].xml.ts
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { CATEGORIES } from "@/lib/categories";
import type { APIContext } from "astro";

export async function getStaticPaths() {
  return Object.keys(CATEGORIES).map((id) => ({ params: { category: id } }));
}

export async function GET(context: APIContext) {
  const { category } = context.params;
  const items = (await getCollection("articles", ({ data }) => data.category === category))
    .sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());

  return rss({
    title: `tech-book-labs / ${CATEGORIES[category!].label}`,
    description: CATEGORIES[category!].description,
    site: context.site!,
    items: items.map((i) => ({
      title: i.data.title,
      pubDate: i.data.publishedAt,
      description: i.data.summary,
      link: `/articles/${i.id}`,
    })),
  });
}

/feed/forms.xml / /feed/visualization.xml のような category 別フィード が生成される。

6. pagefind による静的全文検索

build 後の dist/ を pagefind が走査して、JS で動く静的検索インデックスを作る:

{
  "scripts": {
    "build": "astro build && pagefind --site dist"
  },
  "devDependencies": {
    "pagefind": "^1.x"
  }
}

検索 UI(クライアント側、~10KB):

<!-- src/components/Search.astro -->
<input id="q" placeholder="検索..." />
<div id="results"></div>

<script>
  import { Pagefind } from "/pagefind/pagefind.js";
  const pagefind = await Pagefind();
  document.getElementById("q")!.addEventListener("input", async (e) => {
    const q = (e.target as HTMLInputElement).value;
    if (q.length < 2) return;
    const search = await pagefind.search(q);
    const data = await Promise.all(search.results.slice(0, 10).map((r) => r.data()));
    document.getElementById("results")!.innerHTML = data.map((d) =>
      `<a href="${d.url}">${d.meta.title}</a>`
    ).join("");
  });
</script>

ポイント:

  • build 時にインデックスを作る(runtime 不要、CDN だけで動く)
  • bundle 影響は数十 KB(language-pack を絞れば軽い)
  • <body data-pagefind-body> を main 要素に付けて検索範囲を絞る

7. 双方向リンクの自動化(graph 抽出)

記事の本文中に [[other-article-id]] のような表記を許して、build 時にバックリンクを自動生成する手法もある。プラグイン例:

// astro.config.mjs
import { remarkWikilink } from "remark-wiki-link";

export default defineConfig({
  markdown: {
    remarkPlugins: [
      [remarkWikilink, { hrefTemplate: (slug) => `/articles/${slug}` }],
    ],
  },
});

build 後に MDX → MD ASTを解析 → 全 wikilink を JSON ファイルに集約すれば、各記事の 「ここに言及している記事」 aside を作れる。Obsidian 風 wiki の感覚。

つまずいたポイント

  • reference の id 文字列が glob loader と一致しない:loader の pattern から拡張子を除いた path が id。frontmatter 側でその文字列を書く
  • 同じ collection を循環参照すると無限ループ:reference を辿る depth に上限を設ける(2 hop で十分なケースが多い)
  • relatedArticles を全記事に手で書くのが大変:tag / category の 自動グルーピングで代替 + 手動 reference は本当に強い関連だけ
  • pagefind が docs を index しない:<body data-pagefind-body><main> に移すか、data-pagefind-ignore で除外指定
  • build 時間が増える:pagefind は 1000 記事で +10 秒程度。CI でキャッシュすると軽い

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784814401093

Effective TypeScript(第2版) : 型システムの力を最大限に引き出す83項目

Dan Vanderkam/今村 謙士 · オライリージャパン · 2025年 · ¥4,620

急速に普及が進んでいるTypeScriptの実用書! TypeScriptの実用書。TypeScript…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784297137397

かんたん TypeScript

HIRO · 技術評論社 · 2023年

本書は、「広く・正しく・新しく」をコンセプトにTypeScriptでプログラミングをはじめるにあたって基本的なことはすべて学習できる内容となっています。また、イラストによる図解方式で概念をやさしく解説している…

詳細を tech-book.net で見る
tech-book.net /books/9784297126353

ゼロからわかる TypeScript入門

WINGSプロジェクト 齊藤新三/山田 祥寛
詳細を tech-book.net で見る
tech-book.net /books/9784297127473

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

鈴木 僚太 · 技術評論社 · 2022年

TypeScriptは、JavaScriptに静的型付けの機能を加えたオープンソースのプログラミング言語です。本書では、根幹となるJavaScr…

詳細を tech-book.net で見る
セクション · 2026-05-10 · astro 6.3.x

Astro View Transitions で MPA に SPA 風遷移を載せる

Astro 6 の <ClientRouter /> による View Transitions API 統合 — transition:name で要素を繋ぎ、transition:persist で state を維持、transition:animate で挙動を上書き、astro:before-swap でナビゲーションフックを取る実装パターンをまとめた実装メモ。

検証日: 2026-05-10

使用バージョン: astro@6.3.x

対象: 静的 / MPA な Astro サイトに、ページ遷移のチラつきや scroll リセットを防ぐ「SPA 風」体験を載せたい人

Astro 6 の <ClientRouter />(View Transitions API 統合)で、MPA(Multi-Page App)サイトに SPA 風のページ遷移を載せるパターン。transition:name で要素を繋ぎ、transition:persist で state を維持、astro:before-swap 等のナビゲーションフックを取る実装を整理します。

なぜ View Transitions

通常の MPA は ページ遷移時に white flash + 全要素が再描画 される。

Astro 6 の <ClientRouter /> を入れると:

  • ページ遷移を fetch + DOM 差分置換 に切替(SPA 風)
  • ブラウザの View Transitions API(or fallback)で要素を滑らかに繋ぐ
  • 共通要素(header / footer)を 再描画せずに維持
  • scroll 位置や music player の state も保持できる

「重 SPA を作らずに UX だけ SPA 化」できるのが Astro の強み。

ナビゲーションのライフサイクル

<ClientRouter /> を入れると、internal link クリック時に以下の流れに切り替わる。各 phase で発火する astro:* イベントを hook して、ローディング表示や scroll 制御を差し込める。

ClientRouter のナビゲーションライフサイクル — 4 つのフックで割り込める (クリックで拡大)

実用ヒント:

  • astro:before-preparation で「読込中…」UI を出すと、遅い遷移でも体感が悪くない
  • astro:after-swap は新 DOM 確定後に呼ばれるので、新 page の DOM クエリができる
  • astro:page-load新規訪問・遷移後の両方で呼ばれる(普通の DOMContentLoaded 代わりに使える)

1. 最小導入

BaseLayout の <head><ClientRouter /> を 1 行入れるだけで、すべての internal link がナビゲーション制御下に入る:

---
import { ClientRouter } from "astro:transitions";
---

<html>
  <head>
    <ClientRouter />
    <!-- 他の head タグ -->
  </head>
  <body>
    <slot />
  </body>
</html>

これだけで全 internal link がナビゲーション制御下に入る。追加の component を書く必要なし

2. transition:name で要素を繋ぐ

「カード一覧 → 詳細ページ」で、同じ要素が動いて見えるようにする:

{articles.map((a) => (
  <a href={`/articles/${a.id}`}>
    <article transition:name={`card-${a.id}`}>
      <h2 transition:name={`title-${a.id}`}>{a.title}</h2>
    </article>
  </a>
))}

<!-- 詳細側 src/pages/articles/[...slug].astro -->
<article transition:name={`card-${entry.id}`}>
  <h1 transition:name={`title-${entry.id}`}>{entry.data.title}</h1>
  <Content />
</article>

ポイント:

  • 同じ transition:name を別ページの 2 要素に:遷移時に View Transitions API が「同じ要素」とみなして補完
  • id をユニーク化:カード ID / 記事 slug を含める
  • 入れ子で書ける:title-{id} は別の transition、card-{id} の中の小要素として個別補完

3. transition:persist で state / DOM を保持

ヘッダの音楽プレイヤー、検索ボックスの入力中の値、SPA 内 state など、ページを跨いで生き残らせたい要素:

<header>
  <audio controls transition:persist src="/bgm.mp3" />
  <input transition:persist:props placeholder="検索..." />
</header>

ポイント:

  • transition:persist = DOM 要素自体を再利用(再 mount しない、内部 state 保持)
  • transition:persist:props = props を流用(Astro Island の React state 保持)
  • キーが必要な場合:transition:persist="player" のように名前を付ける

「ページ遷移しても再生中の audio が止まらない」「フォーム入力が消えない」のような MPA では難しかった UX が低コストで実現できる。

4. transition:animate でアニメ挙動を上書き

fade / slide / none などの組込み effect、または customTransition() で独自 keyframes も渡せる:

<aside transition:animate="slide">     <!-- スライド -->
<aside transition:animate="fade">       <!-- フェード -->
<aside transition:animate="initial">    <!-- ブラウザデフォルト -->
<aside transition:animate="none">       <!-- アニメなし -->

<!-- カスタム -->
<aside transition:animate={customAnim}>

カスタム定義:

const customAnim = {
  forwards: {
    old: { name: "fadeOut", duration: "0.2s", easing: "ease-out" },
    new: { name: "slideIn", duration: "0.3s", easing: "ease-out" },
  },
  backwards: { /* 戻る時 */ },
};

forwards(進む)/backwards(戻る)で別々に定義可。@keyframes を CSS で書いて name で参照。

5. ナビゲーションのライフサイクルイベント

クライアント側 JS で遷移を hook できる:

<script>
  document.addEventListener("astro:before-preparation", (e) => {
    console.log("遷移開始:", e.from, e.to);
  });
  document.addEventListener("astro:before-swap", (e) => {
    // DOM 差し替え直前。新しい document を加工できる
    e.newDocument.body.dataset.theme = localStorage.getItem("theme") ?? "light";
  });
  document.addEventListener("astro:after-swap", () => {
    console.log("DOM 差し替え完了、新ページ表示");
  });
  document.addEventListener("astro:page-load", () => {
    console.log("初回 + 遷移後に発火、Analytics 投げ込む等");
  });
</script>
イベントタイミング用途
astro:before-preparation遷移開始(fetch 前)loading state
astro:after-preparation新 document 取得完了加工準備
astro:before-swapDOM 差し替え直前テーマ / 動的属性の再注入
astro:after-swapDOM 差し替え直後scroll 位置調整 / 再 hydrate
astro:page-load初回 + 遷移後analytics / 計測

6. ナビゲーションを抑止 / プログラム移動

astro:before-preparationevent.preventDefault() で遷移をキャンセル、navigate() で JS 側からプログラム遷移を発火:

<!-- このリンクは ClientRouter を通さず通常 navigation -->
<a href="/external" data-astro-reload>外部風</a>

<!-- script から遷移 -->
<script>
  import { navigate } from "astro:transitions/client";
  navigate("/articles");
</script>

data-astro-reload を付けると 強制的に full reload。ログアウト後に session を完全クリアする時など。

7. 計測 / 分析タグの再発火

GA4 や GTM は通常 DOMContentLoaded で発火するが、<ClientRouter /> だと 2 ページ目以降は発火しないastro:page-load で再発火が必要:

<script>
  document.addEventListener("astro:page-load", () => {
    if (window.gtag) {
      window.gtag("event", "page_view", { page_path: location.pathname });
    }
  });
</script>

<head> の中で 1 回だけ書けばよい(<ClientRouter /> が頭の script を維持してくれる)。

8. fallback と feature detection

ブラウザが View Transitions API を持たない場合、Astro は fade fallback に自動切替。 特殊な処理は不要だが、要素ごとに挙動を変えたい時:

if (document.startViewTransition) {
  // 対応ブラウザ
} else {
  // fallback
}

サポート状況(2026-05 時点)= Chrome / Edge / Safari は対応、Firefox は 進行中。fallback は cross-browser でフェードのみ。

つまずいたポイント

  • <ClientRouter /> は head に置く:body に置くと初回ロードで効かない
  • transition:name のキー衝突:同一ページ内で別要素に同じ name を使うとエラー
  • input の transition:persist:props で値が戻る:Astro Island 内で controlled な input は state 保持されない、transition:persist のみだと OK
  • prefersReducedMotion 対応:OS 設定で reduced-motion を有効にしているユーザのために、@media (prefers-reduced-motion: reduce)* { animation: none !important } を入れる
  • Sentry / Analytics の 2 重発火:遷移ごとに発火するので、初回ロードでは発火しない条件分岐を入れる
  • 画像 / 動画 の re-fetch:transition:persist で audio / video を生かす、それ以外はキャッシュに任せる

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784297144944

JavaScriptによるはじめてのアルゴリズム入門

河西 朝雄
詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション

Kyran Dale/嶋田 健志/木下 哲也
詳細を tech-book.net で見る
tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784627857216

はじめてのWebデザイン&amp;プログラミング : HTML、CSS、JavaScript、PHPの基本

村上 祐治
詳細を tech-book.net で見る
tech-book.net /books/9784297138714

フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が学べる本

株式会社ICS 池田 泰延/西原 翼/松本 ゆき
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。