diff --git a/.changeset/heading-ids-no-duplicates.md b/.changeset/heading-ids-no-duplicates.md new file mode 100644 index 000000000000..05a20cfa7978 --- /dev/null +++ b/.changeset/heading-ids-no-duplicates.md @@ -0,0 +1,5 @@ +--- +'@astrojs/markdown-satteri': patch +--- + +Fixes headings being listed twice in a page's `headings` metadata when an integration (such as Starlight) assigns heading IDs with its own heading pass before adding anchor links diff --git a/packages/markdown/satteri/src/satteri-processor.ts b/packages/markdown/satteri/src/satteri-processor.ts index 894c4dc34038..c2e206b36ddb 100644 --- a/packages/markdown/satteri/src/satteri-processor.ts +++ b/packages/markdown/satteri/src/satteri-processor.ts @@ -117,6 +117,8 @@ export function makeFragmentNode(html: string): HastNode { export function createHeadingIdsPlugin(): HastPluginDefinition { const slugger = new Slugger(); + // Collect headings in a separate array so we can make this idempotent + const headings: MarkdownHeading[] = []; return { name: 'heading-ids', element: { @@ -130,7 +132,8 @@ export function createHeadingIdsPlugin(): HastPluginDefinition { const existingId = node.properties?.id; const slug = typeof existingId === 'string' ? existingId : slugger.slug(text); const depth = Number.parseInt(node.tagName[1], 10); - astro?.headings.push({ depth, slug, text }); + headings.push({ depth, slug, text }); + if (astro) astro.headings = headings; if (typeof existingId !== 'string') { ctx.setProperty(node, 'id', slug); } diff --git a/packages/markdown/satteri/test/markdown.test.ts b/packages/markdown/satteri/test/markdown.test.ts index b4b4d2a3825b..19574fc0ed13 100644 --- a/packages/markdown/satteri/test/markdown.test.ts +++ b/packages/markdown/satteri/test/markdown.test.ts @@ -74,6 +74,17 @@ describe('satteri markdown', () => { assert.equal(seenId, 'hello-world'); }); + it('does not duplicate headings when `satteriHeadingIdsPlugin()` runs as a user plugin too', async () => { + const processor = await createSatteriMarkdownProcessor({ + hastPlugins: [satteriHeadingIdsPlugin()], + }); + const { metadata } = await processor.render('## Some text\n\n## Some text'); + assert.deepEqual(metadata.headings, [ + { depth: 2, slug: 'some-text', text: 'Some text' }, + { depth: 2, slug: 'some-text-1', text: 'Some text' }, + ]); + }); + it('respects heading IDs set by a user hast plugin in both DOM and `headings`', async () => { const setIdPlugin: HastPluginDefinition = { name: 'set-heading-id',