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

Rendering Markdown

ใน Next.js พร้อม contentlayer คุณ import MDX component โดยตรง ใน Astro คุณเรียก render(entry) (API เดียวตั้งแต่ Astro 5 — method entry.render() แบบเก่าถูกถอดออกไปตอน entry กลายเป็น plain serializable object) เพื่อรับ Content component แล้วใช้ <Content /> ใน template ฟิลด์ frontmatter อยู่ที่ entry.data ส่วนเนื้อหาที่ render แล้วมาจาก Content

ต้องใช้โปรเจกต์ Astro เต็มรูปแบบ ใช้ npm create astro@latest เพื่อสร้างโปรเจกต์

React
// app/blog/[slug]/page.tsx (Next.js App Router + contentlayer)
import { allPosts } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks';
export default function PostPage({ params }) {
const post = allPosts.find(p => p.slug === params.slug);
const MDXContent = useMDXComponent(post.body.code);
return (
<article>
<h1>{post.title}</h1>
<MDXContent />
</article>
);
}
Astro
---
// src/pages/blog/[slug].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({ params: { slug: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>

render(entry) คืนค่า { Content, headings, remarkPluginFrontmatter } — Content เป็น component ที่ render ได้โดยตรง ไม่มี hook ไม่มีการ compile MDX ฝั่ง client

src/pages/blog/[slug].astro
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.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, headings } = await render(post);
---
<BaseLayout title={post.data.title}>
<article>
<header>
<h1>{post.data.title}</h1>
<time datetime={post.data.date.toISOString()}>
{post.data.date.toLocaleDateString('en-US', { dateStyle: 'long' })}
</time>
</header>
<Content />
</article>
</BaseLayout>
React
// app/blog/page.tsx (Next.js App Router + contentlayer)
import { allPosts } from 'contentlayer/generated';
export default function BlogIndex() {
const sorted = allPosts.sort(
(a, b) => new Date(b.date) - new Date(a.date)
);
return (
<ul>
{sorted.map(post => (
<li key={post.slug}>
<a href={post.url}>{post.title}</a>
</li>
))}
</ul>
);
}
Astro
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sorted = posts.sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
---
<ul>
{sorted.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
</li>
))}
</ul>

Astro 7 render ไฟล์ .md และ .mdx ด้วย Sätteri ซึ่งเป็น Markdown pipeline แบบ native ที่เขียนด้วย Rust แทน pipeline remark/rehype (unified) แบบเดิม Content Collections, render(entry), headings และ frontmatter ทำงานเหมือนเดิมทุกอย่าง แค่ build เร็วขึ้น (Astro รายงานว่า build เร็วขึ้นโดยรวม 15–61% จาก benchmark เว็บไซต์จริงของทีม Astro) สองเรื่องที่ควรรู้ตอนย้ายโปรเจกต์ Next.js/contentlayer:

  • Markdown ธรรมดา, ตาราง GFM, code fence กับ Shiki และ heading ID ไม่ต้องตั้งค่าอะไรเพิ่ม
  • ถ้าพึ่งพา remark/rehype plugin แบบ custom (เช่น rehype-autolink-headings) ให้ opt-in กลับไปใช้ unified pipeline อย่างชัดเจน:
astro.config.mjs
import { defineConfig } from 'astro/config';
import { unified } from '@astrojs/markdown-remark';
export default defineConfig({
markdown: {
processor: unified(), // remark/rehype pipeline
rehypePlugins: [/* ... */],
},
});

MDX ยังผ่าน @astrojs/mdx เหมือนเดิม สิ่งที่เปลี่ยนคือ processor ค่าเริ่มต้นของเนื้อหา Markdown เท่านั้น

ฟังก์ชันใดที่เรียกเพื่อรับ renderable component จาก collection entry?
แสดงเนื้อหาที่ render แล้วใน Astro template อย่างไร?
`getStaticPaths()` คืนค่าอะไรสำหรับ dynamic collection route?