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-headingsneeds anidon each heading, sorehype-slugmust 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.
| Option | Type | Effect |
|---|---|---|
gfm | boolean | Toggle GitHub Flavored Markdown (default true) |
smartypants | boolean | Toggle smart punctuation (default true) |
remarkPlugins | array | Custom remark plugins, merged with defaults |
rehypePlugins | array | Custom rehype plugins, merged with defaults |
remarkRehype | object | Options forwarded to remark-rehype |
Adding auto heading links
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
rehypePluginsdeliberately —rehype-slugmust precederehype-autolink-headings. - Prefer the built-in
headingsarray for simple tables of contents; reach forremark-toconly 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
behavioror 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.