withastro/astro · error · Error

[MDX] A remark or rehype plugin attempted to inject invalid

Error message

[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.

What it means

`rehype-apply-frontmatter-export` reads `vfile.data.astro.frontmatter` and, if missing or failing `isFrontmatterValid`, throws a plain `Error`. The plugin then emits `export const frontmatter = {...}` (and a layout import) into the MDX AST. The check guards against remark/rehype plugins that set `astro.frontmatter` to `null`/`undefined` or a non-JSON-serializable value.

Source

Thrown at packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts:23

import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';

// Passed metadata to help determine adding charset utf8 by default
declare module 'vfile' {
	interface DataMap {
		applyFrontmatterExport?: {
			srcDir?: URL;
		};
	}
}

const exportConstPartialTrueRe = /export\s+const\s+partial\s*=\s*true/;

export function rehypeApplyFrontmatterExport() {
	return function (tree: Root, vfile: VFile) {
		const frontmatter = vfile.data.astro?.frontmatter;
		if (!frontmatter || !isFrontmatterValid(frontmatter))
			throw new Error(
				// Copied from Astro core `errors-data`
				// TODO: find way to import error data from core
				'[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
			);
		const extraChildren: RootContent[] = [
			jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
		];
		if (frontmatter.layout) {
			extraChildren.unshift(
				jsToTreeNode(
					// NOTE: Use `__astro_*` import names to prevent conflicts with user code
					/** @see 'vite-plugin-markdown' for layout props reference */
					`\
import { jsx as __astro_layout_jsx__ } from 'astro/jsx-runtime';
import __astro_layout_component__ from ${JSON.stringify(frontmatter.layout)};

export default function ({ children }) {
	const { layout, ...content } = frontmatter;

View on GitHub (pinned to d081033d5f)

Solutions

  1. Ensure any remark/rehype plugin touching frontmatter sets `file.data.astro.frontmatter` to a plain JSON-serializable object (never null/undefined).
  2. Check plugin order in `remarkPlugins`/`rehypePlugins` so frontmatter-setting plugins run before this one and are not later overwritten.
  3. If frontmatter is optional, default it to `{}` instead of null.

Example fix

// before — plugin that breaks frontmatter
export function remarkPlugin() {
  return (tree, file) => {
    file.data.astro.frontmatter = null;
  };
}

// after
export function remarkPlugin() {
  return (tree, file) => {
    file.data.astro.frontmatter = { ...(file.data.astro.frontmatter ?? {}), ...computed };
  };
}
Defensive patterns

Strategy: validation

Validate before calling

function isFrontmatterValid(fm: unknown): boolean {
  return fm !== null && fm !== undefined && typeof fm === 'object' && !Array.isArray(fm);
}
// in plugin:
if (!isFrontmatterValid(file.data.astro?.frontmatter)) {
  file.data.astro = { ...(file.data.astro ?? {}), frontmatter: {} };
}

Type guard

function isValidFrontmatter(fm: unknown): fm is Record<string, unknown> {
  return fm !== null && typeof fm === 'object' && !Array.isArray(fm);
}

Prevention

When it happens

Trigger: A remark/rehype plugin assigns `file.data.astro.frontmatter = null` (or `undefined`, or a circular object). Frontmatter is computed asynchronously and not set by the time this plugin runs. A plugin replaces `file.data.astro` entirely.

Common situations: Custom remark plugin that processes frontmatter and forgets to keep a valid object. MDX frontmatter removed conditionally. Plugin ordering issue where a later plugin nulls frontmatter.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/606c5d14074d8c3e. Report an issue: GitHub.