Skip to content

ADR-062: Astro 7 Upgrade Retains the unified/remark Markdown Processor

Accepted (amended 2026-09-12: @astrojs/mdx is now 8.x (#401), which routes .mdx through the configured Markdown processor and warns when remark plugins would be dropped — the unified() setup below is the supported path. @astrojs/markdown-satteri is a peer dependency of both astro@7 and @astrojs/mdx@8, so it stays installed regardless of the processor choice; the “solely for mdx@7” note under Implementation Details is superseded by this. The decision and its revisit trigger are unchanged.)

Astro 6.4.8 carried three XSS advisories (two moderate, one low) whose patches ship only in the 7.x line: reflected XSS via unescaped View Transition params, XSS via unescaped spread attribute names, and XSS via unescaped transition:* values on hydrated islands. As shipped, the template is not exploitable — all transition:name bindings and spread attributes are content-collection- or config-derived — but the template’s job is to be a safe baseline, and every cloned project inherits these patterns with user data one binding away. A fourth advisory (esbuild, low) also cleared only via Astro 7’s move to Vite 8.

Astro 7 replaces the default Markdown processor: Sätteri supersedes the unified/remark pipeline, and @astrojs/markdown-remark is no longer installed by default. This repository wires two custom remark plugins through the Markdown config:

  • remarkValidateLinks — fails the build on broken internal /docs and /adr links
  • remarkSnippetIncludes — expands {% snippet "name" %} shortcodes from docs/snippets/

Both are build-time correctness gates, not decoration. The upgrade therefore forced a choice between porting them to Sätteri and retaining the unified pipeline explicitly.

  • Security: the astro advisories are the last remaining pnpm audit --production findings
  • Correctness gates must survive: losing link validation or snippet expansion silently would be worse than any processor benefit
  • Blast radius: the upgrade already spans three majors (astro 7, @astrojs/mdx 7, @astrojs/preact 6) plus the new Rust compiler’s strict HTML enforcement
  • Reversibility: a processor port is easy to do later; doing it mid-upgrade couples two risky changes in one PR

Option 1: Upgrade and retain unified/remark via @astrojs/markdown-remark

Section titled “Option 1: Upgrade and retain unified/remark via @astrojs/markdown-remark”

Description: Install @astrojs/markdown-remark explicitly and pass the existing plugins to unified({ remarkPlugins }) as markdown.processor.

Pros:

  • Both custom plugins run unchanged — zero porting risk
  • The security upgrade lands without coupling to a processor migration
  • The unified toolchain (remark, unist-util-visit, to-vfile) already ships in devDependencies for maintainer scripts, so no new dependency weight

Cons:

  • Foregoes Sätteri’s build-speed benefit for Markdown processing, for now
  • One more explicit dependency to track (@astrojs/markdown-remark)

Option 2: Port both plugins to Sätteri during the upgrade

Section titled “Option 2: Port both plugins to Sätteri during the upgrade”

Description: Rewrite remarkValidateLinks and remarkSnippetIncludes against Sätteri’s plugin API as part of the Astro 7 PR.

Pros:

  • Lands on the new default processor immediately; no legacy pipeline to carry

Cons:

  • Couples a security-motivated upgrade to a rewrite of two build-correctness gates
  • A subtle porting bug (e.g. link validation silently not running) passes CI green while removing the protection — the failure mode is invisible
  • Sätteri’s plugin API is new; patterns and pitfalls are not yet well established

Description: Defer the upgrade; accept or allowlist the advisories.

Pros:

  • Zero migration work

Cons:

  • The XSS advisories never clear — no 6.x backports were published
  • Advisory noise ships to every cloned project, undercutting the template’s purpose

We will go with Option 1 because it separates two independently risky changes. The security upgrade is urgent-ish and mechanical; the processor port is optional and subtle. The plugins’ liveness under the retained pipeline was verified by negative probe: a deliberately broken /adr/ link in a rendered blog post fails the Astro 7 build (exit 1), proving remarkValidateLinks executes.

astro.config.mjs
import { unified } from "@astrojs/markdown-remark";
export default defineConfig({
markdown: {
processor: unified({
remarkPlugins: [
[remarkValidateLinks, { /* ... */ }],
[remarkSnippetIncludes, { /* ... */ }],
],
}),
},
});

@astrojs/markdown-satteri is installed solely to satisfy @astrojs/mdx@7’s peer dependency; the MDX integration routes plugin execution through the configured unified processor.

  • pnpm audit --production reports zero advisories
  • Both build-correctness gates verified live under Astro 7
  • The new Rust compiler’s strict HTML validation passed with zero source changes, and warm build time dropped (~4.8s to ~2.7s for the build step)
  • Cloned projects inherit a clean audit baseline
  • The template carries the legacy Markdown pipeline until a Sätteri port happens; the port is deferred work with a real (if modest) build-speed upside
  • markdown.processor is now explicit config that upgrades must preserve — a future removal of @astrojs/markdown-remark support would force the port

Port the plugins to Sätteri (superseding the processor choice here, not this upgrade) when either: Astro deprecates @astrojs/markdown-remark, or Markdown build time becomes a measured bottleneck in the perf baselines.

  • Testable consequences:
    • TC-1: astro stays on the 7.x major with markdown.processor explicitly configured for the unified/remark pipeline.
    • TC-2: the snippet-include plugin survives a cold rebuild (the failure this upgrade surfaced).
  • Checks:
    • TC-1 → version-check in quality:ci (status: block, pre-existing gate)
    • TC-2 → scripts/src/__tests__/remark-snippet-includes.test.ts via test:unit (status: block, pre-existing gate)
  • Not machine-checkable: whether a future Astro major warrants re-evaluating the Sätteri port is the Revisit Trigger above.
  • Graduation log: (empty at creation; entries added when a check changes status)