withastro/astro · error · MarkdocError

Unexpected problem adding heading IDs to Markdoc file. Did…

Error message

Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?

What it means

The Markdoc integration injects a `ctx.headingSlugger` (a github-slugger instance) into the render config so the custom `heading` node schema can generate stable IDs and collect headings. If that ctx object or slugger is absent at transform time — typically because user Markdoc config code replaced `ctx` — the schema throws this invariant error. It signals the integration's render context was clobbered, not bad document content.

Solutions

  1. Remove any `ctx` assignment/override in your markdoc.config; treat the injected ctx as read-only.
  2. Update @astrojs/markdoc (and astro) so config surface matches what the heading schema expects.
  3. If you need custom heading behavior, extend via `config.nodes.heading` render mapping instead of replacing ctx.
  4. Re-run the dev server to confirm headings get IDs again.

Example fix

// before — markdoc.config.mjs clobbers ctx
export default {
  ctx: { headingSlugger: mySlugger },
};

// after — let the integration inject ctx; only map render
export default {
  nodes: { heading: { render: 'MyHeading' } },
};
Defensive patterns

Strategy: type-guard

Type guard

import type { Config, NodeType } from '@markdoc/markdoc';
interface HeadingCtx { headingSlugger?: { slug(text: string): string } }
// Guard before invoking a transform pipeline that includes the heading schema
function hasHeadingContext(config: Config): boolean {
  const ctx = (config as Config & { ctx?: HeadingCtx }).ctx;
  return typeof ctx?.headingSlugger?.slug === 'function';
}

Prevention

When it happens

Trigger: A markdoc.config that assigns/overwrites `config.ctx` (or a custom render pipeline invoking the heading schema outside the integration) so `config.ctx?.headingSlugger` is undefined when a heading transforms.

Common situations: Copying a markdoc.config from a vanilla Markdoc project that manipulates ctx; upgrading the integration while a stale custom config from an older version mutates shared state.

Related errors


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

Appendix: source

Thrown at packages/integrations/markdoc/src/heading-ids.ts:42

}

/*
	Expose standalone node for users to import in their config.
	Allows users to apply a custom `render: AstroComponent`
	and spread our default heading attributes.
*/
export const heading: Schema = {
	children: ['inline'],
	attributes: {
		id: { type: String },
		level: { type: Number, required: true, default: 1 },
	},
	transform(node, config: HeadingIdConfig) {
		const { level, ...attributes } = node.transformAttributes(config);
		const children = node.transformChildren(config);

		if (!config.ctx?.headingSlugger) {
			throw new MarkdocError({
				message:
					'Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?',
			});
		}
		const slug = getSlug(attributes, children, config.ctx.headingSlugger);

		const render = config.nodes?.heading?.render ?? `h${level}`;

		const tagProps =
			// For components, pass down `level` as a prop,
			// alongside `__collectHeading` for our `headings` collector.
			// Avoid accidentally rendering `level` as an HTML attribute otherwise!
			typeof render === 'string'
				? { ...attributes, id: slug }
				: { ...attributes, id: slug, __collectHeading: true, level };

		return new Markdoc.Tag(render, tagProps, children);
	},

View on GitHub (pinned to 52e6c34790)