Skip to content
Astro as markdown 4 min read

Remark & Rehype Plugins

Astro builds Markdown on top of the unified ecosystem, which means you are not limited to the syntax that ships out of the box. With remark plugins you transform the parsed Markdown syntax tree, and with rehype plugins you transform the HTML tree it produces. Together they let you add features like automatic heading anchors, reading-time estimates, callout boxes, and tables of contents without writing custom rendering code. Because all of this runs at build time, the enhancements add zero client-side JavaScript to your pages.

How the pipeline works

Every Markdown and MDX file passes through a two-stage pipeline. First, the raw text is parsed into an mdast (Markdown AST), where remark plugins run. The tree is then converted into an hast (HTML AST), where rehype plugins run before the final HTML is serialized.

Markdown text
  → remark-parse → mdast → [remark plugins]
  → remark-rehype → hast → [rehype plugins]
  → HTML

Knowing which stage you need matters: anything that works with Markdown-level concepts (headings, links, footnotes) belongs in remark, while anything that manipulates the resulting elements (adding id attributes, wrapping nodes, injecting <svg> icons) belongs in rehype.

Configuring plugins

Plugins are registered in astro.config.mjs under the markdown key. Install the plugin from npm, import it, and add it to remarkPlugins or rehypePlugins.

npm install remark-toc rehype-autolink-headings rehype-slug
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkToc],
    rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
  },
});

Order matters. rehype-autolink-headings needs an id on each heading, so rehype-slug must run before it. List plugins in the order they should execute.

When a plugin takes options, pass it as a [plugin, options] tuple instead of a bare reference.

markdown: {
  remarkPlugins: [[remarkToc, { heading: 'contents', maxDepth: 3 }]],
}

Default plugins and extendDefaultPlugins

Astro ships with GitHub Flavored Markdown (remark-gfm) and SmartyPants enabled by default, giving you tables, strikethrough, autolinked URLs, and smart quotes. When you supply your own remarkPlugins array, Astro keeps those defaults and merges yours in. You can opt out of either default explicitly.

OptionTypeEffect
gfmbooleanToggle GitHub Flavored Markdown (default true)
smartypantsbooleanToggle smart punctuation (default true)
remarkPluginsarrayCustom remark plugins, merged with defaults
rehypePluginsarrayCustom rehype plugins, merged with defaults
remarkRehypeobjectOptions forwarded to remark-rehype

A common request is clickable anchor links next to every heading. The combination of rehype-slug (generates id attributes) and rehype-autolink-headings (wraps or appends a link) handles this entirely at build time.

// astro.config.mjs
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

export default defineConfig({
  markdown: {
    rehypePlugins: [
      'rehype-slug',
      [
        rehypeAutolinkHeadings,
        { behavior: 'append', content: { type: 'text', value: ' #' } },
      ],
    ],
  },
});

A heading like ## Configuration then renders as anchored, linkable HTML:

Output:

<h2 id="configuration">Configuration<a href="#configuration"> #</a></h2>

Writing a custom plugin: reading time

You don’t need a published package for project-specific transforms. A remark plugin is just a function returning a transformer that walks the tree. This one estimates reading time and writes it into the file’s frontmatter so layouts can display it.

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

export function remarkReadingTime() {
  return function (tree, file) {
    const text = toString(tree);
    const reading = getReadingTime(text);
    // astroData.frontmatter is exposed to layouts and collections
    file.data.astro.frontmatter.minutesRead = reading.text;
  };
}
// astro.config.mjs
import { remarkReadingTime } from './plugins/remark-reading-time.mjs';

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkReadingTime],
  },
});

The injected value is now available on frontmatter in a layout or on entry.data for collection entries:

---
const { frontmatter } = Astro.props;
---
<p>{frontmatter.minutesRead}</p>

Output:

<p>4 min read</p>

MDX uses the same config

The MDX integration inherits everything you set under markdown. If you need plugins that apply only to .mdx files, pass them to the integration directly; by default MDX also runs the shared Markdown plugins unless you set extendMarkdownConfig: false.

import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx({ remarkPlugins: [/* mdx-only plugins */] })],
});

Best Practices

  • Choose the right stage: transform Markdown concepts in remark, transform HTML elements in rehype.
  • Order your rehypePlugins deliberately — rehype-slug must precede rehype-autolink-headings.
  • Prefer the built-in headings array for simple tables of contents; reach for remark-toc only when you want the TOC inline in the content.
  • Keep custom plugins small and pure functions of the tree, and colocate them in a plugins/ folder for clarity.
  • Pin plugin versions, since unified plugins occasionally change their default behavior or option names across major releases.
  • Remember these run at build time only, so they never add runtime JavaScript or slow the visitor’s page load.
Last updated June 14, 2026
Was this helpful?