· astro / mdx / content-collections

Cách thêm MDX vào Astro 5 Content Collections

Hướng dẫn cài đặt @astrojs/mdx và kết nối với Content Layer API của Astro 5 — frontmatter có kiểu dữ liệu, import component, và các lỗi thường gặp.

Bởi

1.147 từ · 6 phút đọc

Nếu bạn muốn nhúng React component tương tác vào các bài viết trên blog Astro, bạn cần MDX. Đây là cách thiết lập chính xác cho Astro 5 — bao gồm cấu hình Content Layer API đã thay thế cách tiếp cận cũ từ v2/v4.

Dành cho ai

Các developer đang dùng Astro 5 với Content Layer API và muốn nhúng component vào bài viết markdown. Nếu bạn vẫn đang dùng Astro v2 hoặc v3, bạn cần migrate src/content/config.ts trước — xem hướng dẫn nâng cấp lên Astro v5 trước khi tiếp tục. Nếu bạn đã dùng Astro 6, các bước dưới đây vẫn áp dụng được; xem bài review Astro 6 của chúng tôi để biết thêm những gì thay đổi trong bản đó.

Môi trường thử nghiệm

  • Astro: 5.18.2
  • @astrojs/mdx: 4.3.14
  • Node: 20.x, macOS

Bước 1: Cài đặt MDX integration

Cách nhanh nhất là dùng Astro CLI:

npx astro add mdx

Lệnh này cài @astrojs/mdx@^4 và tự động thêm integration vào astro.config.mjs.

Cài thủ công nếu bạn muốn kiểm soát rõ ràng hơn:

npm install @astrojs/mdx@^4
# hoặc: pnpm add @astrojs/mdx@^4
# hoặc: bun add @astrojs/mdx@^4

Sau đó thêm vào config:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx()],
});

Lỗi thường gặp: nếu astro.config.mjs đang có lỗi cú pháp khi bạn chạy npx astro add mdx, CLI sẽ báo thành công nhưng thực ra bỏ qua việc inject integration. Hãy mở file sau đó và xác nhận mdx() đã xuất hiện trong integrations.


Bước 2: Cấu hình src/content.config.ts

Astro 5 đã chuyển config collection từ src/content/config.ts (đường dẫn cũ của v2/v4) sang src/content.config.ts ở thư mục gốc của src/. Các collection giờ bắt buộc phải có thuộc tính loader: — thiếu nó sẽ gây ra lỗi Zod validation khó hiểu lúc build.

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({
    base: './src/content/blog',
    pattern: '**/*.{md,mdx}',  // chấp nhận cả md lẫn mdx
  }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

z.coerce.date() quan trọng ở đây: ngày trong frontmatter YAML được đọc dưới dạng string, và coerce xử lý việc chuyển đổi string thành Date mà không cần custom transform.

Lỗi thường gặp: giữ nguyên đường dẫn cũ src/content/config.ts khiến Astro âm thầm bỏ qua schema của bạn — build vẫn thành công, nhưng frontmatter không có kiểu dữ liệu.


Bước 3: Viết file .mdx

Đặt một file .mdx bất kỳ trong đường dẫn glob và import component trực tiếp trong phần nội dung:

---
title: "Hello MDX"
description: "A post with an inline callout component."
pubDate: 2025-06-01
---
import Callout from '../../components/Callout.astro';

Bài viết này nhúng component trực tiếp:

<Callout type="warning">
  Tính năng này hiện đang trong giai đoạn beta.
</Callout>

Tiếp tục đọc...

Để tránh rủi ro khi di chuyển bài viết vào thư mục con, hãy dùng path alias đã định nghĩa trong tsconfig.json (@/components/Callout.astro) thay vì relative import — relative path sẽ bị gãy khi bạn di chuyển file.


Bước 4: Render trong [...slug].astro

Có hai thay đổi trong Astro v5 dễ bị bỏ sót: render() giờ là import độc lập từ astro:content (không còn là method trên post object nữa), và dynamic segment nên dùng post.id thay vì post.slug.

---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
import Callout from '../../components/Callout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map(post => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<html>
  <body>
    <h1>{post.data.title}</h1>
    <!-- truyền Callout vào để mọi file MDX có thể dùng mà không cần import tường minh -->
    <Content components={{ Callout }} />
  </body>
</html>

Truyền components vào <Content /> là tùy chọn — nó ánh xạ tên component sang implementation cụ thể để các file MDX có thể dùng <Callout> mà không cần import trực tiếp. Hữu ích khi bạn có một component từ design system mà mọi bài viết đều dùng.


Các lỗi thường gặp

  • Đường dẫn config đã thay đổi: src/content/config.tssrc/content.config.ts (nằm ở gốc src/, không phải trong content/). Đường dẫn cũ bị bỏ qua âm thầm — không có build error, không có type safety.
  • slug đổi tên thành id: post.slug trả về undefined trong Astro v5. Dùng post.id ở khắp nơi, kể cả trong getStaticPaths().
  • render() là import độc lập: await post.render() (v4) → import { render } from 'astro:content'; await render(post) (v5).
  • HTML comment gây lỗi parse: MDX là JSX. <!-- comment --> là lỗi cú pháp. Dùng {/* comment */} thay thế.
  • File bắt đầu bằng underscore không còn bị tự động loại trừ: _draft.mdx bị bỏ qua âm thầm trong v4. Trong v5 nó được đưa vào collection. Hãy lọc tường minh bằng draft: true trong frontmatter và getCollection('blog', ({ data }) => !data.draft).
  • Thứ tự Remark plugin: @astrojs/mdx kế thừa markdown.remarkPlugins theo mặc định. Nếu một plugin giả định nó chạy cuối cùng, nó có thể thấy các MDX node chưa được xử lý. Đặt extendMarkdownConfig: false trong tùy chọn MDX integration và khai báo tường minh tất cả plugin.

Cloudflare Pages

MDX được xử lý hoàn toàn lúc build. Khi bạn chạy astro build, mọi file .mdx được biên dịch thành HTML và JavaScript tĩnh trước khi deploy. Cloudflare Pages serve output đó — không cần cấu hình adapter thêm cho MDX.

SSR với @astrojs/cloudflare không bị ảnh hưởng: MDX collection luôn được prerender bất kể adapter. Nếu build của bạn chạy thành công cục bộ với astro build, Cloudflare Pages sẽ serve đúng. Xem hướng dẫn deploy đầy đủ tại Cách deploy site Astro lên Cloudflare Pages.


Nhận xét

Thêm MDX khi bài viết cần nhúng UI tương tác — code playground, live demo, hoặc design-system component như callout hay warning. Nếu bài viết là nội dung thuần văn bản, .md build nhanh hơn, dễ bàn giao hơn cho tác giả không phải developer, và dễ chuyển sang framework khác hơn.

MDX thêm một bước biên dịch JSX cho mỗi file. Với 50–100 bài viết, overhead build không đáng kể trong thực tế. Với 1.000+ file, hãy bật optimize: true trong config của MDX integration — Astro docs đánh dấu đây là tùy chọn được khuyến nghị cho collection lớn.

Nếu hướng dẫn này là một phần trong quá trình xây dựng site đa ngôn ngữ, bước tiếp theo tự nhiên là Cách xây dựng site Astro đa ngôn ngữ.