Astro 7 Breaks Content Collections in Three Places
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
In Astro 7, src/content/config.ts is rejected with LegacyContentConfigError: the config moves to src/content.config.ts, collections need a loader, and entry.render() becomes render(entry).
The short version
Astro 7 changed three things about content collections at once: where the config file lives, the now-required loader, and the render API. Fixing one only reveals the next.
src/content/config.ts → src/content.config.ts location
type: 'content' → loader: glob({ ... }) how entries are read
await entry.render() → await render(entry) rendering
The first thing you see is LegacyContentConfigError. That message points at only the first of the three. Move the file and the build gets further, then hits the second. You end up walking them one at a time, so it is faster to fix all three up front.
What it looks like
After upgrading Astro, the build dies while reading the content collection config.
LegacyContentConfigError
The error says nothing about the contents of the config. You can re-read the zod schema looking for a mistake and find nothing wrong with it. What fails is the fact that a config file exists at that path at all.
Once moved, it stops on the collection definition instead. Further along, generating the article pages, it reports that entry.render() does not exist. Because it arrives in three stages, you fix and rebuild, fix and rebuild.
Why
All three are part of the same change. Collections went from “implicitly read a directory” to “declare where you read what from”.
Location
src/content/config.ts no longer works. It goes to src/content.config.ts — directly under src, not inside the directory, with the separator changing from a slash to a dot. The two look almost identical, so scanning a diff by eye will miss it.
loader is required
Instead of the old type: 'content', you pass how entries are read.
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 is relative to the project root, not to the config file. Get it wrong and there is no error — the collection is simply empty. The build succeeds and only the index pages come out blank. That is the failure mode that is hardest to notice.
id and rendering
An entry’s identifier is id, not slug, and it is the path relative to base with the extension dropped. With per-language directories that means ja/<slug>. Anywhere you build a URL needs to strip the leading directory.
Rendering is a function, not a method on the entry.
import { getCollection, render, type CollectionEntry } from 'astro:content';
const { Content, headings } = await render(entry);
What comes back is unchanged, so only the shape of the call is rewritten. But it is needed at every call site.
Fixing it
This order works.
- Move
src/content/config.tstosrc/content.config.ts - Drop
typefrom each collection and passglob({ pattern, base })asloader - Rewrite
entry.render()torender(entry)and importrenderfromastro:content - Change references to
entry.slugtoentry.idand strip the directory prefix
Only the fourth produces no error. The build passes and the URLs are broken, so after the replacement, open one real path and check it.
Why it took a while
The migration steps are in the official upgrade guide. We still walked them one at a time because the first error message named only one of the three changes.
LegacyContentConfigError says “the location is old” and nothing else. You stop there, move the file, rebuild, and only the next error tells you about the second change. Fixing error-by-error means you never learn there were three changes at all. Reading the whole set of changes the moment the first error appears is faster — it was a question of order, not of difficulty.
To avoid repeating this, the three points are now a note at the top of the schema’s source-of-truth file. Anyone touching the config opens that file, so it lands in front of them before they go looking for the migration guide.
A similar shape on the deploy side is in Three traps in deploying to Vercel.
よくある質問
Q1How do I get rid of LegacyContentConfigError?
Move the config file from src/content/config.ts to src/content.config.ts. It sits directly under src rather than inside the directory, and the separator changes from a slash to a dot. The location itself is what changed, so editing the file's contents will not clear the error.
Q2I moved the file and got a different error
There is no loader. Collections used to take type: 'content' or 'data'; now you declare where the collection reads its entries from. For Markdown, pass a pattern and a base directory to glob from astro/loaders.
Q3It says entry.render() does not exist
The render entry point moved. Instead of a method on the entry, import render from astro:content and call render(entry). What comes back is the same — the Content component and the list of headings. Every call site needs the rewrite.
Q4Can I still use entry.slug?
It is id now. id is the path relative to the base directory with the extension dropped. If you split content into per-language subdirectories it looks like ja/foo, so anywhere you build a URL needs to strip the leading directory.
この記事の根拠
- ドキュメントファイル 9〜14行目
- TypeScriptファイル 12〜16行目
- Astroファイル 1〜3行目
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。