Markdown in Astro
Markdown is the most natural way to write long-form content in Astro. A .md file is plain text with a lightweight syntax for headings, lists, links, and code, plus an optional block of YAML metadata at the top. Astro understands Markdown natively: it parses your files, renders them to semantic HTML, and ships zero JavaScript for the content itself. This makes Markdown ideal for blog posts, documentation, and any text-heavy page where speed and authoring comfort matter.
How Astro treats Markdown files
Astro processes Markdown in two distinct places, and knowing which one you are in determines how the file is used:
- File-based pages — a
.mdfile placed undersrc/pages/becomes a route automatically.src/pages/about.mdis served at/about, with no extra wiring. - Content collection entries — a
.mdfile undersrc/content/(or a configured content directory) is data, not a route. You query it with the Content Collections API and render it from an.astropage you control.
For anything beyond a one-off page, content collections are the recommended approach because they give you type-safe frontmatter and a single rendering layout.
Frontmatter
Frontmatter is a fenced block of YAML at the very top of the file, delimited by three dashes. It holds structured metadata — title, publish date, tags, draft status — that Astro parses separately from the body.
---
title: "Getting Started with Astro"
pubDate: 2026-06-14
author: "Ada Lovelace"
tags: ["astro", "ssg"]
draft: false
---
# Getting Started
Your first **Markdown** page renders as semantic HTML.
Quote any value that could be misread as a non-string — dates, numbers, and booleans you intend to keep as text. An unquoted
2026-06-14parses as a date, while"2026-06-14"stays a string.
When a Markdown file lives in src/pages/, its frontmatter is exposed to a layout via the frontmatter prop. When it lives in a collection, the same data is validated against your schema and returned as entry.data.
Assigning a layout
A standalone Markdown page renders only the content’s HTML — no <html>, <head>, or site chrome. The layout frontmatter key points at an .astro component that wraps the rendered body and receives the frontmatter automatically.
---
// src/layouts/PostLayout.astro
const { frontmatter } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{frontmatter.title}</title>
</head>
<body>
<h1>{frontmatter.title}</h1>
<p>By {frontmatter.author}</p>
<article>
<slot />
</article>
</body>
</html>
---
layout: ../layouts/PostLayout.astro
title: "Hello World"
author: "Grace Hopper"
---
This body is injected into the layout's `<slot />`.
The rendered Markdown is placed into the layout’s <slot />. Beyond frontmatter, the layout also receives helpful props such as url, file, and a headings array you can use to build a table of contents.
| Prop | Type | Description |
|---|---|---|
frontmatter | object | All parsed YAML frontmatter fields |
url | string | undefined | The page’s public path (pages only) |
file | string | Absolute path to the source .md file |
headings | MarkdownHeading[] | { depth, slug, text } for each heading |
Rendering collection entries
Collection Markdown carries no layout key. Instead you fetch the entry, call render(), and place the resulting <Content /> component inside your own page layout.
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';
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);
---
<PostLayout frontmatter={post.data}>
<Content />
</PostLayout>
This pattern keeps your content portable: the same Markdown can be rendered, listed, filtered, or fed to an RSS feed without ever hard-coding a layout into the file.
Frontmatter access summary
// Pages: read via the layout prop
const { frontmatter } = Astro.props;
// Collections: read via the schema-validated data object
const { Content } = await render(post);
console.log(post.data.title);
Output:
Getting Started with Astro
Best practices
- Reserve
src/pages/*.mdfor true one-off pages; route everything repeatable through content collections for type safety. - Define a schema for every collection so missing or mistyped frontmatter fails at build time instead of in production.
- Keep one canonical layout per content type and render collections through it rather than scattering
layout:keys across files. - Quote ambiguous frontmatter values (dates, ISBNs, versions like
"18") to prevent YAML from coercing them. - Use the
headingsarray to generate tables of contents instead of re-parsing the body. - Reach for MDX only when a page genuinely needs interactive components — plain
.mdstays lighter and ships zero JavaScript.