How to opt into the unified remark/rehype pipeline in Astro 7
When I added a reading-time badge and a code-block toolbar to this blog, both features were built as plugins — one remark plugin and one rehype plugin. But writing the plugins was only half the job. I also had to make sure Astro actually runs them, and in Astro 7 that takes one extra step compared to older versions. This post explains that step: opting into the unified() processor so your custom plugins run.
Astro 7’s default processor
Out of the box, Astro 7 processes Markdown with a new default processor (in this project’s config it’s referred to as the “Sätteri processor”). It’s fast and handles the common cases, but the tradeoff is that it doesn’t automatically run an arbitrary list of your own remark/rehype plugins the way the classic pipeline did.
If you just need standard Markdown, the default is fine and you don’t have to touch anything. But the moment you want a custom transform — computing reading time, wrapping code blocks, rendering diagrams — you need the full unified pipeline, because that’s the ecosystem remark and rehype plugins are written for.
remark, rehype, and unified in one breath
Quick vocabulary, since the names blur together:
- unified is the umbrella engine that runs a chain of plugins over a document.
- remark plugins operate on the Markdown tree (mdast) — good for content-level logic like word counting.
- rehype plugins operate on the HTML tree (hast) — good for reshaping the output markup, like wrapping code blocks.
A document flows through remark first (Markdown side), then gets converted to HTML and flows through rehype (HTML side).
flowchart LR A[.mdx source] --> B[remark plugins - mdast] B --> C[convert to HTML tree] C --> D[rehype plugins - hast] D --> E[final HTML page]
Overriding the processor
Here’s the real Markdown block from this blog’s config. This is the whole opt-in:
// blog/astro.config.mjs
import { unified } from "@astrojs/markdown-remark"
import { remarkReadingTime } from "./src/utils/remark-reading-time.mjs"
import { remarkMermaid } from "./src/utils/remark-mermaid.mjs"
import { rehypeCodeToolbar } from "./src/utils/rehype-code-toolbar.mjs"
export default defineConfig({
// ...
markdown: {
// Astro 7 defaults to the Sätteri processor; opt into the remark/rehype
// pipeline so our reading-time remark plugin runs.
processor: unified({
remarkPlugins: [remarkReadingTime, remarkMermaid],
rehypePlugins: [rehypeCodeToolbar],
}),
shikiConfig: {
theme: "github-dark",
wrap: true,
},
},
})The key line is processor: unified({ ... }). By setting markdown.processor explicitly, we tell Astro: “don’t use your default — run everything through unified with exactly these plugins.” Note that unified is imported from @astrojs/markdown-remark, Astro’s own package, so it’s configured to slot into Astro’s build correctly.
How the three pieces fit together
Inside that unified() call, the two plugin arrays map directly onto the two phases from the diagram:
remarkPluginsruns on the Markdown tree.remarkReadingTimereads the prose and writes a “min read” value onto the frontmatter;remarkMermaidhandles diagram fences. Order matters here — plugins run top to bottom.rehypePluginsruns on the HTML tree, after conversion.rehypeCodeToolbarwraps each Shiki<pre>in a<figure>with copy and fullscreen buttons.
And where does shikiConfig fit? Shiki is Astro’s built-in syntax highlighter, which you theme through shikiConfig. It runs as part of this same Markdown processing, turning fenced code into styled <pre class="astro-code"> markup:
shikiConfig: {
theme: "github-dark",
wrap: true,
}theme picks the color scheme; wrap: true makes long lines wrap instead of scroll horizontally. Because Shiki produces the <pre> that our rehype plugin later wraps, the two cooperate: Shiki highlights, then rehypeCodeToolbar decorates.
A gotcha: pass functions, don’t call them
Notice each plugin in the arrays is written as a bare reference — remarkReadingTime, not remarkReadingTime(). unified calls the plugin for you and passes it options. If you accidentally invoke it yourself, you’ll pass the result (a transformer function) where unified expects the plugin, and things break in confusing ways. When a plugin needs options, you wrap it in an array: [myPlugin, { some: "option" }].
Sanity-checking that it ran
The quickest confirmation is a feature that only exists because a plugin ran. For this blog, that’s the reading-time value — it shows up on remarkPluginFrontmatter after rendering a post:
---
const { Content, remarkPluginFrontmatter } = await post.render()
---
<span>{remarkPluginFrontmatter.minutesRead}</span>If minutesRead is populated, the remarkPlugins array is wired up correctly. If it’s undefined, you’re almost certainly still on the default processor and haven’t set markdown.processor yet.
Takeaways
- Astro 7 uses a new default Markdown processor that doesn’t run arbitrary custom plugins for you.
- Set
markdown.processor: unified({ ... })(imported from@astrojs/markdown-remark) to opt into the full remark/rehype pipeline. remarkPluginstransform the Markdown tree;rehypePluginstransform the HTML tree; they run in that order, top to bottom.shikiConfigconfigures the built-in highlighter that produces the<pre>your rehype plugins can then decorate.- Register plugins by reference, and use
[plugin, options]when you need to pass options.