Markdown & MDX

Markdown is commonly used to author text-heavy content like blog posts and documentation. Astro includes built-in support for standard Markdown (.md, .markdown, .mdown, .mkdn, .mkd, .mdwn) files.

With the @astrojs/mdx integration installed, Astro also supports MDX (.mdx) files which bring added features like support for JavaScript expressions and components in your Markdown content.

Use either or both types of files to write your Markdown content!

Astro treats any .md (or alternative supported extension) or .mdx file inside of the /src/pages/ directory as a page. Placing a file in this directory, or any sub-directory, will automatically build a page route using the pathname of the file.

📚 Read more about Astro’s file-based routing.

To start using Markdown in Astro, add a new file page-1.md to your project in the src/pages/ folder. Copy the basic template below into your file, and then view the rendered HTML in your browser preview. Usually, this is at http://localhost:3000/page-1.

src/pages/page-1.md
---
title: Hello, World
---

# Hi there!

This is your first markdown page. It probably isn't styled much, although
Markdown does support **bold** and _italics._

To learn more about adding a layout to your page, read the next section on **Markdown Layouts.**

Astro provides Markdown and MDX pages with a special frontmatter property for layout that defines the relative path to an Astro layout component. This component will wrap your Markdown content, providing a page shell and any other included page template elements.

src/pages/page.md
---
layout: ../layouts/BaseLayout.astro
title: "Astro v1 Launch!"
author: "Matthew Phillips"
date: "09 Aug 2022"
---

A typical layout for Markdown pages includes:

  1. The frontmatter prop to access the Markdown or MDX page’s frontmatter and other data. See Markdown Layout Props for a complete list of props available.
  2. A default <slot /> to indicate where the page’s Markdown content should be rendered.
src/layouts/BaseLayout.astro
---
// 1. The frontmatter prop gives access to frontmatter and other data
const { frontmatter } = Astro.props;
---
<html>
  <head>
    <!-- Add other Head elements here, like styles and meta tags. -->
    <title>{frontmatter.title}</title>
  </head>
  <body>
    <!-- Add other UI components here, like common headers and footers. -->
    <h1>{frontmatter.title} by {frontmatter.author}</h1>
    <!-- 2. Rendered HTML will be passed into the default slot. -->
    <slot />
    <p>Written on: {frontmatter.date}</p>
  </body>
</html>

You can set a layout’s Props type with the MarkdownLayoutProps helper:

src/layouts/BaseLayout.astro
---
import type { MarkdownLayoutProps } from 'astro';

type Props = MarkdownLayoutProps<{
  // Define frontmatter props here
  title: string;
  author: string;
  date: string;
}>;

// Now, `frontmatter`, `url`, and other Markdown layout properties
// are accessible with type safety
const { frontmatter, url } = Astro.props;
---
<html>
  <head>
    <title>{frontmatter.title}</title>
  </head>
  <body>
    <h1>{frontmatter.title} by {frontmatter.author}</h1>
    <slot />
    <p>Written on: {frontmatter.date}</p>
  </body>
</html>

A Markdown layout will have access to the following information via Astro.props:

  • file - The absolute path of this file (e.g. /home/user/projects/.../file.md).
  • url - If it’s a page, the URL of the page (e.g. /en/guides/markdown-content).
  • frontmatter - all frontmatter from the Markdown or MDX document.
    • frontmatter.file - The same as the top-level file property.
    • frontmatter.url - The same as the top-level url property.
  • headings - A list of headings (h1 -> h6) in the Markdown document with associated metadata. This list follows the type: { depth: number; slug: string; text: string }[].
  • rawContent() - A function that returns the raw Markdown document as a string.
  • compiledContent() - A function that returns the Markdown document compiled to an HTML string.

An example blog post may pass the following Astro.props object to its layout:

Astro.props = {
  file: "/home/user/projects/.../file.md",
  url: "/en/guides/markdown-content/",
  frontmatter: {
    /** Frontmatter from a blog post */
    title: "Astro 0.18 Release",
    date: "Tuesday, July 27 2021",
    author: "Matthew Phillips",
    description: "Astro 0.18 is our biggest release since Astro launch.",
    /** Generated values */
    file: "/home/user/projects/.../file.md",
    url: "/en/guides/markdown-content/"
  },
  headings: [
    {
      "depth": 1,
      "text": "Astro 0.18 Release",
      "slug": "astro-018-release"
    },
    {
      "depth": 2,
      "text": "Responsive partial hydration",
      "slug": "responsive-partial-hydration"
    }
    /* ... */
  ],
  rawContent: () => "# Astro 0.18 Release\nA little over a month ago, the first public beta [...]",
  compiledContent: () => "<h1>Astro 0.18 Release</h1>\n<p>A little over a month ago, the first public beta [...]</p>",
}

Example: Using one Layout for .md, .mdx, and .astro files

Section titled Example: Using one Layout for .md, .mdx, and .astro files

A single Astro layout can be written to receive the frontmatter object from .md (or .markdown etc.) and .mdx files, as well as any named props passed from .astro files.

In the example below, the layout will display the page title either from an Astro component passing a title attribute or from a frontmatter YAML title property:

src/components/MyLayout.astro
---
const { title } = Astro.props.frontmatter || Astro.props;
---
<html>
  <head></head>
  <body>
    <h1>{title}</h1>
    <slot />
  </body>
</html>

Astro will add autogenerated ids to all headings in Markdown files automatically using github-slugger. But, if a custom id is specified, it won’t be overridden.

These ids will be added after all the other plugins are executed, so if you have a plugin like rehype-toc that needs ids, you should add your own slugging plugin (like rehype-slug).

draft: true is an optional frontmatter value that will mark an individual .md (or .markdown etc.) page or post as “unpublished.” By default, this page will be excluded from the site build.

Markdown pages without the draft property or those with draft: false are unaffected and will be included in the final build.

src/pages/post/blog-post.md
---
layout: ../../layouts/BaseLayout.astro
title: My Blog Post
draft: true
---

This is my in-progress blog post.

No page will be built for this post.

To build and publish this post:

- update the frontmatter to `draft: false` or
- remove the `draft` property entirely.

To exclude draft posts from being included in a post archive, or list of most recent posts, you can filter the results returned by your Astro.glob().

const posts = await Astro.glob('../pages/post/*.md');
const nonDraftPosts = posts.filter((post) => !post.frontmatter.draft);

⚙️ To enable building draft pages:

Add drafts: true to markdown in astro.config.mjs

astro.config.mjs
export default defineConfig({
  markdown: {
    drafts: true,
  },
});

Certain characters have a special meaning in Markdown and MDX. You may need to use a different syntax if you want to display them. To do this, you can use HTML entities for these characters instead.

For example, to prevent < being interpreted as the beginning of an HTML element, write &lt;. Or, to prevent { being interpreted as the beginning of a JavaScript expression in MDX, write &lcub;.

Please install the official @astrojs/mdx integration to use:

See the migration guide for help converting your existing Astro .md ( or .markdown etc.) files to .mdx.

Astro includes full support for MDX with the official @astrojs/mdx integration. See the MDX integration guide for more information on this integration, which supports the deprecated features from the previous section and enhances your Markdown authoring.

With the @astrojs/mdx integration, you can use variables and JSX expressions in MDX (.mdx) files.

With the @astrojs/mdx integration, you can use Astro or UI framework components in MDX (.mdx) files just as you would use them in any other Astro component.

Don’t forget to include a client:directive if necessary!

src/pages/about.mdx
---
layout: ../layouts/BaseLayout.astro
title: About me
---
import Button from '../components/Button.astro';
import ReactCounter from '../components/ReactCounter.jsx';

I live on **Mars** but feel free to <Button title="Contact me" />.

Here is my counter component, working in MDX:

<ReactCounter client:load />

You can import Markdown and MDX files directly into your Astro files! You can import one specific page with import or multiple pages with Astro.glob().

src/pages/index.astro
---
// Import some markdown. Dynamic import() is also supported!
import * as greatPost from '../pages/post/great-post.md';

// Also, you can import multiple files with Astro.glob
const posts = await Astro.glob('../pages/post/*.md');
---

A Great Post: <a href={greatPost.url}>{greatPost.frontmatter.title}</a>

<ul>
  {posts.map(post => <li>{post.frontmatter.title}</li>)}
</ul>

You can optionally provide a type for the frontmatter variable using a TypeScript generic:

src/pages/index.astro
---
interface Frontmatter {
  title: string;
  description?: string;
}
const posts = await Astro.glob<Frontmatter>('../pages/post/*.md');
---

<ul>
  {posts.map(post => <li>{post.frontmatter.title}</li>)}
  <!-- post.frontmatter.title will be `string`! -->
</ul>

Each Markdown file exports the following properties.

Contains any data specified in this file’s YAML frontmatter.

The absolute path of this file (e.g. /home/user/projects/.../file.md).

If it’s a page, URL of the page (e.g. /en/guides/markdown-content).

An async function that returns the headings in the Markdown file. The response follows this type:

{ depth: number; slug: string; text: string }[]

A function that returns the raw content of the Markdown file (excluding the frontmatter block) as a string.

A function that returns the parsed HTML document as a string. Note this does not include layouts configured in your frontmatter! Only the markdown document itself will be returned as HTML.

A component that returns the full rendered contents of the Markdown file. Here is an example:

src/pages/content.astro
---
import {Content as PromoBanner} from '../components/promoBanner.md';
---

<h2>Today's promo</h2>
<PromoBanner />

When using getStaticPaths and Astro.glob() to generate pages from Markdown files, you can pass the <Content/> component through the page’s props. You can then retrieve the component from Astro.props and render it in your template.

src/pages/[slug].astro
---
export async function getStaticPaths() {
  const posts = await Astro.glob('../posts/**/*.md')

  return posts.map(post => ({
    params: { 
      slug: post.frontmatter.slug 
    },
    props: {
      post
    },
  }))
}

const { Content } = Astro.props.post
---
<article>
  <Content/>
</article>

Markdown support in Astro is powered by remark, a powerful parsing and processing tool with an active ecosystem. Other Markdown parsers like Pandoc and markdown-it are not currently supported.

You can customize how remark parses your Markdown in astro.config.mjs. See the reference documentation for full configuration details or follow our guides below on how to add remark plugins and customize syntax highlighting.

Astro supports third-party remark and rehype plugins for Markdown. These plugins allow you to extend your Markdown with new capabilities, like auto-generating a table of contents, applying accessible emoji labels, and more. We encourage you to browse awesome-remark and awesome-rehype for popular plugins!

This example applies the remark-toc and rehype-accessible-emojis plugins. See each project’s README for installation instructions.

astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkToc from 'remark-toc';
import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis';

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkToc],
    rehypePlugins: [rehypeAccessibleEmojis],
    // Preserve Astro's default plugins: GitHub-flavored Markdown and Smartypants
    // default: false
    extendDefaultPlugins: true,
  },
}

Markdown content is transformed into HTML through remark-rehype which has a number of options.

You can use remark-rehype options in your config file like so:

astro.config.mjs
export default {
  markdown: {
    remarkRehype: {
      footnoteLabel: 'Catatan kaki',
      footnoteBackLabel: 'Kembali ke konten',
    },
  },
};

You may want to add frontmatter properties to your Markdown files programmatically. By using a remark or rehype plugin, you can generate these properties based on a file’s contents.

You can append to the data.astro.frontmatter property from your plugin’s file argument like so:

example-remark-plugin.mjs
export function exampleRemarkPlugin() {
  // All remark and rehype plugins return a separate function
  return function (tree, file) {
    file.data.astro.frontmatter.customProperty = 'Generated property';
  }
}

After applying this plugin to your markdown config:

astro.config.mjs
import { exampleRemarkPlugin } from './example-remark-plugin.mjs';

export default {
  markdown: {
    remarkPlugins: [exampleRemarkPlugin],
    extendDefaultPlugins: true,
  },
};

…every Markdown file will have customProperty in its frontmatter! This is available when importing your markdown and from the Astro.props.frontmatter property in your layouts.

Example: calculate reading time

Section titled Example: calculate reading time

You can use a remark plugin to add a reading time to your frontmatter. We recommend two helper packages:

npm install reading-time mdast-util-to-string

We can apply these packages to a remark plugin like so:

remark-reading-time.mjs
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';

export function remarkReadingTime() {
  return function (tree, { data }) {
    const textOnPage = toString(tree);
    const readingTime = getReadingTime(textOnPage);
    // readingTime.text will give us minutes read as a friendly string,
    // i.e. "3 min read"
    data.astro.frontmatter.minutesRead = readingTime.text;
  };
}

Once you apply this plugin to your config:

astro.config.mjs
import { remarkReadingTime } from './remark-reading-time.mjs';

export default {
  markdown: {
    remarkPlugins: [remarkReadingTime],
    extendDefaultPlugins: true,
  },
};

…all Markdown documents will have a calculated minutesRead. You can use this to include an “X min read” banner in a markdown layout, for instance:

src/layouts/BlogLayout.astro
---
const { minutesRead } = Astro.props.frontmatter;
---

<html>
  <head>...</head>
  <body>
    <p>{minutesRead}</p>
    <slot />
  </body>
</html>

Astro comes with built-in support for Shiki and Prism. This provides instant syntax highlighting for:

Shiki is enabled by default, preconfigured with the github-dark theme. The compiled output will be limited to inline styles without any extraneous CSS classes, stylesheets, or client-side JS.

If you opt to use Prism, we will apply Prism’s CSS classes instead. Note that you need to bring your own CSS stylesheet for syntax highlighting to appear! See the Prism configuration section for more details.

Shiki is our default syntax highlighter. If you’d like to switch to 'prism' or disable syntax highlighting entirely, you can use the markdown config object:

astro.config.mjs
export default {
  markdown: {
    // Can be 'shiki' (default), 'prism' or false to disable highlighting
    syntaxHighlight: 'prism',
  },
};

When using Shiki, you’ll configure all options via the shikiConfig object like so:

astro.config.mjs
export default {
  markdown: {
    shikiConfig: {
      // Choose from Shiki's built-in themes (or add your own)
      // https://github.com/shikijs/shiki/blob/main/docs/themes.md
      theme: 'dracula',
      // Add custom languages
      // Note: Shiki has countless langs built-in, including .astro!
      // https://github.com/shikijs/shiki/blob/main/docs/languages.md
      langs: [],
      // Enable word wrap to prevent horizontal scrolling
      wrap: true,
    },
  },
};

Instead of using one of Shiki’s predefined themes, you can import a custom theme from a local file.

astro.config.mjs
import { defineConfig } from 'astro/config';
import customTheme from './my-shiki-theme.json';

export default defineConfig({
  markdown: {
    shikiConfig: { theme: customTheme },
  },
});

We also suggest diving into their theme documentation to explore more about themes, light vs dark mode toggles, or styling via CSS variables.

When using Prism, you’ll need to add a stylesheet to your project for syntax highlighting. If you’re just getting started and prefer to use Prism over Shiki, we suggest:

  1. Setting syntaxHighlight: 'prism' from your @astrojs/markdown-remark config.
  2. Choosing a premade stylesheet from the available Prism Themes.
  3. Adding this stylesheet to your project’s public/ directory.
  4. Loading this into your page’s <head> via a <link> tag.

You can also visit the list of languages supported by Prism for options and usage.

Astro was primarily designed for local Markdown files that could be saved inside of your project directory. However, there may be certain cases where you need to fetch Markdown from a remote source. For example, you may need to fetch and render Markdown from a remote API when you build your website (or when a user makes a request to your website, when using SSR).

Astro does not include built-in support for remote Markdown! To fetch remote Markdown and render it to HTML, you will need to install and configure your own Markdown parser from npm. This will not inherit from any of Astro’s built-in Markdown and MDX settings that you have configured. Be sure that you understand these limitations before implementing this in your project.

src/pages/remote-example.astro
---
// Example: Fetch Markdown from a remote API 
// and render it to HTML, at runtime.
// Using "marked" (https://github.com/markedjs/marked)
import { marked } from 'marked';
const response = await fetch('https://raw.githubusercontent.com/wiki/adam-p/markdown-here/Markdown-Cheatsheet.md');
const markdown = await response.text();
const content = marked.parse(markdown);
---
<article set:html={content} />