Ports of the most-used unified (remark/rehype) plugins to Sätteri, the Rust Markdown/MDX engine behind Astro 7.
Sätteri does not run remark or rehype plugins — it has its own AST, parser and serializer. Upgrading for the speed means leaving your plugin stack behind. These are behaviour-compatible replacements, each tested against the output of the plugin it replaces.
| Package | npm | Replaces | What it does |
|---|---|---|---|
satteri-slug |
rehype-slug |
Adds id to every heading, via the same github-slugger |
|
satteri-autolink-headings |
rehype-autolink-headings |
Adds anchor links to headings | |
satteri-katex |
rehype-katex |
Renders math with KaTeX | |
satteri-sanitize |
rehype-sanitize |
Strips unsafe HTML, attributes and URLs | |
satteri-breaks |
remark-breaks |
Turns single newlines into <br> |
|
satteri-mathjax |
rehype-mathjax |
Renders math with MathJax | |
satteri-github |
remark-github |
Autolinks issues, mentions and SHAs | |
satteri-mdx-frontmatter |
remark-mdx-frontmatter |
Exposes MDX frontmatter as exports | |
satteri-validate-links |
remark-validate-links |
Finds links to missing files and headings |
Live example, built by Astro 7 in CI: https://ashish-codejourney.github.io/satteri-plugins (source)
Much of what needed a plugin under unified is built into Sätteri. Check here before looking for a port.
| Plugin | Replacement |
|---|---|
remark-gfm |
features.gfm (on by default) — tables, footnotes, strikethrough, tasklists, autolinks |
remark-frontmatter |
features.frontmatter (on by default) — YAML and TOML |
remark-math |
features.math — parsing only, pair with satteri-katex to render |
remark-directive |
features.directive |
remark-smartypants |
features.smartPunctuation |
remark-parse, remark-stringify, remark-rehype, rehype-parse, rehype-stringify |
The pipeline itself |
remark-wiki-link |
features.wikilinks |
remark-sup, remark-sub |
features.superscript, features.subscript |
remark-definition-list |
features.definitionList |
Custom {#id} heading syntax |
features.headingAttributes |
Sätteri does not generate heading ids on its own — that is what
satteri-slug is for.
| Plugin | Port |
|---|---|
rehype-slug |
satteri-slug |
rehype-autolink-headings |
satteri-autolink-headings |
rehype-katex |
satteri-katex |
rehype-sanitize |
satteri-sanitize |
remark-breaks |
satteri-breaks |
rehype-mathjax |
satteri-mathjax |
remark-github |
satteri-github |
remark-mdx-frontmatter |
satteri-mdx-frontmatter |
| Plugin | Use instead |
|---|---|
rehype-external-links |
satteri-external-links |
remark-emoji |
satteri-emoji |
remark-toc |
@bhdouglass/satteri-toc, pretty-toc |
rehype-mermaid |
satteri-beautiful-mermaid, @xingwangzhe/satteri-mermaid |
rehype-github-alerts, remark-github-blockquote-alert |
satteri-callouts |
| Code highlighting | satteri-expressive-code |
| MDX auto-imports | @bhdouglass/satteri-auto-imports, @xsynaptic/satteri-auto-import |
A unified compatibility shim is the one left worth building. It is wanted, but Sätteri's one-pass
model means a documented support matrix rather than a drop-in adapter. Contributions welcome.
rehype-highlight — Astro 7 already highlights code blocks with Shiki, with no plugin at all. Port
this only if you have existing highlight.js CSS themes to keep. For richer rendering, see
satteri-expressive-code or
Treelight.
Sätteri passes raw HTML through unparsed, so Markdown from an untrusted source is an XSS vector by
default. If any of your content is user-supplied, run
satteri-sanitize last in your hastPlugins.
Every package is published from CI by npm trusted publishing over OIDC. There is no long-lived npm token in this repository or in any maintainer's shell, so there is no token to steal.
Releases carry SLSA provenance attestations linking the tarball to the commit and workflow that built it. Verify them yourself:
npm audit signaturesThe first 0.1.0 of satteri-breaks, satteri-mathjax, satteri-github and
satteri-mdx-frontmatter was published by hand to bootstrap trusted publishing, so those four
versions carry no attestation. Their next release does.
To report a vulnerability, see SECURITY.md.
Two things about Astro's Sätteri processor are worth knowing before you wire anything up, because both are invisible until your output is wrong. Astro composes the plugin list as:
[ syntax highlighter ] → [ your hastPlugins ] → [ image marker ] → [ heading ids ]
- Pass
satteri-katextomdastPlugins, nothastPlugins. The highlighter runs first and, on HAST, display math is still a<pre><code>— so it gets highlighted as aplaintextcode block and never reaches a HAST math plugin. satteri-slugis a prerequisite forsatteri-autolink-headings. Astro assigns heading ids after your plugins, so withoutsatteri-slugthe headings have noidyet and every anchor is skipped.satteri()accepts onlymdastPlugins,hastPluginsandfeatures. Anything else you pass it —shikiConfig,gfm,smartypants— is silently dropped. Those belong one level up, onmarkdown, alongsideprocessor.- A returned plugin definition is reused across every compile.
MdastPluginInputalso accepts a factory (() => defineMdastPlugin(...)), and that is the only way to get per-document state. Any plugin holding a counter, a dedupe set or an emit-once flag must return a factory or it will leak state between pages, silently. There is also no root or end-of-document hook, so a plugin cannot append output after traversal. ctx.report()diagnostics never reach you. They are collected per visitor and readable only throughctx.getDiagnostics()inside a plugin.markdownToHtmlreturns{ html, frontmatter, data }with no diagnostics field, and Astro's processor never reads them. A plugin that wants to surface warnings has to write them intoctx.data, which is returned asresult.data, or print them itself.
export default defineConfig({
markdown: {
shikiConfig: { themes: { light: "github-light", dark: "github-dark" } },
processor: satteri({
features: { math: true },
mdastPlugins: [satteriKatex()],
hastPlugins: [satteriSlug(), satteriAutolinkHeadings()],
}),
},
});Every package follows the same method:
- Characterise. Run the original plugin over a fixture corpus and capture its HTML. That output is the specification — not the original's source code.
- Test first. Each behaviour gets a failing test asserting the captured HTML before any implementation exists.
- Mutation-test. Deliberately break the implementation and confirm a test catches it. Anything that survives is a missing test or dead code, and gets fixed either way.
- Verify in a real build.
pnpm test:e2ebuildsexamples/astro7-sitewith Astro 7 and asserts on the HTML on disk. Both Astro ordering problems above were found this way, after the unit tests were already green. - Document divergences. Where Sätteri's model makes exact parity impossible, each README says so under Differences from ….
See CONTRIBUTING.md. Ports are held to the method above — a PR that skips the characterisation or mutation steps will be asked to redo them.
pnpm install
pnpm test # unit tests
pnpm -r typecheck
pnpm -r build
pnpm test:e2e # builds the example site with Astro 7 and asserts on its HTMLMIT © Ashish Vaghela