Rendering Content

From an Entry to Actual HTML on the Page

Rendering Content

An entry's body is raw Markdown/MDX. `render(entry)` returns a `<Content />` component you can drop into JSX.

4 min read Level 2/5 #astro#render#content
What you'll learn
  • Render Markdown/MDX content
  • Pass components into MDX
  • Read the table of contents (`headings`)

A collection entry’s body is raw text. To render it as HTML, use the render() helper.

The Pattern

---
// 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(post) returns:

  • Content — a component that renders the parsed Markdown/MDX
  • headings — an array of { depth, slug, text } for building a table of contents
  • remarkPluginFrontmatter — frontmatter after remark plugins have run

Building a Table of Contents

---
const { Content, headings } = await render(post);
---

<nav>
  <ul>
    {headings.map(h => (
      <li class:list={[`toc-depth-${h.depth}`]}>
        <a href={`#${h.slug}`}>{h.text}</a>
      </li>
    ))}
  </ul>
</nav>

<article>
  <Content />
</article>

Passing Components to MDX

For MDX entries, you can supply components to replace default elements:

---
import Callout from "../components/Callout.astro";
const { Content } = await render(post);
---

<Content components={{ Callout }} />

Now any <Callout> used inside the .mdx body resolves to your component without needing an import in every file.

When You Don’t Need render

If you only need frontmatter on an index page, skip render() — you don’t need the parsed body for listings.

Chapter Done

That wraps content collections. Next chapter steps OUT of pure static — adding interactivity via islands.

Islands Intro →