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
- Remove any `ctx` assignment/override in your markdoc.config; treat the injected ctx as read-only.
- Update @astrojs/markdoc (and astro) so config surface matches what the heading schema expects.
- If you need custom heading behavior, extend via `config.nodes.heading` render mapping instead of replacing ctx.
- 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
- Never assign or replace `ctx` in markdoc.config — treat integration-injected context as opaque.
- Extend heading behavior via `nodes.heading.render`, not by swapping the context.
- After upgrading @astrojs/markdoc, review any custom config for ctx-touching leftovers.
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
- Apps must be an object with an id, a name and an entrypoint.
- [astro] deprecated. Move onto your processor instead (e.g…
- `Astro.session` was accessed but no session storage is…
- Auto-generating collections for folders in "src/content/"…
- not applied.
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)