2026-04-22 · 6 min
Migrate dari Astro 4 content collections ke Astro 5+ Content Layer API
Astro 5 (released late 2024) menggeser content collections dari file-system magic ke explicit loader API. Sebagian besar project Astro 4 break ketika upgrade. Saya migrate 3 project Astro saya dari 4.x ke 5.x bulan lalu, dan dokumentasikan di sini.
TL;DR perbedaan
Astro 4:
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
schema: z.object({ title: z.string() }),
});
export const collections = { posts };
Astro 4 mengasumsikan content ada di src/content/posts/ secara implicit.
Astro 5:
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({ title: z.string() }),
});
export const collections = { posts };
Explicit loader, dengan base path yang explicit.
Migration steps
1. Rename src/content/config.ts ke src/content.config.ts
File config sekarang di root src/, bukan di dalam content/ folder. Lebih konsisten dengan Astro convention lain.
mv src/content/config.ts src/content.config.ts
2. Import glob loader
// tambah ini di atas content.config.ts
import { glob } from 'astro/loaders';
3. Convert setiap collection
Untuk setiap defineCollection({ schema }) di file lama, ubah jadi:
defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/<folder>' }),
schema: <existing schema>,
});
4. Update usage di pages
Sebagian besar API yang sama:
---
// works in both Astro 4 and 5
import { getCollection, getEntry } from 'astro:content';
const posts = await getCollection('posts');
---
Tetapi render() API berubah. Astro 4:
---
const post = await getEntry('posts', slug);
const { Content } = await post.render();
---
Astro 5:
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('posts', slug);
const { Content } = await render(post);
---
5. Update getStaticPaths
post.slug is now post.id in Astro 5:
// Astro 4
return posts.map((post) => ({ params: { slug: post.slug } }));
// Astro 5
return posts.map((post) => ({ params: { slug: post.id } }));
Yang harus diwaspadai
- Image fields: Astro 5 mengubah cara
image()Zod helper bekerja. Pastikan import dariastro:contentmasih works untuk schema dengan image field. - Reference fields:
reference()di schema tetap bekerja, tetapi pastikan target collection juga sudah menggunakan loader API. - Plugins yang custom load content (e.g., dari API external) — sekarang lebih sustainable dengan custom loader. Lihat docs untuk
defineLoader.
Bug yang saya temukan
Saat migrate, saya temukan satu bug yang tidak immediately obvious:
Issue: pubDate Date type tidak parse dengan benar setelah migrate. Frontmatter pubDate: 2026-04-22 (tanpa quotes) yang previously bekerja sekarang error.
Fix: wrap dengan z.coerce.date() instead of z.date():
schema: z.object({
pubDate: z.coerce.date(), // bukan z.date()
})
Astro 5 lebih strict dengan type handling di YAML frontmatter. z.coerce.date() lebih forgiving.
Performance
Tidak ada perubahan signifikan di build time setelah migrate. Project saya yang 200+ markdown files build dalam 8-12 detik di Astro 4, sekarang 9-13 detik di Astro 5. Difference within noise.
Tetapi developer experience lebih baik — error message saat schema validation gagal sekarang menunjuk file + line yang tepat. Di Astro 4 sering hanya “validation failed” tanpa context.
Kalau Anda belum siap migrate
Astro 4 LTS support sampai akhir 2025. Tetapi sebagian besar integration baru (@astrojs/mdx ^5, @astrojs/starlight ^0.39) hanya support Astro 5/6. Pinning Astro 4 berarti pinning integration versions juga.
Saran saya: migrate sebelum Anda butuh integration update. Lebih predictable daripada forced migration di tengah-tengah feature work.
Migration ini dilakukan pada 3 project Astro saya selama April 2026. Repo contoh dengan before/after di github.com/ricky/astro-5-migration-example — placeholder, ganti dengan repo nyata.