withastro/astro · error · Error

`markdown.remarkPlugins`, `markdown.rehypePlugins`, and…

Error message

`markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` run on the `unified` processor from `@astrojs/markdown-remark`, which is no longer installed by default now that Sätteri is the default Markdown processor. Install it with:
  npm install @astrojs/markdown-remark

What it means

In Astro versions where Sätteri is the default Markdown processor, `@astrojs/markdown-remark` is no longer installed as a default dependency. If your config sets `markdown.remarkPlugins`, `markdown.rehypePlugins`, or `markdown.remarkRehype`, config validation dynamically imports `@astrojs/markdown-remark` to run those plugins on the `unified` processor; when the import fails, this plain Error is thrown with the exact install command.

Solutions

  1. Install the package: `npm install @astrojs/markdown-remark` (message prints this command).
  2. Verify it is in the project's own `dependencies`, not merely hoisted transitively.
  3. If you no longer need unified plugins, remove the legacy `markdown.*` keys and use Sätteri's equivalents instead.

Example fix

# before — throws: @astrojs/markdown-remark is no longer a default dependency
export default defineConfig({
  markdown: { remarkPlugins: [remarkToc] },
});

# after
npm install @astrojs/markdown-remark remark-toc

export default defineConfig({
  markdown: { remarkPlugins: [remarkToc] },
});
Defensive patterns

Strategy: validation

Validate before calling

// CI gate: legacy markdown plugins require @astrojs/markdown-remark
try {
  await import('@astrojs/markdown-remark');
} catch {
  console.error('markdown.remarkPlugins/rehypePlugins need: npm install @astrojs/markdown-remark');
  process.exit(1);
}

Type guard

// True when the config uses unified-era markdown options
function usesLegacyMarkdownPlugins(config: {
  markdown?: { remarkPlugins?: unknown[]; rehypePlugins?: unknown[]; remarkRehype?: object };
}): boolean {
  const md = config.markdown ?? {};
  return (
    (md.remarkPlugins?.length ?? 0) > 0 ||
    (md.rehypePlugins?.length ?? 0) > 0 ||
    Object.keys(md.remarkRehype ?? {}).length > 0
  );
}

Prevention

When it happens

Trigger: Any astro.config containing a non-empty `remarkPlugins`, `rehypePlugins`, or `remarkRehype` under `markdown`, on a project where `@astrojs/markdown-remark` is not an installed dependency (removed during upgrade, or never directly depended on).

Common situations: Upgrading an older project that used remark/rehype plugins (TOC, reading-time, syntax highlighters) to a Sätteri-default Astro; monorepo cleanup dropping the previously transitive package.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/714b485d74ab81fa. Report an issue: GitHub.

Appendix: source

Thrown at packages/astro/src/core/config/validate.ts:82

	if (!md) return;

	const remarkPlugins = md.remarkPlugins ?? [];
	const rehypePlugins = md.rehypePlugins ?? [];
	const remarkRehype = md.remarkRehype ?? {};
	if (
		remarkPlugins.length === 0 &&
		rehypePlugins.length === 0 &&
		Object.keys(remarkRehype).length === 0
	) {
		return;
	}

	let unified: typeof import('@astrojs/markdown-remark').unified;
	let isUnifiedProcessor: typeof import('@astrojs/markdown-remark').isUnifiedProcessor;
	try {
		({ unified, isUnifiedProcessor } = await import('@astrojs/markdown-remark'));
	} catch {
		throw new Error(
			'`markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` run on the `unified` processor from `@astrojs/markdown-remark`, which is no longer installed by default now that Sätteri is the default Markdown processor. Install it with:\n  npm install @astrojs/markdown-remark',
		);
	}

	const current = md.processor;
	if (!current || isUnifiedProcessor(current)) {
		// `createRenderer` reads `.options.*` at render time, so in-place mutation propagates.
		const target = (current ?? (md.processor = unified())) as ReturnType<typeof unified>;
		const counts = migratedLegacyPluginCounts.get(target.options) ?? { remark: 0, rehype: 0 };
		// Only push entries past what we've already folded; mergeConfig appends, so they sit at the tail.
		if (remarkPlugins.length > counts.remark) {
			target.options.remarkPlugins.push(...remarkPlugins.slice(counts.remark));
		}
		if (rehypePlugins.length > counts.rehype) {
			target.options.rehypePlugins.push(...rehypePlugins.slice(counts.rehype));
		}
		// `remarkRehype` is an object, so Object.assign is idempotent for unchanged keys
		// and absorbs any new keys integrations add.

View on GitHub (pinned to 52e6c34790)