@mintlify/common 1.0.1031 → 1.0.1033

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,31 +4,59 @@ export function generateParamFieldId(name, count) {
4
4
  const suffix = count > 0 ? `_${count}` : '';
5
5
  return slugify(`param-${name}${suffix}`, { decamelize: true, separator: '-' });
6
6
  }
7
+ const PARAM_FIELD_COMPONENTS = ['ParamField', 'Param', 'ResponseField'];
8
+ const PARAM_NAME_PRECEDENCE = ['query', 'path', 'body', 'header'];
9
+ const getStringAttribute = (element, attrName) => {
10
+ const attr = element.attributes.find((attr) => 'name' in attr && attr.name === attrName);
11
+ return attr && typeof attr.value === 'string' && attr.value ? attr.value : undefined;
12
+ };
13
+ const hasIdAttribute = (element) => element.attributes.some((attr) => 'name' in attr && attr.name === 'id');
14
+ const getFieldName = (element) => {
15
+ if (element.name === 'ResponseField')
16
+ return getStringAttribute(element, 'name');
17
+ for (const attrName of PARAM_NAME_PRECEDENCE) {
18
+ const value = getStringAttribute(element, attrName);
19
+ if (value)
20
+ return value;
21
+ }
22
+ return undefined;
23
+ };
7
24
  export const rehypeParamFieldIds = () => {
8
25
  return (tree) => {
9
- const paramCounts = new Map();
26
+ const claimedIds = new Set();
27
+ const slugCounts = new Map();
28
+ visit(tree, 'mdxJsxFlowElement', (element) => {
29
+ if (!element.name || !PARAM_FIELD_COMPONENTS.includes(element.name))
30
+ return;
31
+ if (!hasIdAttribute(element))
32
+ return;
33
+ const explicitId = getStringAttribute(element, 'id');
34
+ if (explicitId)
35
+ claimedIds.add(explicitId);
36
+ });
10
37
  visit(tree, 'mdxJsxFlowElement', (element) => {
11
38
  var _a;
12
- if (element.name === 'ParamField' ||
13
- element.name === 'Param' ||
14
- element.name === 'ResponseField') {
15
- const nameAttr = element.attributes.find((attr) => 'name' in attr &&
16
- (attr.name === 'query' ||
17
- attr.name === 'path' ||
18
- attr.name === 'body' ||
19
- attr.name === 'header' ||
20
- attr.name === 'name'));
21
- if (nameAttr && typeof nameAttr.value === 'string' && nameAttr.value) {
22
- const currentCount = (_a = paramCounts.get(nameAttr.value)) !== null && _a !== void 0 ? _a : 0;
23
- paramCounts.set(nameAttr.value, currentCount + 1);
24
- const id = generateParamFieldId(nameAttr.value, currentCount);
25
- element.attributes.push({
26
- type: 'mdxJsxAttribute',
27
- name: 'id',
28
- value: id,
29
- });
30
- }
39
+ if (!element.name || !PARAM_FIELD_COMPONENTS.includes(element.name))
40
+ return;
41
+ if (hasIdAttribute(element))
42
+ return;
43
+ const name = getFieldName(element);
44
+ if (!name)
45
+ return;
46
+ const baseSlug = generateParamFieldId(name, 0);
47
+ let count = (_a = slugCounts.get(baseSlug)) !== null && _a !== void 0 ? _a : 0;
48
+ let id = generateParamFieldId(name, count);
49
+ while (claimedIds.has(id)) {
50
+ count += 1;
51
+ id = generateParamFieldId(name, count);
31
52
  }
53
+ slugCounts.set(baseSlug, count + 1);
54
+ claimedIds.add(id);
55
+ element.attributes.push({
56
+ type: 'mdxJsxAttribute',
57
+ name: 'id',
58
+ value: id,
59
+ });
32
60
  });
33
61
  return tree;
34
62
  };
@@ -93,31 +93,31 @@ export const remarkComponentIds = (pageMetadata) => (tree) => {
93
93
  const processTabsRecursively = (node) => {
94
94
  if (node.type === 'mdxJsxFlowElement' && node.name === 'Tab') {
95
95
  const slug = tabSlugs.get(node);
96
- if (!slug)
97
- return;
98
- // `slug` already equals the explicit `id` when one was provided, so only
99
- // add the attribute when the Tab doesn't already have one.
100
- const hasId = node.attributes.some((attr) => 'name' in attr && attr.name === 'id');
101
- if (!hasId) {
102
- node.attributes.push(createMdxJsxAttribute('id', slug));
103
- }
104
- const childTabIds = [];
105
- for (const child of node.children) {
106
- const childIds = collectDirectChildIds(child, 'Tab');
107
- childTabIds.push(...childIds);
108
- }
109
- if (childTabIds.length > 0) {
110
- try {
111
- node.attributes.push(createMdxJsxAttribute(CHILD_TAB_IDS_ATTRIBUTE, JSON.stringify(childTabIds)));
96
+ if (slug) {
97
+ // `slug` already equals the explicit `id` when one was provided, so only
98
+ // add the attribute when the Tab doesn't already have one.
99
+ const hasId = node.attributes.some((attr) => 'name' in attr && attr.name === 'id');
100
+ if (!hasId) {
101
+ node.attributes.push(createMdxJsxAttribute('id', slug));
112
102
  }
113
- catch (_a) { }
114
- }
115
- const childHeadingIds = collectDirectChildIds(node, 'Heading');
116
- if (childHeadingIds.length > 0) {
117
- try {
118
- node.attributes.push(createMdxJsxAttribute(CHILD_HEADING_IDS_ATTRIBUTE, JSON.stringify(childHeadingIds)));
103
+ const childTabIds = [];
104
+ for (const child of node.children) {
105
+ const childIds = collectDirectChildIds(child, 'Tab');
106
+ childTabIds.push(...childIds);
107
+ }
108
+ if (childTabIds.length > 0) {
109
+ try {
110
+ node.attributes.push(createMdxJsxAttribute(CHILD_TAB_IDS_ATTRIBUTE, JSON.stringify(childTabIds)));
111
+ }
112
+ catch (_a) { }
113
+ }
114
+ const childHeadingIds = collectDirectChildIds(node, 'Heading');
115
+ if (childHeadingIds.length > 0) {
116
+ try {
117
+ node.attributes.push(createMdxJsxAttribute(CHILD_HEADING_IDS_ATTRIBUTE, JSON.stringify(childHeadingIds)));
118
+ }
119
+ catch (_b) { }
119
120
  }
120
- catch (_b) { }
121
121
  }
122
122
  for (const child of node.children) {
123
123
  processTabsRecursively(child);
@@ -21,11 +21,20 @@ export function remarkExtractChangelogFilters(mdxExtracts) {
21
21
  if (!Array.isArray(tags)) {
22
22
  continue;
23
23
  }
24
- tags.forEach((tag) => {
25
- if (!!tag.trim()) {
26
- tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
24
+ const nodeTags = new Set();
25
+ for (const tag of tags) {
26
+ if (typeof tag !== 'string') {
27
+ console.warn(`Invalid tag ${JSON.stringify(tag)} in <Update> tags — tags must be strings. Skipping it.`);
28
+ continue;
27
29
  }
28
- });
30
+ const trimmed = tag.trim();
31
+ if (trimmed) {
32
+ nodeTags.add(trimmed);
33
+ }
34
+ }
35
+ for (const tag of nodeTags) {
36
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
37
+ }
29
38
  }
30
39
  const filters = Array.from(tagCounts.entries())
31
40
  .map(([tag, count]) => ({
@@ -28,9 +28,13 @@ export const remarkExtractTableOfContents = (mdxExtracts, pageMetadata) => {
28
28
  seenSlugs.set(slug, count + 1);
29
29
  if (count === 0)
30
30
  return slug;
31
- const suffixed = `${slug}-${count + 1}`;
32
- if (!seenSlugs.has(suffixed))
33
- seenSlugs.set(suffixed, 0);
31
+ let suffix = count + 1;
32
+ let suffixed = `${slug}-${suffix}`;
33
+ while (seenSlugs.has(suffixed)) {
34
+ suffix += 1;
35
+ suffixed = `${slug}-${suffix}`;
36
+ }
37
+ seenSlugs.set(suffixed, 1);
34
38
  return suffixed;
35
39
  };
36
40
  return (tree) => {
@@ -1,4 +1,12 @@
1
1
  import { visit } from 'unist-util-visit';
2
+ import { isMdxJsxFlowElement } from '../../utils.js';
3
+ const countTabs = (children) => children.reduce((count, child) => {
4
+ if (!isMdxJsxFlowElement(child))
5
+ return count;
6
+ if (child.name == null)
7
+ return count + countTabs(child.children);
8
+ return child.name === 'Tab' ? count + 1 : count;
9
+ }, 0);
2
10
  export const remarkSplitTabs = () => (tree) => {
3
11
  const tabsToProcess = [];
4
12
  visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
@@ -9,7 +17,7 @@ export const remarkSplitTabs = () => (tree) => {
9
17
  // process in reverse document order so splices don't shift the saved
10
18
  // indices of earlier siblings or clone-before-split nested Tabs
11
19
  for (const { node, index, parent } of tabsToProcess.reverse()) {
12
- const numberOfTabs = node.children.length;
20
+ const numberOfTabs = countTabs(node.children);
13
21
  if (numberOfTabs <= 1)
14
22
  continue;
15
23
  const newNodes = [];
@@ -1,11 +1,19 @@
1
- import { visit } from 'unist-util-visit';
1
+ import { visitParents } from 'unist-util-visit-parents';
2
+ import { isMdxJsxFlowElement, isMdxJsxFragment } from '../../utils.js';
2
3
  export const remarkValidateTabs = () => (tree) => {
3
- visit(tree, 'mdxJsxFlowElement', (node, _idx, parent) => {
4
- if (node.name === 'Tab') {
5
- const parentElement = parent;
6
- if (!parentElement || parentElement.name !== 'Tabs') {
7
- console.warn('Please ensure that all <Tab> components are direct children of <Tabs>.');
8
- }
4
+ visitParents(tree, 'mdxJsxFlowElement', (node, ancestors) => {
5
+ var _a;
6
+ if (node.name !== 'Tab')
7
+ return;
8
+ const meaningfulAncestors = ancestors.filter((ancestor) => !isMdxJsxFragment(ancestor));
9
+ const parent = meaningfulAncestors[meaningfulAncestors.length - 1];
10
+ const parentIsTabs = parent !== undefined && isMdxJsxFlowElement(parent) && parent.name === 'Tabs';
11
+ if (!parentIsTabs) {
12
+ console.warn('Please ensure that all <Tab> components are direct children of <Tabs>.');
13
+ }
14
+ const title = (_a = node.attributes.find((attr) => 'name' in attr && attr.name === 'title')) === null || _a === void 0 ? void 0 : _a.value;
15
+ if (title === '') {
16
+ console.warn('<Tab> has an empty title — the tab has no accessible name and cannot deep-link. Add a descriptive title.');
9
17
  }
10
18
  });
11
19
  };
@@ -87,12 +87,16 @@ function divisionSummary(result) {
87
87
  function mapHrefs(map) {
88
88
  return new Map([...map.entries()].map(([key, value]) => [key, value.href]));
89
89
  }
90
- function assertInvariantForEveryPath(nav) {
90
+ function assertInvariantForEveryPath(nav, includeProductVersionMetadata = false) {
91
91
  const paths = getAllPathsInDecoratedNav(nav);
92
92
  expect(paths.length).toBeGreaterThan(0);
93
93
  for (const path of paths) {
94
94
  const currentPath = `/${path.replace(/^\//, '')}`;
95
- const scoped = getScopedNavForPath({ decoratedNav: nav, currentPath });
95
+ const scoped = getScopedNavForPath({
96
+ decoratedNav: nav,
97
+ currentPath,
98
+ includeProductVersionMetadata,
99
+ });
96
100
  const full = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage);
97
101
  const rescoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage);
98
102
  expect(rescoped.groupsOrPages, currentPath).toEqual(full.groupsOrPages);
@@ -132,4 +136,149 @@ describe(getScopedNavForPath, () => {
132
136
  expect(serialized).not.toContain('/v1/es/guides/overview');
133
137
  expect(serialized).toContain('/en/guides/new-in-v2');
134
138
  });
139
+ describe('includeProductVersionMetadata', () => {
140
+ const productNav = {
141
+ products: [
142
+ {
143
+ product: 'Model Serving',
144
+ versions: [
145
+ {
146
+ version: '6.0',
147
+ default: true,
148
+ groups: [
149
+ {
150
+ group: 'Guides',
151
+ pages: [
152
+ { title: 'Quickstart', href: '/serving/6.0/quickstart' },
153
+ { title: 'Endpoints', href: '/serving/6.0/endpoints' },
154
+ ],
155
+ },
156
+ ],
157
+ },
158
+ {
159
+ version: '5.9',
160
+ groups: [
161
+ {
162
+ group: 'Guides',
163
+ pages: [{ title: 'Quickstart', href: '/serving/5.9/quickstart' }],
164
+ },
165
+ ],
166
+ },
167
+ ],
168
+ },
169
+ {
170
+ product: 'Workbench',
171
+ global: {
172
+ versions: [{ version: 'external-9.9', href: 'https://legacy.example.com' }],
173
+ },
174
+ versions: [
175
+ {
176
+ version: '3.2',
177
+ name: 'Workbench 3.2',
178
+ default: true,
179
+ groups: [
180
+ { group: 'Guides', pages: [{ title: 'Setup', href: '/workbench/3.2/setup' }] },
181
+ ],
182
+ },
183
+ {
184
+ version: '3.1',
185
+ groups: [
186
+ { group: 'Guides', pages: [{ title: 'Setup', href: '/workbench/3.1/setup' }] },
187
+ ],
188
+ },
189
+ {
190
+ version: '3.0-beta',
191
+ hidden: true,
192
+ groups: [
193
+ { group: 'Guides', pages: [{ title: 'Setup', href: '/workbench/3.0/setup' }] },
194
+ ],
195
+ },
196
+ ],
197
+ },
198
+ ],
199
+ };
200
+ const currentPath = '/serving/6.0/quickstart';
201
+ function findProduct(nav, name) {
202
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
203
+ const products = nav.products;
204
+ return products.find((product) => product.product === name);
205
+ }
206
+ it('drops sibling product versions by default', () => {
207
+ var _a;
208
+ const scoped = getScopedNavForPath({ decoratedNav: productNav, currentPath });
209
+ expect((_a = findProduct(scoped.scopedNav, 'Workbench')) === null || _a === void 0 ? void 0 : _a.versions).toBeUndefined();
210
+ });
211
+ it('keeps pageless version metadata on sibling product stubs when enabled', () => {
212
+ var _a;
213
+ const scoped = getScopedNavForPath({
214
+ decoratedNav: productNav,
215
+ currentPath,
216
+ includeProductVersionMetadata: true,
217
+ });
218
+ expect((_a = findProduct(scoped.scopedNav, 'Workbench')) === null || _a === void 0 ? void 0 : _a.versions).toEqual([
219
+ { version: '3.2', name: 'Workbench 3.2' },
220
+ { version: '3.1' },
221
+ ]);
222
+ expect(JSON.stringify(scoped.scopedNav)).not.toContain('/workbench/3.1/setup');
223
+ expect(JSON.stringify(scoped.scopedNav)).not.toContain('external-9.9');
224
+ });
225
+ it('leaves the active product and jump maps unchanged when enabled', () => {
226
+ const scoped = getScopedNavForPath({ decoratedNav: productNav, currentPath });
227
+ const withMetadata = getScopedNavForPath({
228
+ decoratedNav: productNav,
229
+ currentPath,
230
+ includeProductVersionMetadata: true,
231
+ });
232
+ expect(findProduct(withMetadata.scopedNav, 'Model Serving')).toEqual(findProduct(scoped.scopedNav, 'Model Serving'));
233
+ expect(withMetadata.firstHrefInVersion).toEqual(scoped.firstHrefInVersion);
234
+ expect(withMetadata.firstHrefInLanguage).toEqual(scoped.firstHrefInLanguage);
235
+ expect(withMetadata.currentVersion).toBe(scoped.currentVersion);
236
+ });
237
+ it('filters identically to the full nav for every path when enabled', () => {
238
+ assertInvariantForEveryPath(multiDivisionNav, true);
239
+ assertInvariantForEveryPath(decoratedNavigationConfig, true);
240
+ });
241
+ it('changes nothing but the added metadata for every path', () => {
242
+ const paths = getAllPathsInDecoratedNav(productNav);
243
+ expect(paths.length).toBeGreaterThan(0);
244
+ for (const path of paths) {
245
+ const currentPath = `/${path.replace(/^\//, '')}`;
246
+ const scopedOff = getScopedNavForPath({ decoratedNav: productNav, currentPath });
247
+ const scopedOn = getScopedNavForPath({
248
+ decoratedNav: productNav,
249
+ currentPath,
250
+ includeProductVersionMetadata: true,
251
+ });
252
+ expect(stripMetadataVersions(scopedOn.scopedNav), currentPath).toEqual(scopedOff.scopedNav);
253
+ expect(scopedOn.firstHrefInVersion, currentPath).toEqual(scopedOff.firstHrefInVersion);
254
+ expect(scopedOn.firstHrefInLanguage, currentPath).toEqual(scopedOff.firstHrefInLanguage);
255
+ expect(scopedOn.currentVersion, currentPath).toBe(scopedOff.currentVersion);
256
+ expect(scopedOn.currentLanguage, currentPath).toBe(scopedOff.currentLanguage);
257
+ const filteredOff = filterWith(scopedOff.scopedNav, currentPath, scopedOff.currentVersion, scopedOff.currentLanguage);
258
+ const filteredOn = filterWith(scopedOn.scopedNav, currentPath, scopedOn.currentVersion, scopedOn.currentLanguage);
259
+ expect(divisionSummary(filteredOn), currentPath).toEqual(divisionSummary(filteredOff));
260
+ expect(filteredOn.groupsOrPages, currentPath).toEqual(filteredOff.groupsOrPages);
261
+ expect(mapHrefs(filteredOn.firstHrefInVersion), currentPath).toEqual(mapHrefs(filteredOff.firstHrefInVersion));
262
+ }
263
+ });
264
+ });
135
265
  });
266
+ function stripMetadataVersions(value) {
267
+ if (Array.isArray(value))
268
+ return value.map(stripMetadataVersions);
269
+ if (typeof value !== 'object' || value === null)
270
+ return value;
271
+ const entries = Object.entries(value).flatMap(([key, entry]) => {
272
+ if (key === 'versions' &&
273
+ Array.isArray(entry) &&
274
+ entry.every((version) => typeof version === 'object' &&
275
+ version !== null &&
276
+ !('pages' in version) &&
277
+ !('groups' in version) &&
278
+ !('href' in version))) {
279
+ return [];
280
+ }
281
+ return [[key, stripMetadataVersions(entry)]];
282
+ });
283
+ return Object.fromEntries(entries);
284
+ }
@@ -10,9 +10,10 @@ export type ScopedNavForPath = {
10
10
  currentVersion: string | undefined;
11
11
  currentLanguage: LocaleType | undefined;
12
12
  };
13
- export declare function getScopedNavForPath({ decoratedNav, currentPath, userGroups, isPreview, }: {
13
+ export declare function getScopedNavForPath({ decoratedNav, currentPath, userGroups, isPreview, includeProductVersionMetadata, }: {
14
14
  decoratedNav: DecoratedNavigationConfig;
15
15
  currentPath: string;
16
16
  userGroups?: Set<string>;
17
17
  isPreview?: boolean;
18
+ includeProductVersionMetadata?: boolean;
18
19
  }): ScopedNavForPath;
@@ -357,6 +357,40 @@ function scopeDivisionArray(entries, key, ctx) {
357
357
  return stubDivision(entry, key, ctx);
358
358
  });
359
359
  }
360
+ function collectProductVersionMetadata(node, out, seen) {
361
+ if (Array.isArray(node)) {
362
+ for (const entry of node) {
363
+ collectProductVersionMetadata(entry, out, seen);
364
+ }
365
+ return;
366
+ }
367
+ if (!isNavNode(node))
368
+ return;
369
+ for (const [key, value] of Object.entries(node)) {
370
+ // versions under a nested product belong to that product, and global
371
+ // holds external links never attributed to a product by the consumer
372
+ if (key === 'products' || key === 'global')
373
+ continue;
374
+ if (key === 'versions' && Array.isArray(value)) {
375
+ for (const version of value) {
376
+ if (!isNavNode(version))
377
+ continue;
378
+ if (typeof version.version !== 'string' || version.hidden === true)
379
+ continue;
380
+ if (seen.has(version.version))
381
+ continue;
382
+ seen.add(version.version);
383
+ const metadata = { version: version.version };
384
+ if (typeof version.name === 'string')
385
+ metadata.name = version.name;
386
+ out.push(metadata);
387
+ }
388
+ }
389
+ if (Array.isArray(value) || isNavNode(value)) {
390
+ collectProductVersionMetadata(value, out, seen);
391
+ }
392
+ }
393
+ }
360
394
  function stubProduct(product, ctx) {
361
395
  if (typeof product.href === 'string')
362
396
  return product;
@@ -373,6 +407,14 @@ function stubProduct(product, ctx) {
373
407
  stub[entryKey] = value;
374
408
  }
375
409
  }
410
+ if (ctx.includeProductVersionMetadata) {
411
+ const versionMetadata = [];
412
+ collectProductVersionMetadata(product, versionMetadata, new Set());
413
+ // pageless version entries so the search version filter can list a
414
+ // non-active product's versions; invisible to sidebar/dict consumers
415
+ if (versionMetadata.length)
416
+ stub.versions = versionMetadata;
417
+ }
376
418
  stub.pages = target.isRaw
377
419
  ? [target.href]
378
420
  : [makeStubPage(typeof product.product === 'string' ? product.product : '', target)];
@@ -433,7 +475,7 @@ export function scopeDocsConfigNavToPath(docsConfig, currentPath) {
433
475
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
434
476
  navigation: scopeNode(docsConfig.navigation, { currentPath }) });
435
477
  }
436
- export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = new Set(), isPreview = false, }) {
478
+ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = new Set(), isPreview = false, includeProductVersionMetadata = false, }) {
437
479
  var _a, _b, _c, _d, _e, _f, _g, _h;
438
480
  const containingProductNames = new Set();
439
481
  collectContainingProductNames(
@@ -489,6 +531,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
489
531
  userGroups,
490
532
  isPreview,
491
533
  skipVersionLanguageStubs: productAmbiguous,
534
+ includeProductVersionMetadata,
492
535
  }
493
536
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
494
537
  );
package/dist/rss/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import slugify from '@sindresorhus/slugify';
2
2
  import { stringifyTree } from '../mdx/index.js';
3
3
  import { getArrayExpressionStringProperties, getObjectExpressionStringProperty, } from '../mdx/utils.js';
4
+ import { safeCleanHeadingId } from '../slugify.js';
4
5
  export const isFrontmatter = (node) => {
5
6
  return (node === null || node === void 0 ? void 0 : node.type) === 'yaml';
6
7
  };
@@ -85,6 +86,17 @@ export const getRssPropsData = (updateComponent) => {
85
86
  const description = getObjectExpressionStringProperty('description', rssData);
86
87
  return { rssTitle: title, rssDescription: description };
87
88
  };
89
+ const getUpdateAnchorId = (updateComponent) => {
90
+ const idAttribute = updateComponent.attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' &&
91
+ attribute.name === 'id' &&
92
+ typeof attribute.value === 'string');
93
+ if (typeof (idAttribute === null || idAttribute === void 0 ? void 0 : idAttribute.value) !== 'string')
94
+ return undefined;
95
+ // Match the page's id pipeline (Update.tsx uses safeCleanHeadingId) so feed
96
+ // fragment links point at the same DOM id the page renders; the RSS pipeline
97
+ // never runs the ToC plugin, so the cleaning must happen here.
98
+ return safeCleanHeadingId(idAttribute.value) || undefined;
99
+ };
88
100
  export const getUpdateTitle = (updateComponent) => {
89
101
  var _a;
90
102
  const attributes = updateComponent.attributes;
@@ -313,7 +325,7 @@ export const processUpdatePerNode = ({ updateNode, date, title, description, })
313
325
  return [];
314
326
  }
315
327
  const rssTitle = title || label;
316
- const anchor = slugify(label);
328
+ const anchor = getUpdateAnchorId(updateNode) || slugify(label);
317
329
  const categories = getTags(updateNode);
318
330
  const contentNodes = updateNode.children.filter((child) => isNormalMarkdown(child));
319
331
  const contentString = stringifyTreeForRSS({