Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

Astro 7 でコンテンツコレクションが3箇所同時に壊れる

公開 読了時間 約4分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

Astro 7 では src/content/config.ts が LegacyContentConfigError で拒否されるため、設定は src/content.config.ts へ移し、コレクション定義には loader を与え、本文の描画は entry.render() ではなく astro:content の render(entry) に書き換える必要がある。

結論

Astro 7 のコンテンツコレクションは、設定ファイルの置き場所・loader の必須化・描画 API の3つが同時に変わっています。1つ直しても次が出ます。

src/content/config.ts   →  src/content.config.ts    置き場所
type: 'content'         →  loader: glob({ ... })    エントリの読み方
await entry.render()    →  await render(entry)      描画

最初に出るのは LegacyContentConfigError です。この文言は3つのうち1つ目しか指していません。 ファイルを移すとビルドは先へ進み、そこで2つ目が出ます。順に踏むことになるので、最初から3つまとめて直すほうが速いです。

症状

Astro を上げてビルドすると、コンテンツコレクションの設定を読む段階で落ちます。

LegacyContentConfigError

エラーは設定の中身については何も言いません。 スキーマの書き方を疑って zod の定義を読み直しても、そこには問題がありません。落ちているのは「そのパスに設定ファイルが置かれていること」自体です。

移したあとは、コレクションの定義側で止まります。さらにその先、記事ページを生成する段階で entry.render() が見つからないと言われます。3段階に分かれて出るため、直しては再ビルドを繰り返す形になります。

原因

3つとも同じ変更の一部です。コレクションが「ディレクトリを暗黙に読むもの」から、**「どこから何を読むかを明示するもの」**に変わりました。

置き場所

src/content/config.ts は使えません。src/content.config.ts に置きます。ディレクトリの中ではなく src の直下で、区切りがスラッシュからドットになります。見た目がほとんど同じなので、差分を目で追うと見落とします。

loader が必須

以前の type: 'content' の代わりに、エントリの読み方を渡します。

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

const posts = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
  schema: z.object({ /* ... */ }),
});

export const collections = { posts };

base は設定ファイルからの相対ではなく、プロジェクトルートからの相対です。ここを間違えるとエラーにはならず、エントリがゼロ件になります。ビルドは通り、一覧ページだけが空になります。この失敗の仕方がいちばん気づきにくいです。

id と描画

エントリの識別子は slug ではなく id で、base からの相対パスから拡張子を落としたものになります。言語ごとにディレクトリを切っている構成なら ja/<slug> のような値です。URL を組み立てている箇所は、先頭のディレクトリを外す処理が要ります。

描画は、エントリのメソッドではなく関数です。

import { getCollection, render, type CollectionEntry } from 'astro:content';

const { Content, headings } = await render(entry);

返ってくるものは以前と同じなので、書き換えるのは呼び出しの形だけです。ただし呼び出している箇所すべてに要ります。

直す

順番はこれで固定できます。

  1. src/content/config.tssrc/content.config.ts へ移す
  2. 各コレクションの type を消し、loaderglob({ pattern, base }) を渡す
  3. entry.render()render(entry) に書き換え、renderastro:content からインポートする
  4. entry.slug を参照している箇所を entry.id に直し、ディレクトリぶんの接頭辞を落とす

4つ目だけはエラーになりません。ビルドが通ってから URL が壊れていることに気づく形なので、置換のあとに実際のパスを1本開いて確認します。

気づけなかった理由

移行手順そのものは公式のアップグレードガイドに載っています。それでも順に踏んだのは、最初のエラー文言が3つのうち1つしか指していなかったからです。

LegacyContentConfigError は「置き場所が古い」とだけ言います。そこで手が止まり、移して再ビルドし、次のエラーで初めて2つ目の変更を知る。エラー駆動で直していくと、変更が3つあることを最後まで知らないまま進みます。1つ目のエラーが出た時点で、変更の全体像を先に読みに行くほうが速い、という順序の話でした。

同じ経緯を繰り返さないよう、この3点はスキーマの正本ファイルの冒頭に注意書きとして残してあります。設定を触る人が必ず開くファイルなので、移行ガイドを探しに行くより先に目に入ります。

デプロイ側で似た形を踏んだ話は Vercel デプロイで踏んだ3つの罠 に書いています。

よくある質問

Q1LegacyContentConfigError はどうすれば消えますか?

設定ファイルを src/content/config.ts から src/content.config.ts へ移します。ディレクトリの中ではなく src の直下で、ファイル名の区切りがスラッシュからドットに変わる形です。移し忘れではなく置き場所そのものが変わっているので、中身を直しても消えません。

Q2ファイルを移したら別のエラーが出ました

loader が無いためです。以前は type に content や data を指定していましたが、コレクションがどこからエントリを読むかを loader で明示する形に変わりました。Markdown を読むなら astro/loaders の glob に、パターンと base ディレクトリを渡します。

Q3entry.render() が無いと言われます

描画の入口が変わりました。エントリのメソッドではなく、astro:content から render をインポートして render(entry) の形で呼びます。返ってくるものは同じで、Content コンポーネントと見出しの一覧を受け取れます。書き換えは呼び出し箇所すべてに必要です。

Q4entry.slug は使えますか?

id に変わりました。id は base ディレクトリからの相対パスで、拡張子は含みません。言語ごとにサブディレクトリを切っている構成では ja/foo のような値になるため、URL を組み立てている箇所は先頭のディレクトリを外す処理が要ります。