目次15

Aulvem サイトの構成について、技術スタックの紹介ではなく「何を schema で強制し、何を捨てたか」に絞ってまとめる起点記事です。スタック自体は Astro + MDX + Content Collections の組み合わせで、特別なことはしていません。書きたいのは典型構成の上で、書き手がうっかり忘れる類の運用ルールをどうビルド時に落とすか、です。

全体像のハブとしてここに集約しました。個別判断(schema 強制 / 構造化データ一致 / sitemap lastmod の独自実装)は派生記事に分けます。

スタックの全体像 — Astro 5 + MDX + Content Collections + R2

Aulvem は Astro 5(静的出力)+ MDX + Content Collections + Tailwind 3 + Pagefind + Cloudflare R2 で動いています。動的サーバーは使わず、生成済み HTML を Cloudflare Pages から配信するだけのシンプルな構成です。

選択の軸は次の 3 つに絞りました。

  • ビルド成果物が完全に静的であること
  • 記事のメタデータを schema で縛れること
  • コアの依存数が少ないこと

Astro 5 はこの 3 つすべてに当てはまりました。部分ハイドレーションや UI フレームワーク連携などの機能は今のところ使っていません。

「Astro が良いか」は要件次第です。動的な認証・コメント・購読系を載せたいなら、Next.js のような選択肢のほうが向いているはず。

用語: Content Collections とは

Aulvem では blog(記事)と services(プロダクトページ)の 2 コレクションを定義していて、schema は 1 ファイルに集約し、ビルド時に両方を検証しています。

全体スタック — 主要パッケージ

本番依存は 8 個に絞っています。

パッケージ用途備考
astro ^5SSG コア静的出力のみ使用
@astrojs/mdx ^4MDX サポート記事は全て .mdx
@astrojs/sitemap ^3sitemap 生成lastmod は自前で差し込む
@astrojs/rss ^4RSS 生成全文 content:encoded 配信
@astrojs/tailwind ^6Tailwind 統合applyBaseStyles: false
rehype-external-links ^3外部リンクに rel 付与noopener noreferrer
rehype-mermaid ^3mermaid 図の build 時 SVG 化inline-svg strategy
tailwindcss ^3.4スタイリングv4 は様子見

dev 依存は pagefind(全文検索)、sharp(ローカル画像処理)、playwright(mermaid 図の build 時 SVG 化に必要)、typescript@types/node のみ。React も Vue も Vite プラグインも入れていません。

「とりあえず後から要るかもしれない」を前提に依存を足さない、を入口の方針にしました。未使用の依存はビルド時間とセキュリティアラート対応のノイズに効いてくるので、入れる前に使うシーンを言語化できるかを確認しています。

譲れなかった 3 つの仕組み

1. ルールはスキーマで強制する

運用ルールを README に書かず、frontmatter schema で強制しています。書き手が忘れてもビルド時に落ちる構造にしておきたかった、というのが理由です。

たとえば category: reviews の記事は ASP 規約上「広告であること」を明示する必要があるので、affiliate: true を必須にしました。これを README に書くだけの取り決めにせず、Zod の .refine()category === "reviews"affiliate === true が常に等しいことを強制しています。片方だけ書き換えるとビルドが落ちる形にしておけば、書き手の記憶に頼らずに済みます。

同じ発想を howto / faq の構造化データにも適用していますが、これは派生記事で扱う予定です。

2. 構造化データと本文を一致させる

JSON-LD(howto / faq)の中身は frontmatter から自動生成しつつ、本文にも同じ内容を必ず書きます。理由は Google の structured data mismatch ペナルティを避けるためで、「JSON-LD だけ豪華にして本文に書かない」のは検索品質ガイドライン違反です。検出されると構造化データの表示資格を失います。

Aulvem では「frontmatter と本文の両側に同じ内容を書く」をルールにして、片側だけ更新する事故を最初から起こさない形にしています。

3. sitemap の lastmod は frontmatter から自分で読む

Astro 公式の sitemap integration は MDX frontmatter の updatedDate を読みません。そこで Aulvem では自前のロジックで全 MDX を走査し、updatedDate ?? pubDatelastmod として sitemap に流しています。

これをやらないと sitemap の lastmod がビルド時刻に固定され、「全記事が毎ビルド更新されている」というノイズシグナルを検索エンジンに送ることになります。AI 検索の鮮度判定にも lastmod は使われる(要出典: Google / Bing 双方のドキュメントで明示)ので、ここは雑にやらないようにしています。

同時に、noindex, follow を返している分割ページは sitemap から除外しています。「noindex なものを sitemap で送る」のは矛盾シグナルなので、両方を一緒に正しく扱う必要があります。

3 つはどうつながっているか

flowchart LR
  FM[frontmatter] --> Zod
  FM --> Sitemap
  FM --> Sync
  Body[本文 MDX] --> Sync

  Zod["Zod schema 検証<br/>category ⇔ affiliate"] --> Build
  Sitemap["sitemap lastmod 差し込み<br/>updatedDate を読む"] --> Build
  Sync["frontmatter ↔ 本文<br/>の整合"] --> Build

  Build[Astro build]

  Build --> HTML[HTML + JSON-LD + SVG]
  Build --> SM[sitemap.xml]

frontmatter と本文という 2 つの入力から別経路で 3 つの仕組みが走ります。frontmatter は Zod schema 検証と sitemap lastmod 差し込みを通り、frontmatter と本文の両方が「同じ内容が両側に書かれているか」の整合チェックに渡ります。

すべての結果が Astro build で束ねられ、HTML + JSON-LD + sitemap.xml として公開されます。どれか 1 つが落ちるとビルドが失敗するので、片方だけ整えても公開できません。

運用フローは単一情報源に集約する

記事追加・プロダクト追加・retire(記事削除)といった運用フローは、1 つの doc を単一情報源にしています。そこから雛形生成と整合性チェックのスクリプトを連動させる構成にして、毎回同じ手順を踏む前提にしました。これで「やり方が毎回バラつく」揺らぎは、執筆時の温度感以外の部分でほぼ吸収できると思います。

なお updatedDate を「冒頭注記の追加だけでは更新しない」「実質改稿時のみ更新する」というルールも明文化しました。検索エンジンと AI が dateModified を鮮度シグナルとして引くので、ここを甘く運用するとサイト全体の権威性が下がります。基準はあらかじめ決めておきます。

よくある質問

なぜ Astro 5 を選んだのか?

「Markdown 中心 / 完全静的出力 / frontmatter を型で縛れる / コア依存が少ない」の 4 条件で絞ったとき、Astro 5 が一番うまく当てはまりました。Content Collections と MDX が標準で入っており、追加プラグインを積まずに型付きコンテンツを扱える点で選んでいます。

ヘッドレス CMS(Sanity, Contentful 等)を入れないのはなぜ?

投稿頻度が週 1〜数本で、編集者が 1 名のため CMS の管理画面より MDX + git で書くほうが速いです。CMS を入れた瞬間に下書きの所在・スキーマ変更・API 認証など運用コストが乗ります。読者 1 万人規模を超えるか、複数編集者が入った時点で再評価する想定です。

全文検索はどう実装している?

Pagefind を astro build の後に走らせて、ビルド時に静的な検索インデックス(バイナリ化された辞書)を生成しています。フロントでは数十 KB の WASM ローダーから引いていて、サーバーレス関数も外部 API も使いません。

i18n は Astro 標準の機能をそのまま使っている?

ルーティングは Astro 5 の i18n(defaultLocale: en / prefixDefaultLocale: false)をそのまま使っています。一方で hreflang と sitemap の lastmod は MDX frontmatter から独自に取得する実装にしました。詳細は派生記事で扱います。

まとめ

全体像はここまでです。実際に作って回してみると、スタックを何で組むかより、frontmatter を schema でどこまで縛るかを決めるほうが、後の運用には効きました。