Skip to content

Rendering Markdown

In Next.js with contentlayer you import the MDX component directly. In Astro you call render(entry) (Astro 5+) to get a Content component, then drop <Content /> into your template. The frontmatter fields live on entry.data, the rendered body comes from Content.

Needs a full Astro project to run. Use npm create astro@latest to scaffold one.

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 (Astro 5+)
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
}
const { post } = Astro.props;
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>

render(entry) returns { Content, headings, remarkPluginFrontmatter }. Content is a component you render directly — no hooks, no client-side MDX compilation.

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.slug },
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.slug}/`}>{post.data.title}</a>
</li>
))}
</ul>
What function do you call to get a renderable component from a collection entry?
How do you output the rendered content in the Astro template?
What does `getStaticPaths()` return for a dynamic collection route?