ข้ามไปยังเนื้อหา

Content Collections

collection อยู่ในไฟล์ config ไฟล์เดียวคือ src/content.config.ts แต่ละ collection คือการเรียก defineCollection ที่มีสองส่วน คือ loader (content มาจากไหน) กับ schema (โครงสร้างที่ validate แล้ว):

src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
const blog = defineCollection({
// loader: อ่านทุกไฟล์ .md/.mdx ใต้ src/data/blog (ข้ามไฟล์ _drafts)
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/data/blog' }),
// schema: validate frontmatter ของแต่ละไฟล์
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };

กฎสำคัญของ Astro 6 คือ ทุก collection ต้องประกาศ loader พฤติกรรมเก่าที่แค่โยนไฟล์ลง src/content/blog/ แล้ว Astro เก็บให้อัตโนมัติ หายไปแล้ว การเขียนให้ชัดเจนแปลว่า API เดียวกันโหลดไฟล์ในเครื่องได้วันนี้ และโหลด API ปลายทางได้พรุ่งนี้ โดยวิธี query ไม่ต้องเปลี่ยนเลย

Astro มี loader มาให้สองตัวจาก astro/loaders:

  • glob() — โหลดหลายไฟล์ที่ตรง pattern แต่ละไฟล์กลายเป็นหนึ่ง entry ใช้กับโฟลเดอร์ที่มีโพสต์บล็อกหรือหน้า docs: glob({ pattern: '**/*.md', base: './src/data/blog' })
  • file() — โหลดหลาย entry จากไฟล์ข้อมูล ไฟล์เดียว (array JSON หรือ YAML) ใช้กับข้อมูลที่มีโครงสร้างอย่าง author หรือ product: file('./src/data/authors.json')

pattern **/[^_]*.{md,mdx} น่าอ่านให้เข้าใจ pattern นี้ตรงกับไฟล์ .md และ .mdx ในทุก subfolder แต่ [^_] ตัดไฟล์ที่ขึ้นต้นด้วย underscore ออก ที่เป็น convention ที่สะดวกสำหรับ draft

นอกจากนี้ loader เป็น interface ที่มีเอกสารกำกับ ecosystem จึงมี loader สำหรับ CMS และ API ต่าง ๆ และคุณเขียนเองเพื่อดึงจากแหล่งไหนก็ได้

flowchart LR
  src["files: src/data/blog/*.md"] --> loader["glob loader อ่านไฟล์"]
  loader --> schema["schema validate frontmatter"]
  schema --> store["entry ที่มี type ใน content store"]
  store --> get["getCollection('blog')"]
  get --> page["page render แต่ละ entry"]
pipeline ของ content collection

เมื่อประกาศแล้ว เรา query collection จาก component script ไหนก็ได้ ด้วยสองฟังก์ชันจาก astro:content:

---
import { getCollection, getEntry } from 'astro:content';
// โพสต์ที่ publish ทั้งหมด ใหม่สุดก่อน
const posts = (await getCollection('blog'))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// entry เดียวจาก id
const featured = await getEntry('blog', 'hello-world');
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>

แต่ละ entry มี id (ได้มาจากชื่อไฟล์) และ object data ซึ่งคือ frontmatter ที่ validate แล้ว มี type ครบจาก schema เพราะ validation รันไปแล้ว post.data.pubDate จึงเป็น Date จริง ไม่ใช่ string และ editor ก็ autocomplete field ให้

การ query ให้ frontmatter ของ entry แต่ body ที่เป็น Markdown ยังไม่ใช่ HTML ถ้าจะ render body ต้องเรียก render (จาก astro:content) บน entry จะคืน component <Content /> ที่เราเอาไปวางใน template ได้:

---
import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'hello-world');
if (!post) throw new Error('Post not found');
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toDateString()}</time>
<Content />
</article>

render คอมไพล์ Markdown/MDX ของ entry ให้เป็น component ดังนั้น <Content /> จึงพ่น HTML สำเร็จรูปออกมา ทั้งหัวข้อ, code block, และ component ที่ใช้ใน MDX render ครบ

ใน Astro 6 ประกาศ content collection ที่ไหน?
loader แบบ glob กับ file ต่างกันยังไง?
จะ render body ที่เป็น Markdown ของ entry ให้เป็น HTML ยังไง?
หลัง schema validate แล้ว field แบบ z.coerce.date() บน entry.data เป็น type อะไร?