Skip to content

Rendering articles

The loader compiles each article’s Markdown at build time, so rendering is the standard Astro collection flow: render(entry) returns a Content component you drop into the page. You don’t run a Markdown renderer yourself.

src/pages/blog/[slug].astro
---
import { getCollection, render } from "astro:content";
export const getStaticPaths = async () =>
(await getCollection("articles")).map((p) => ({ params: { slug: p.id }, props: { p } }));
const { p } = Astro.props;
const { Content } = await render(p);
---
<h1>{p.data.title}</h1>
<Content />

render is imported from astro:content and called with the whole entry. It reads entry.rendered (the compiled body) — not entry.data — and gives you back Content, plus headings for a table of contents if you want one.

The body is pre-compiled, not in entry.data

Section titled “The body is pre-compiled, not in entry.data”

The Markdown lives in two places on the entry, never in entry.data:

  • entry.body — the raw Markdown source string.
  • entry.rendered — the compiled output that <Content /> renders.

So entry.data is metadata only (title, author, tags, timestamps, image). Reach for entry.body only if you need the raw source — for a reading-time estimate, an excerpt, or a search index. For display, use <Content />.

The <head> is driven by the metadata fields, separate from the body. Set the title, description, and canonical from entry.data in the same page:

src/pages/blog/[slug].astro
---
import { getCollection, render } from "astro:content";
export const getStaticPaths = async () =>
(await getCollection("articles")).map((p) => ({ params: { slug: p.id }, props: { p } }));
const { p } = Astro.props;
const { Content } = await render(p);
const { title, seoTitle, seoDescription, canonicalUrl, image, updatedAt } = p.data;
const canonical = canonicalUrl ?? new URL(p.id, Astro.site).href;
---
<head>
<title>{seoTitle}</title>
<meta name="description" content={seoDescription ?? ""} />
<link rel="canonical" href={canonical} />
{image && <meta property="og:image" content={image.url} />}
<meta property="article:modified_time" content={updatedAt.toISOString()} />
</head>
<article>
<h1>{title}</h1>
<Content />
</article>

seoTitle already falls back to title server-side, so it’s always a string. seoDescription, canonicalUrl, and image can be null — guard them. The timestamps are Date objects, so call .toISOString() (or .toLocaleDateString() for display) directly.