{"record":{"id":"6668753bd0f97aaf","repo":"withastro/astro","slug":"unexpected-problem-adding-heading-ids-to-markdoc-f","errorCode":null,"errorMessage":"Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?","messagePattern":"Unexpected problem adding heading IDs to Markdoc file\\. Did you modify the `ctx\\.headingSlugger` property in your Markdoc config\\?","errorType":"exception","errorClass":"MarkdocError","httpStatus":null,"severity":"error","filePath":"packages/integrations/markdoc/src/heading-ids.ts","lineNumber":42,"sourceCode":"}\n\n/*\n\tExpose standalone node for users to import in their config.\n\tAllows users to apply a custom `render: AstroComponent`\n\tand spread our default heading attributes.\n*/\nexport const heading: Schema = {\n\tchildren: ['inline'],\n\tattributes: {\n\t\tid: { type: String },\n\t\tlevel: { type: Number, required: true, default: 1 },\n\t},\n\ttransform(node, config: HeadingIdConfig) {\n\t\tconst { level, ...attributes } = node.transformAttributes(config);\n\t\tconst children = node.transformChildren(config);\n\n\t\tif (!config.ctx?.headingSlugger) {\n\t\t\tthrow new MarkdocError({\n\t\t\t\tmessage:\n\t\t\t\t\t'Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?',\n\t\t\t});\n\t\t}\n\t\tconst slug = getSlug(attributes, children, config.ctx.headingSlugger);\n\n\t\tconst render = config.nodes?.heading?.render ?? `h${level}`;\n\n\t\tconst tagProps =\n\t\t\t// For components, pass down `level` as a prop,\n\t\t\t// alongside `__collectHeading` for our `headings` collector.\n\t\t\t// Avoid accidentally rendering `level` as an HTML attribute otherwise!\n\t\t\ttypeof render === 'string'\n\t\t\t\t? { ...attributes, id: slug }\n\t\t\t\t: { ...attributes, id: slug, __collectHeading: true, level };\n\n\t\treturn new Markdoc.Tag(render, tagProps, children);\n\t},","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/withastro/astro/blob/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/integrations/markdoc/src/heading-ids.ts#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — markdoc.config.mjs clobbers ctx\nexport default {\n  ctx: { headingSlugger: mySlugger },\n};\n\n// after — let the integration inject ctx; only map render\nexport default {\n  nodes: { heading: { render: 'MyHeading' } },\n};","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"import type { Config, NodeType } from '@markdoc/markdoc';\ninterface HeadingCtx { headingSlugger?: { slug(text: string): string } }\n// Guard before invoking a transform pipeline that includes the heading schema\nfunction hasHeadingContext(config: Config): boolean {\n  const ctx = (config as Config & { ctx?: HeadingCtx }).ctx;\n  return typeof ctx?.headingSlugger?.slug === 'function';\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["markdoc","headings","render-context","config"],"backgroundTag":"missing-render-context","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}