# Markdown and MDX

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Content
Source: https://www.devtutorials.ai/learn/astro/content/markdown-and-mdx

> Markdown pages, frontmatter, heading IDs, import.meta.glob() (which replaced Astro.glob()), layouts for Markdown, the Sätteri and unified processors, the markdown config option, syntax highlighting with Shiki and Prism, the Code component, MDX and Markdoc.

## Markdown in Astro

- **Markdown** = a simple way to write formatted text with plain characters.
- `# Title` makes a heading. `**word**` makes bold text. `- item` makes a list.
- Writers like it because they can focus on words, not tags.
- Astro turns `.md` files into HTML pages for you.
- In this lesson, Asha moves her posts from `.astro` code into Markdown files.

**Simple lesson:** write content in Markdown, and Astro builds the HTML.

### A Real-World Example

- Asha's posts are long. Writing every paragraph inside `<p>` tags is slow.
- Her friend Ravi wants to help write posts, but he does not know HTML.
- In Markdown, both of them just type text, headings and lists.
- Each `.md` file in `src/pages` becomes a page, like an `.astro` file.

Press **▶ Run**. Then open `first-post.md` and change the text.

What happened:

- The part between `---` lines at the top is **frontmatter**: data about the page.
- Below it, the Markdown became headings, a paragraph, bold text and a list.
- Open **HTML sent to browser**. Each heading got an `id`, such as `id="what-i-like"`.

### What You Will Learn

In this lesson, you will learn:

1. How Markdown pages and frontmatter work.
2. How headings get IDs for anchor links.
3. How to read many Markdown files with `import.meta.glob()`.
4. How to give Markdown pages a layout.
5. How Astro processes Markdown, and how to change the processor.
6. How code blocks get syntax highlighting.
7. What MDX adds, and when to use it.

## Markdown Basics

### Markdown Pages

- A `.md` file in `src/pages` becomes a page. `src/pages/first-post.md` builds to `/first-post`.
- The same routing rules apply as for `.astro` files.

### Frontmatter

- **Frontmatter** = data at the top of a Markdown file, between two `---` lines.
- It is written in **YAML**: `key: value` on each line.
- Common fields: `title`, `description`, `pubDate`, `tags`, `layout`.
- A list in YAML is `tags: [astro, markdown]`.

### Heading IDs and Anchor Links

- Astro gives every heading an `id` made from its text.
- `## What I like` gets `id="what-i-like"`.
- You can link straight to it: `<a href="/first-post#what-i-like">`.
- This is how a table of contents jumps to a section.

Press **▶ Run**. The frontmatter is read by the home page. Then open the guide and check the heading IDs in **HTML sent to browser**.

<p>
    <a href="/setup-guide">{guide.frontmatter.title}</a>
    · {guide.frontmatter.minutes} min · tags: {guide.frontmatter.tags.join(", ")}
</p>
<p>Jump straight to a section: <a href="/setup-guide#install-nodejs">Install Node.js</a></p>
`,
    "src/pages/setup-guide.md": `---
title: Set up your computer
minutes: 10
tags: [setup, nodejs]
---

## Install Node.js

Download the LTS version.

## Create the project

Run npm create astro@latest.
`,
  }}
/>

- `import * as guide` loads the Markdown file as a module. `guide.frontmatter` holds its data.
- `## Install Node.js` got `id="install-nodejs"`. The dot was dropped.

**Simple lesson:** frontmatter holds the data, the Markdown below it holds the content.

## Layouts for Markdown Pages

### The layout Frontmatter Property

- A bare Markdown page has no `<html>`, `<head>` or header.
- Add `layout: ../layouts/PostLayout.astro` to the frontmatter.
- Astro wraps the page in that layout.

### Receiving frontmatter Props in a Layout

- The layout gets the frontmatter as a prop called `frontmatter`.
- It also gets `headings`: a list of every heading, with `depth`, `slug` and `text`.
- The Markdown content arrives in the layout's `<slot />`.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 210" role="img" aria-labelledby="fig-mdlayout" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-mdlayout">Animation: the frontmatter goes to the layout as the frontmatter prop. The Markdown body becomes HTML and goes into the layout's slot.</title>
  <defs><marker id="ml-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="20" width="220" height="170" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)", strokeWidth: 1.5 }} />
  <text x="130" y="40" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12 }}>first-post.md</text>
  <rect x="32" y="50" width="196" height="54" rx="6" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} />
  <text x="44" y="72" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 12 }}>title: My first post</text>
  <text x="44" y="92" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 12 }}>layout: PostLayout</text>
  <rect x="32" y="114" width="196" height="64" rx="6" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="44" y="138" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 12 }}># Heading</text>
  <text x="44" y="160" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 12 }}>Some **bold** text</text>
  <rect x="400" y="20" width="220" height="170" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} />
  <text x="510" y="40" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>PostLayout.astro</text>
  <text x="414" y="78" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>{"{"}frontmatter.title{"}"}</text>
  <rect x="414" y="114" width="192" height="40" rx="6" style={{ fill: "none", stroke: "var(--ngc-green)", strokeDasharray: "4 4" }} />
  <text x="510" y="139" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 12 }}>&lt;slot /&gt;</text>
  <path d="M228 77 H410" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2, fill: "none" }} markerEnd="url(#ml-arrow)" />
  <path d="M228 146 H410" style={{ stroke: "var(--ngc-blue)", strokeWidth: 2, fill: "none" }} markerEnd="url(#ml-arrow)" />
  <circle r="6" cy="77" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="cx" values="228;410" dur="2s" repeatCount="indefinite" /></circle>
  <circle r="6" cy="146" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="228;410" dur="2s" begin="1s" repeatCount="indefinite" /></circle>
  <text x="320" y="68" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 11 }}>frontmatter prop</text>
  <text x="320" y="137" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>HTML into the slot</text>
</svg>
<figcaption>Data goes in as a prop. The content goes into the slot.</figcaption>
</figure>

Press **▶ Run**. The layout shows the title, the author and a small table of contents from `headings`.

    <head>
      <meta charset="utf-8" />
      <title>{frontmatter.title}</title>
    </head>
    <body>
      <h1>{frontmatter.title}</h1>
      <p><em>By {frontmatter.author}</em></p>
      <nav>
        <strong>On this page:</strong>
        <ul>
          {headings.filter((h) => h.depth === 2).map((h) => (
            <li><a href={"#" + h.slug}>{h.text}</a></li>
          ))}
        </ul>
      </nav>
      <article>
        <slot />
      </article>
    </body>
</html>
`,
    "src/pages/learning-markdown.md": `---
layout: ../layouts/PostLayout.astro
title: Learning Markdown
author: Asha
---

## Headings

Use one to six hash signs.

## Lists

- Start a line with a dash.
- Or with a number and a dot.

## Links

Write [the link text](https://astro.build) and the address.
`,
  }}
/>

In Markdown pages, the `layout` field is the only way to add a layout. In content collections, you will render Markdown inside an `.astro` page instead. That is the more common way in real projects.

**Simple lesson:** `layout:` in frontmatter wraps a Markdown page. The layout reads `frontmatter` and `headings`.

## Importing Markdown Files

### import.meta.glob()

- `import.meta.glob()` loads many files at once, using a **glob pattern**.
- **Glob pattern** = a file path with wildcards. `*` means "any name". `**` means "any folders".
- `import.meta.glob("../posts/*.md", { eager: true })` loads every `.md` file in `src/posts`.
- It returns an object: each key is a file path, each value is the file's module.
- A Markdown module has `frontmatter`, `url`, `file`, `getHeadings()` and a `Content` component.

### Astro.glob() Was Removed

- Older tutorials use `Astro.glob()`. It no longer exists in Astro 7.
- `import.meta.glob()` is the replacement. It comes from Vite, the build tool inside Astro.
- With `{ eager: true }`, the files load right away. Without it, each value is a function you must call.

Press **▶ Run**. The home page lists every post file in `src/posts`, without listing them by hand.

<ul>
    {posts.map((post) => (
      <li>
        <strong>{post.frontmatter.title}</strong>
        <small> · {post.frontmatter.date}</small>
      </li>
    ))}
</ul>
`,
    "src/posts/why-astro.md": `---
title: Why I chose Astro
date: 10 Nov 2026
---

Because it sends HTML first.
`,
    "src/posts/markdown-tips.md": `---
title: Markdown tips
date: 12 Nov 2026
---

Use headings to split long posts.
`,
  }}
/>

- Add a third file in your head: it would appear in the list with no other change.
- For real blogs, **Content Collections** do this job better. They also check each file's frontmatter.

**Simple lesson:** `import.meta.glob()` reads many files at once. It replaced `Astro.glob()`.

## Markdown Processing

### The Sätteri Processor (Default in Astro 7)

- **Markdown processor** = the tool that turns Markdown text into HTML.
- Astro 7's default processor is **Sätteri**, Astro's own pipeline written in Rust. It is very fast.
- It supports GitHub-Flavored Markdown (GFM): tables, task lists, strikethrough and footnotes.

### The Unified Processor

- **Unified** = the older processor, built from **remark** (Markdown) and **rehype** (HTML) plugins.
- Use it when you need a remark or rehype plugin, such as `remark-toc` for a table of contents.

### Switching Between Processors

You choose the processor with `markdown.processor` in `astro.config.mjs`:

```js
import { defineConfig } from "astro/config";
import { unified } from "@astrojs/markdown-remark";
import remarkToc from "remark-toc";

export default defineConfig({
  markdown: {
    processor: unified({
      remarkPlugins: [remarkToc],
    }),
  },
});
```

- To set options for Sätteri, install `@astrojs/markdown-satteri` and pass `satteri({ ... })` the same way.

### The markdown Configuration Option

- `markdown` in the config holds all Markdown settings: the processor, `syntaxHighlight` and `shikiConfig`.
- Older top-level options, such as `markdown.remarkPlugins`, still work but are deprecated. Pass plugins to the processor instead.

In this lab, Markdown is built by Astro's unified processor (`@astrojs/markdown-remark`), because Sätteri's browser build needs a feature this site does not turn on. For normal Markdown, both give the same HTML.

**Simple lesson:** Sätteri is the fast default. Switch to `unified()` when you need remark or rehype plugins.

## Syntax Highlighting

### Shiki Highlighting for Code Blocks

- **Syntax highlighting** = colouring code so it is easier to read.
- In Markdown, a code block starts with three backticks and a language name, such as `js` or `bash`.
- Astro colours it at build time with **Shiki**, the default highlighter. No JavaScript is sent.

### Themes

- Shiki has many themes. Pick one in the config:

```js
markdown: {
  shikiConfig: {
    theme: "github-dark",
  },
},
```

- For **light and dark mode**, set two themes: `themes: { light: "github-light", dark: "github-dark" }`. Astro writes both as CSS variables. Your CSS picks one.
- You can add your own theme as a JSON file, or change colours with `transformers`.

### The Code Component

- In `.astro` files, use `<Code />` to highlight a string of code:

```astro
---
import { Code } from "astro:components";
---

<Code code={`const site = "Dev Journal";`} lang="js" theme="dracula" />
```

- Its props: `code`, `lang`, `theme`, and `transformers` for extra changes.

### Prism (Opt-In)

- **Prism** = another highlighter. Turn it on with `markdown: { syntaxHighlight: "prism" }`.
- Prism only adds class names. You add a Prism CSS theme yourself.
- In `.astro` files, use `<Prism lang="js" code={...} />` from `@astrojs/prism`.

Press **▶ Run**. This lab highlights with Prism, so the code blocks get `token` classes. A small Prism theme is added with CSS.

`,
  }}
/>

Highlighting at build time means visitors download no highlighter code. The colours are just HTML and CSS.

**Simple lesson:** code blocks are coloured at build time. Shiki by default, Prism if you choose it.

## MDX

### Adding MDX with astro add mdx

- **MDX** = Markdown that can also use components and JavaScript expressions.
- Add it with `npx astro add mdx`. Then `.mdx` files work like `.md` files.

### Using Components in MDX

```mdx
---
title: My MDX post
---
import Note from "../components/Note.astro";

# Hello

<Note>This box is an Astro component inside Markdown.</Note>
```

### Exporting Variables from MDX

- An MDX file can export values: `export const readingTime = 3;`.
- Use them in the same file with `{readingTime}`.

### Replacing HTML Elements with Custom Components

- `<Content components={{ h2: FancyHeading }} />` swaps every `h2` for your own component.
- Useful to add an icon to every link, or a copy button to every code block.

### Choosing Between Markdown and MDX

Use **Markdown** when:

- the content is plain text, headings, lists, links and images,
- writers who do not code will edit it.

Use **MDX** when:

- the content needs components, such as notes, charts or demos,
- you are comfortable with a little JSX syntax.

### Markdoc (Alternative)

- **Markdoc** = another format with its own tag syntax, `{% note %}...{% /note %}`.
- Add it with `npx astro add markdoc`. Teams pick it when writers must not run any code.

MDX needs the `@astrojs/mdx` package. This lab cannot install packages, so try the MDX examples on your own computer.

**Simple lesson:** Markdown for plain content. MDX when content needs components.

## TL;DR

1. **Markdown** = formatted text from plain characters. `.md` in `src/pages` = a page.
2. **Frontmatter** = YAML data between `---` lines at the top.
3. **Heading IDs** = made from the text: `## What I like` → `#what-i-like`.
4. **layout:** in frontmatter = wrap the page. The layout gets `frontmatter`, `headings` and a `<slot />`.
5. **import.meta.glob()** = load many files: `import.meta.glob("../posts/*.md", { eager: true })`. It replaced `Astro.glob()`.
6. **Processors:**
   - Sätteri = Astro 7's fast default.
   - `unified()` = remark and rehype plugins.
7. **Highlighting** = Shiki by default, Prism opt-in, `<Code />` in `.astro` files.
8. **MDX** = Markdown plus components. `npx astro add mdx`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Using `Astro.glob()` | Old code still works | Use `import.meta.glob()` | Build error in Astro 7 |
| Wrong layout path in frontmatter | Paths start from `src` | The path is relative to the `.md` file | Build error: layout not found |
| Forgetting `{ eager: true }` | Values are modules | Without it, values are functions to call | `post.frontmatter` is `undefined` |
| Bad YAML indentation | Any spacing works | Use `key: value`, one per line, with a space after the colon | The frontmatter is empty or the build fails |
| `remarkPlugins` with Sätteri | All processors take remark plugins | Use `unified()` for remark plugins | The plugin is ignored or errors |
| Components in a `.md` file | Markdown runs components | Use `.mdx` for components | The tag shows as plain text |

</div>

## Check Yourself

## Exercises

## What to Remember

- A `.md` file in `src/pages` is a page. Frontmatter holds its data.
- `layout:` wraps a Markdown page. The layout gets `frontmatter` and `headings`.
- `import.meta.glob()` loads many files. `Astro.glob()` is gone.
- Sätteri is the default processor. `unified()` runs remark and rehype plugins.
- Code blocks are highlighted at build time. MDX adds components.

Next: **Content Collections**, where every post is checked by a schema before the site is built.
