jamdesk 1.1.193 → 1.1.195

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Sync Verification Test
3
3
  *
4
- * Ensures the frontmatter-utils.ts file stays in sync with build-service.
4
+ * Ensures the frontmatter-utils.ts file and the frontmatter-text.ts leaf it
5
+ * re-exports stay in sync with build-service.
5
6
  * Catches drift if someone forgets to run `npm run vendor`.
6
7
  */
7
8
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter-sync.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/frontmatter-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
1
+ {"version":3,"file":"frontmatter-sync.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/frontmatter-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Sync Verification Test
3
3
  *
4
- * Ensures the frontmatter-utils.ts file stays in sync with build-service.
4
+ * Ensures the frontmatter-utils.ts file and the frontmatter-text.ts leaf it
5
+ * re-exports stay in sync with build-service.
5
6
  * Catches drift if someone forgets to run `npm run vendor`.
6
7
  */
7
8
  import { describe, it, expect } from 'vitest';
@@ -20,7 +21,21 @@ describe('frontmatter-utils sync verification', () => {
20
21
  const cliWithoutHeader = cliContent.replace(/^\/\*\*\n \* Frontmatter Utilities[\s\S]*?AUTO-SYNCED[\s\S]*?\*\/\n\n/, '');
21
22
  // Remove the original header from build-service version
22
23
  const buildServiceWithoutHeader = buildServiceContent.replace(/^\/\*\*[\s\S]*?\*\/\s*\n/, '');
23
- expect(cliWithoutHeader).toBe(buildServiceWithoutHeader);
24
+ // vendor.js rewrites exactly one relative specifier on the way in: src/lib
25
+ // is compiled under NodeNext, which demands the extension build-service
26
+ // cannot write (Turbopack resolves relative specifiers literally). Asserted
27
+ // rather than merely normalised, so the transform cannot silently stop.
28
+ expect(cliWithoutHeader).toContain("from './frontmatter-text.js'");
29
+ expect(cliWithoutHeader.replace("from './frontmatter-text.js'", "from './frontmatter-text'"))
30
+ .toBe(buildServiceWithoutHeader);
31
+ });
32
+ it('CLI frontmatter-text matches build-service source byte for byte', () => {
33
+ // frontmatter-utils re-exports frontmatterText from this leaf, so the copy
34
+ // is load-bearing for the CLI build, not just for behaviour parity. Copied
35
+ // without a banner, so this comparison is exact.
36
+ const cliPath = path.join(__dirname, '../../lib/frontmatter-text.ts');
37
+ const buildServicePath = path.join(__dirname, '../../../../build-service/lib/frontmatter-text.ts');
38
+ expect(fs.readFileSync(cliPath, 'utf-8')).toBe(fs.readFileSync(buildServicePath, 'utf-8'));
24
39
  });
25
40
  });
26
41
  //# sourceMappingURL=frontmatter-sync.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter-sync.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/frontmatter-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gCAAgC,CAAC,CAAC;QACvE,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oDAAoD,CAAC,CAAC;QAEpG,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,mBAAmB,GAAG,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAEvE,gEAAgE;QAChE,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC,uEAAuE,EAAE,EAAE,CAAC,CAAC;QAEzH,wDAAwD;QACxD,MAAM,yBAAyB,GAAG,mBAAmB,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;QAE9F,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"frontmatter-sync.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/frontmatter-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gCAAgC,CAAC,CAAC;QACvE,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oDAAoD,CAAC,CAAC;QAEpG,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,mBAAmB,GAAG,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAEvE,gEAAgE;QAChE,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC,uEAAuE,EAAE,EAAE,CAAC,CAAC;QAEzH,wDAAwD;QACxD,MAAM,yBAAyB,GAAG,mBAAmB,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;QAE9F,2EAA2E;QAC3E,wEAAwE;QACxE,4EAA4E;QAC5E,wEAAwE;QACxE,MAAM,CAAC,gBAAgB,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACnE,MAAM,CAAC,gBAAgB,CAAC,OAAO,CAAC,8BAA8B,EAAE,2BAA2B,CAAC,CAAC;aAC1F,IAAI,CAAC,yBAAyB,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,2EAA2E;QAC3E,2EAA2E;QAC3E,iDAAiD;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,+BAA+B,CAAC,CAAC;QACtE,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mDAAmD,CAAC,CAAC;QAEnG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Coerce a frontmatter value to the string its text consumers require.
3
+ *
4
+ * Frontmatter is whatever YAML decided, not what the author meant:
5
+ * `description: 404` parses to a number and `description: 2026-01-01` to a
6
+ * Date. Both reach surfaces that accept only strings — react-markdown asserts
7
+ * on `children` and THROWS in development, `.trim()`/`.replace()` throw
8
+ * outright, and `<meta name="description">` would carry `[object Object]`.
9
+ *
10
+ * The same is true of a dereferenced OpenAPI spec, which is why ApiMarkdown
11
+ * calls this too: the ISR loader runs SwaggerParser.dereference, NOT .validate
12
+ * (lib/openapi-isr.ts), so a `description:` YAML read as a number or a Date is
13
+ * never rejected on the way in and arrives at the renderer as-is.
14
+ *
15
+ * A Date is rendered back as the ISO date the author typed rather than through
16
+ * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
17
+ * local time, which anywhere west of UTC prints the PREVIOUS day.
18
+ *
19
+ * Canonical here rather than beside any one caller: the renderer, the search
20
+ * index, and llms-full.txt each parse frontmatter independently, and a copy per
21
+ * consumer is how three of them ended up without the Date branch.
22
+ */
23
+ export declare function frontmatterText(value: unknown): string;
24
+ //# sourceMappingURL=frontmatter-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter-text.d.ts","sourceRoot":"","sources":["../../src/lib/frontmatter-text.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAItD"}
@@ -0,0 +1,41 @@
1
+ // builder/build-service/lib/frontmatter-text.ts
2
+ //
3
+ // One function, deliberately in an import-free leaf module of its own.
4
+ //
5
+ // It started in frontmatter-utils.ts and still re-exports from there, so every
6
+ // existing caller is untouched. It had to move because ApiMarkdown.tsx — the
7
+ // single renderer for every description an OpenAPI spec carries — needs the
8
+ // same coercion and is a `'use client'` module: importing frontmatter-utils
9
+ // would drag gray-matter across the client boundary and into the browser
10
+ // bundle. A copy in the component instead is how the Date branch went missing
11
+ // from three frontmatter consumers before this function was made canonical.
12
+ /**
13
+ * Coerce a frontmatter value to the string its text consumers require.
14
+ *
15
+ * Frontmatter is whatever YAML decided, not what the author meant:
16
+ * `description: 404` parses to a number and `description: 2026-01-01` to a
17
+ * Date. Both reach surfaces that accept only strings — react-markdown asserts
18
+ * on `children` and THROWS in development, `.trim()`/`.replace()` throw
19
+ * outright, and `<meta name="description">` would carry `[object Object]`.
20
+ *
21
+ * The same is true of a dereferenced OpenAPI spec, which is why ApiMarkdown
22
+ * calls this too: the ISR loader runs SwaggerParser.dereference, NOT .validate
23
+ * (lib/openapi-isr.ts), so a `description:` YAML read as a number or a Date is
24
+ * never rejected on the way in and arrives at the renderer as-is.
25
+ *
26
+ * A Date is rendered back as the ISO date the author typed rather than through
27
+ * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
28
+ * local time, which anywhere west of UTC prints the PREVIOUS day.
29
+ *
30
+ * Canonical here rather than beside any one caller: the renderer, the search
31
+ * index, and llms-full.txt each parse frontmatter independently, and a copy per
32
+ * consumer is how three of them ended up without the Date branch.
33
+ */
34
+ export function frontmatterText(value) {
35
+ if (value === null || value === undefined || value === '')
36
+ return '';
37
+ if (value instanceof Date)
38
+ return value.toISOString().slice(0, 10);
39
+ return String(value);
40
+ }
41
+ //# sourceMappingURL=frontmatter-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter-text.js","sourceRoot":"","sources":["../../src/lib/frontmatter-text.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,8EAA8E;AAC9E,4EAA4E;AAE5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IACrE,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -22,22 +22,5 @@ export declare function parseFrontmatterLenient(content: string): {
22
22
  };
23
23
  content: string;
24
24
  };
25
- /**
26
- * Coerce a frontmatter value to the string its text consumers require.
27
- *
28
- * Frontmatter is whatever YAML decided, not what the author meant:
29
- * `description: 404` parses to a number and `description: 2026-01-01` to a
30
- * Date. Both reach surfaces that accept only strings — react-markdown asserts
31
- * on `children` and THROWS in development, `.trim()`/`.replace()` throw
32
- * outright, and `<meta name="description">` would carry `[object Object]`.
33
- *
34
- * A Date is rendered back as the ISO date the author typed rather than through
35
- * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
36
- * local time, which anywhere west of UTC prints the PREVIOUS day.
37
- *
38
- * Canonical here rather than beside any one caller: the renderer, the search
39
- * index, and llms-full.txt each parse frontmatter independently, and a copy per
40
- * consumer is how three of them ended up without the Date branch.
41
- */
42
- export declare function frontmatterText(value: unknown): string;
25
+ export { frontmatterText } from './frontmatter-text.js';
43
26
  //# sourceMappingURL=frontmatter-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter-utils.d.ts","sourceRoot":"","sources":["../../src/lib/frontmatter-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA8D7D;AAqBD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG;IAExD,IAAI,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,CAIA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAItD"}
1
+ {"version":3,"file":"frontmatter-utils.d.ts","sourceRoot":"","sources":["../../src/lib/frontmatter-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA8D7D;AAqBD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG;IAExD,IAAI,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,CAIA;AAMD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -97,28 +97,9 @@ export function parseFrontmatterLenient(content) {
97
97
  const { data, content: rawContent } = matter(processed);
98
98
  return { data, content: rawContent };
99
99
  }
100
- /**
101
- * Coerce a frontmatter value to the string its text consumers require.
102
- *
103
- * Frontmatter is whatever YAML decided, not what the author meant:
104
- * `description: 404` parses to a number and `description: 2026-01-01` to a
105
- * Date. Both reach surfaces that accept only strings — react-markdown asserts
106
- * on `children` and THROWS in development, `.trim()`/`.replace()` throw
107
- * outright, and `<meta name="description">` would carry `[object Object]`.
108
- *
109
- * A Date is rendered back as the ISO date the author typed rather than through
110
- * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
111
- * local time, which anywhere west of UTC prints the PREVIOUS day.
112
- *
113
- * Canonical here rather than beside any one caller: the renderer, the search
114
- * index, and llms-full.txt each parse frontmatter independently, and a copy per
115
- * consumer is how three of them ended up without the Date branch.
116
- */
117
- export function frontmatterText(value) {
118
- if (value === null || value === undefined || value === '')
119
- return '';
120
- if (value instanceof Date)
121
- return value.toISOString().slice(0, 10);
122
- return String(value);
123
- }
100
+ // frontmatterText lives in its own import-free leaf so ApiMarkdown.tsx (a
101
+ // `'use client'` module) can share it without pulling gray-matter into the
102
+ // browser bundle. Re-exported here because this is where every caller expects
103
+ // it, and a second import path would be a second thing to keep in step.
104
+ export { frontmatterText } from './frontmatter-text.js';
124
105
  //# sourceMappingURL=frontmatter-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter-utils.js","sourceRoot":"","sources":["../../src/lib/frontmatter-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frontmatter-utils.js","sourceRoot":"","sources":["../../src/lib/frontmatter-utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.193",
3
+ "version": "1.1.195",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -3,6 +3,7 @@
3
3
  import type React from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
+ import { frontmatterText } from '@/lib/frontmatter-text';
6
7
 
7
8
  /**
8
9
  * The single markdown renderer for every description an OpenAPI spec carries.
@@ -23,13 +24,23 @@ import remarkGfm from 'remark-gfm';
23
24
  *
24
25
  * `singleTilde: false` keeps `~x~` from becoming strikethrough, so a lone tilde
25
26
  * in a path or a range survives.
27
+ *
28
+ * `children` is typed `unknown` on purpose. Every call site hands us a
29
+ * `description` straight out of a dereferenced spec, and the ISR loader runs
30
+ * SwaggerParser.dereference, NOT .validate (lib/openapi-isr.ts) — so YAML's
31
+ * reading of `description: 404` (a number) or `description: 2026-01-01` (a
32
+ * Date) is never rejected on the way in. react-markdown asserts on a non-string
33
+ * `children` and THROWS, which on a docs page means a 500 for a spec that
34
+ * merely forgot its quotes. Narrowing at this one boundary covers all thirteen
35
+ * description sites at once; a `string` annotation here would only have made
36
+ * the compiler agree with a promise the data does not keep.
26
37
  */
27
38
  export function ApiMarkdown({
28
39
  children,
29
40
  className,
30
41
  style,
31
42
  }: {
32
- children: string;
43
+ children: unknown;
33
44
  className: string;
34
45
  /** The playground panel styles exclusively with inline CSS variables (see
35
46
  * PlaygroundParamForm's header), so it has no class to pass. Optional so
@@ -39,10 +50,15 @@ export function ApiMarkdown({
39
50
  return (
40
51
  <div className={className} style={style}>
41
52
  <ReactMarkdown remarkPlugins={[[remarkGfm, { singleTilde: false }]]}>
42
- {children}
53
+ {frontmatterText(children)}
43
54
  </ReactMarkdown>
44
55
  </div>
45
56
  );
46
57
  }
47
58
 
48
- export { API_DESCRIPTION_PROSE, MARKDOWN_BLOCK_STRUCTURE } from './markdown-classes';
59
+ // Re-exported so a call site takes the renderer and its typography in one
60
+ // import. MARKDOWN_BLOCK_STRUCTURE is deliberately NOT among them: its only
61
+ // consumer is render-doc-page.tsx, a server module, and routing a constant
62
+ // through this `'use client'` file would drag the component across the
63
+ // boundary with it.
64
+ export { API_DESCRIPTION_PROSE, OPERATION_DESCRIPTION_PROSE } from './markdown-classes';
@@ -7,7 +7,7 @@ import { ApiEndpoint } from './ApiEndpoint';
7
7
  import { CodePanel, CodePanelTab, DelayedSpinner } from '../ui/CodePanel';
8
8
  import { useShikiHighlightMultiple } from '@/hooks/useShikiHighlight';
9
9
  import { preloadHighlighter } from '@/lib/shiki-client';
10
- import { ApiMarkdown, API_DESCRIPTION_PROSE } from './ApiMarkdown';
10
+ import { ApiMarkdown, API_DESCRIPTION_PROSE, OPERATION_DESCRIPTION_PROSE } from './ApiMarkdown';
11
11
  import { resolveServerUrl } from '@/lib/openapi/resolve-server-url';
12
12
 
13
13
 
@@ -983,7 +983,7 @@ export function OpenApiEndpoint({
983
983
 
984
984
  {/* Description only - summary is shown as page title (h1) */}
985
985
  {description && !descriptionAlreadyShown && (
986
- <ApiMarkdown className="mt-4 text-sm text-[var(--color-text-secondary)] leading-relaxed prose prose-sm dark:prose-invert max-w-none [&>p]:mb-3 [&>ul]:my-2 [&>ol]:my-2 [&>h2]:text-base [&>h2]:font-semibold [&>h2]:mt-4 [&>h2]:mb-2 [&>h3]:text-sm [&>h3]:font-semibold [&>h3]:mt-3 [&>h3]:mb-2 [&_li]:my-0.5 [&_code]:text-xs [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_pre]:bg-[var(--color-bg-tertiary)] [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:text-xs [&_pre]:overflow-x-auto [&_strong]:text-[var(--color-text-primary)] [&_table]:w-full [&_table]:my-3 [&_table]:text-xs [&_table]:border-collapse [&_table]:block [&_table]:overflow-x-auto [&_th]:text-left [&_th]:font-semibold [&_th]:p-2 [&_th]:border [&_th]:border-[var(--color-border)] [&_td]:p-2 [&_td]:border [&_td]:border-[var(--color-border)] [&_td]:align-top">
986
+ <ApiMarkdown className={`mt-4 text-sm text-[var(--color-text-secondary)] leading-relaxed prose ${OPERATION_DESCRIPTION_PROSE}`}>
987
987
  {description}
988
988
  </ApiMarkdown>
989
989
  )}
@@ -31,6 +31,29 @@ export const MARKDOWN_BLOCK_STRUCTURE = [
31
31
  '[&_pre]:bg-[var(--color-bg-tertiary)] [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:text-xs [&_pre]:overflow-x-auto',
32
32
  '[&_pre_code]:bg-transparent [&_pre_code]:p-0',
33
33
  '[&>blockquote]:border-l-2 [&>blockquote]:border-[var(--color-border)] [&>blockquote]:pl-3 [&>blockquote]:my-2',
34
+ // The four below matter at the SUBTITLE sites and nowhere else, which is not
35
+ // obvious and is worth stating: every endpoint description renders inside the
36
+ // page's own `.prose` wrapper (render-doc-page.tsx `proseClasses`), and
37
+ // app/globals.css imports themes/base.css UNLAYERED while Tailwind utilities
38
+ // land in `@layer utilities` — so `.prose li > p`, `.prose hr` and friends
39
+ // outrank anything here regardless of specificity. The subtitle sits in the
40
+ // <header>, outside that wrapper, with only preflight underneath it.
41
+ //
42
+ // Markdown wraps list items in <p> the moment a list has a blank line between
43
+ // items ("loose"), and an unstyled block <p> inside an <li> reads as a broken
44
+ // list — which is how a loose list in a page description has always rendered.
45
+ // Values match the `.prose` rules they shadow, so the endpoint sites are
46
+ // unaffected either way.
47
+ '[&_li>p]:m-0 [&_li>p]:inline',
48
+ '[&_li>p+p]:block [&_li>p+p]:mt-2',
49
+ // No `border-0` alongside: preflight already gives an <hr> a top border and
50
+ // no others, so the shorthand would only add a shorthand-vs-longhand pair
51
+ // whose winner is Tailwind's emission order rather than anything stated here.
52
+ '[&_hr]:border-t [&_hr]:border-[var(--color-border)] [&_hr]:my-6',
53
+ // Overflow protection only — deliberately not `.prose img`'s `display: block`
54
+ // and 1rem/2rem margins, which would knock a small inline badge (a normal
55
+ // thing to put in a description) onto a line of its own.
56
+ '[&_img]:max-w-full [&_img]:h-auto',
34
57
  // `block` + `overflow-x-auto`: a wide table must scroll inside its own
35
58
  // container, never widen the page — mobile is 375px.
36
59
  '[&_table]:w-full [&_table]:my-2 [&_table]:text-xs [&_table]:border-collapse [&_table]:block [&_table]:overflow-x-auto',
@@ -49,3 +72,40 @@ export const API_DESCRIPTION_PROSE = [
49
72
  '[&>h3]:text-sm [&>h3]:font-semibold [&>h3]:mt-2 [&>h3]:mb-1.5',
50
73
  '[&_code]:text-xs [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded',
51
74
  ].join(' ');
75
+
76
+ /**
77
+ * The operation description at the top of an endpoint block — the one site that
78
+ * gets a full reading column rather than a dense row.
79
+ *
80
+ * Differs from API_DESCRIPTION_PROSE in exactly one utility: `h2` keeps a larger
81
+ * scale than `h3`, because a long operation description is the one description
82
+ * that actually carries a heading hierarchy worth seeing.
83
+ *
84
+ * It used to spell out its own block structure inline instead, and that is how
85
+ * it ended up the only description site without `[&_.task-list-item]:list-none`
86
+ * — a GFM task list rendered a bullet AND a checkbox there while rendering
87
+ * correctly everywhere else on the same page. Sharing the structure and varying
88
+ * only the scale is what keeps that from recurring.
89
+ *
90
+ * The call site keeps a bare `prose` alongside this constant, and MUST. It is
91
+ * tempting to read as redundant — the page already wraps its content in
92
+ * `.prose` (render-doc-page.tsx `proseClasses`), so every `.prose <descendant>`
93
+ * rule reaches the element anyway — but `.prose` also sets `font-size` on the
94
+ * element it matches (themes/jam/variables.css, themes/pulsar/variables.css),
95
+ * and that is a different question from inheritance: the div carries `text-sm`,
96
+ * and `.prose` only outranks it by being UNLAYERED where Tailwind utilities sit
97
+ * in `@layer utilities`. Drop the class and `text-sm` wins, taking the
98
+ * operation description from 16px to 14px on the default theme. The themes that
99
+ * set no `.prose` font-size (base, nebula, halo) are the reason `text-sm` has to
100
+ * stay too — it is the fallback, not the intended size.
101
+ *
102
+ * `prose-sm` and `dark:prose-invert` went with the inline version and are not
103
+ * coming back: they are Tailwind Typography modifiers and this build has no
104
+ * such plugin, so they never matched anything.
105
+ */
106
+ export const OPERATION_DESCRIPTION_PROSE = [
107
+ MARKDOWN_BLOCK_STRUCTURE,
108
+ '[&>h2]:text-base [&>h2]:font-semibold [&>h2]:mt-4 [&>h2]:mb-2',
109
+ '[&>h3]:text-sm [&>h3]:font-semibold [&>h3]:mt-3 [&>h3]:mb-2',
110
+ '[&_code]:text-xs [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded',
111
+ ].join(' ');
@@ -0,0 +1,39 @@
1
+ // builder/build-service/lib/frontmatter-text.ts
2
+ //
3
+ // One function, deliberately in an import-free leaf module of its own.
4
+ //
5
+ // It started in frontmatter-utils.ts and still re-exports from there, so every
6
+ // existing caller is untouched. It had to move because ApiMarkdown.tsx — the
7
+ // single renderer for every description an OpenAPI spec carries — needs the
8
+ // same coercion and is a `'use client'` module: importing frontmatter-utils
9
+ // would drag gray-matter across the client boundary and into the browser
10
+ // bundle. A copy in the component instead is how the Date branch went missing
11
+ // from three frontmatter consumers before this function was made canonical.
12
+
13
+ /**
14
+ * Coerce a frontmatter value to the string its text consumers require.
15
+ *
16
+ * Frontmatter is whatever YAML decided, not what the author meant:
17
+ * `description: 404` parses to a number and `description: 2026-01-01` to a
18
+ * Date. Both reach surfaces that accept only strings — react-markdown asserts
19
+ * on `children` and THROWS in development, `.trim()`/`.replace()` throw
20
+ * outright, and `<meta name="description">` would carry `[object Object]`.
21
+ *
22
+ * The same is true of a dereferenced OpenAPI spec, which is why ApiMarkdown
23
+ * calls this too: the ISR loader runs SwaggerParser.dereference, NOT .validate
24
+ * (lib/openapi-isr.ts), so a `description:` YAML read as a number or a Date is
25
+ * never rejected on the way in and arrives at the renderer as-is.
26
+ *
27
+ * A Date is rendered back as the ISO date the author typed rather than through
28
+ * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
29
+ * local time, which anywhere west of UTC prints the PREVIOUS day.
30
+ *
31
+ * Canonical here rather than beside any one caller: the renderer, the search
32
+ * index, and llms-full.txt each parse frontmatter independently, and a copy per
33
+ * consumer is how three of them ended up without the Date branch.
34
+ */
35
+ export function frontmatterText(value: unknown): string {
36
+ if (value === null || value === undefined || value === '') return '';
37
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
38
+ return String(value);
39
+ }
@@ -112,25 +112,8 @@ export function parseFrontmatterLenient(content: string): {
112
112
  return { data, content: rawContent };
113
113
  }
114
114
 
115
- /**
116
- * Coerce a frontmatter value to the string its text consumers require.
117
- *
118
- * Frontmatter is whatever YAML decided, not what the author meant:
119
- * `description: 404` parses to a number and `description: 2026-01-01` to a
120
- * Date. Both reach surfaces that accept only strings — react-markdown asserts
121
- * on `children` and THROWS in development, `.trim()`/`.replace()` throw
122
- * outright, and `<meta name="description">` would carry `[object Object]`.
123
- *
124
- * A Date is rendered back as the ISO date the author typed rather than through
125
- * String(): YAML anchors a bare date at UTC midnight, so String() formats it in
126
- * local time, which anywhere west of UTC prints the PREVIOUS day.
127
- *
128
- * Canonical here rather than beside any one caller: the renderer, the search
129
- * index, and llms-full.txt each parse frontmatter independently, and a copy per
130
- * consumer is how three of them ended up without the Date branch.
131
- */
132
- export function frontmatterText(value: unknown): string {
133
- if (value === null || value === undefined || value === '') return '';
134
- if (value instanceof Date) return value.toISOString().slice(0, 10);
135
- return String(value);
136
- }
115
+ // frontmatterText lives in its own import-free leaf so ApiMarkdown.tsx (a
116
+ // `'use client'` module) can share it without pulling gray-matter into the
117
+ // browser bundle. Re-exported here because this is where every caller expects
118
+ // it, and a second import path would be a second thing to keep in step.
119
+ export { frontmatterText } from './frontmatter-text';
@@ -33,7 +33,7 @@ import { slug as githubSlug } from 'github-slugger';
33
33
  import type { DocsConfig, GroupConfig, TabConfig } from '../docs-types.js';
34
34
  import { isPathWithinProject } from '../../shared/path-security.js';
35
35
  import { collectMdxFiles, type CollectMdxOptions } from '../isr-build-executor.js';
36
- import { descriptionWindow } from '../description-window.js';
36
+ import { squeezeToPageDescription } from './squeeze-page-description.js';
37
37
  import { getCachedSpec } from './cache.js';
38
38
  import { generateNavFromSpec } from './generate-nav-from-spec.js';
39
39
  import type { GeneratedPage } from './types.js';
@@ -80,36 +80,6 @@ function generatedSpecSource(openapi: TabConfig['openapi']): string | null {
80
80
  : null;
81
81
  }
82
82
 
83
- /**
84
- * Squeeze an operation description down to a meta description.
85
- *
86
- * `operation.description` is prose with no length contract — real specs put
87
- * several paragraphs, tables and lists in it. Copied verbatim into frontmatter
88
- * it becomes the page's `<meta name="description">` and its rendered subtitle,
89
- * so a 900-character block ships to search engines as one truncated smear and
90
- * pushes the whole page body down the screen. Nothing downstream bounds it:
91
- * validate-page-frontmatter only ever checks `< window.min`, never a maximum.
92
- *
93
- * Nothing is lost — the full description still reaches the page body through
94
- * enrichPageInfosWithOperations, and the subtitle/body de-duplication in
95
- * render-doc-page compares the two texts, so a trimmed subtitle correctly stops
96
- * suppressing the complete operation description below it.
97
- */
98
- function metaDescription(description: string): string {
99
- // First paragraph only, whitespace flattened: a meta description is a single
100
- // line, and a spec's second paragraph is invariably detail, not summary.
101
- const firstParagraph = description.split(/\n\s*\n/)[0] ?? '';
102
- const flat = firstParagraph.replace(/\s+/g, ' ').trim();
103
- const { generateMax } = descriptionWindow({ sample: flat });
104
- if (flat.length <= generateMax) return flat;
105
- // Cut on a word boundary so the snippet does not end mid-word. CJK has no
106
- // spaces, so lastIndexOf returns -1 there and the hard slice stands — which
107
- // is correct for a script that wraps anywhere.
108
- const hardCut = flat.slice(0, generateMax);
109
- const lastSpace = hardCut.lastIndexOf(' ');
110
- return `${(lastSpace > generateMax * 0.6 ? hardCut.slice(0, lastSpace) : hardCut).replace(/[\s,;:.]+$/, '')}…`;
111
- }
112
-
113
83
  /** Frontmatter values are JSON-encoded — valid YAML double-quoted scalars,
114
84
  * immune to colons/quotes/newlines in spec summaries. */
115
85
  function generatedPageMdx(page: GeneratedPage, specSource: string): string {
@@ -119,7 +89,7 @@ function generatedPageMdx(page: GeneratedPage, specSource: string): string {
119
89
  const lines = [
120
90
  '---',
121
91
  `title: ${JSON.stringify(page.title)}`,
122
- ...(page.description ? [`description: ${JSON.stringify(metaDescription(page.description))}`] : []),
92
+ ...(page.description ? [`description: ${JSON.stringify(squeezeToPageDescription(page.description))}`] : []),
123
93
  `openapi: ${JSON.stringify(`${specRef} ${page.method} ${page.path}`)}`,
124
94
  '---',
125
95
  '',
@@ -0,0 +1,53 @@
1
+ // builder/build-service/lib/openapi/squeeze-page-description.ts
2
+ //
3
+ // One function, deliberately in a leaf module of its own: BOTH the page
4
+ // generator that writes a description into generated frontmatter and the
5
+ // renderer that recognises it as redundant must call the same code, and
6
+ // `expand-spec-pages.ts` pulls in `fs` and the whole build executor — far too
7
+ // much to drag into a request-time page render. Only `description-window.ts`
8
+ // (itself import-free) is needed here.
9
+ //
10
+ // Extensionless import, unlike the `.js`-suffixed ones in expand-spec-pages.ts:
11
+ // render-doc-page.tsx pulls this module into the Next/Turbopack graph, which
12
+ // resolves a relative specifier literally and cannot map `.js` onto a `.ts`
13
+ // file. tsc is happy either way under `moduleResolution: "bundler"`, so the
14
+ // bundler's requirement is the binding one — and it fails at `vercel --prod`,
15
+ // which neither `tsc --noEmit` nor the vitest gate exercises.
16
+
17
+ import { descriptionWindow } from '../description-window';
18
+
19
+ /**
20
+ * Squeeze an operation description down to a meta description.
21
+ *
22
+ * EXPORTED because `render-doc-page.tsx` recognises this function's output to
23
+ * decide that a generated page's subtitle is a redundant copy of the operation
24
+ * description below it. Producer and recogniser MUST be the same code: a
25
+ * hand-written matcher for "the shapes this emits" silently stops matching the
26
+ * day the squeeze changes, and the duplicate comes back with no test failing.
27
+ *
28
+ * `operation.description` is prose with no length contract — real specs put
29
+ * several paragraphs, tables and lists in it. Copied verbatim into frontmatter
30
+ * it becomes the page's `<meta name="description">` and its rendered subtitle,
31
+ * so a 900-character block ships to search engines as one truncated smear and
32
+ * pushes the whole page body down the screen. Nothing downstream bounds it:
33
+ * validate-page-frontmatter only ever checks `< window.min`, never a maximum.
34
+ *
35
+ * Nothing is lost — the full description still reaches the page body through
36
+ * enrichPageInfosWithOperations, and render-doc-page recognises this squeeze
37
+ * and drops the shortened SUBTITLE, leaving the endpoint block to render the
38
+ * description whole.
39
+ */
40
+ export function squeezeToPageDescription(description: string): string {
41
+ // First paragraph only, whitespace flattened: a meta description is a single
42
+ // line, and a spec's second paragraph is invariably detail, not summary.
43
+ const firstParagraph = description.split(/\n\s*\n/)[0] ?? '';
44
+ const flat = firstParagraph.replace(/\s+/g, ' ').trim();
45
+ const { generateMax } = descriptionWindow({ sample: flat });
46
+ if (flat.length <= generateMax) return flat;
47
+ // Cut on a word boundary so the snippet does not end mid-word. CJK has no
48
+ // spaces, so lastIndexOf returns -1 there and the hard slice stands — which
49
+ // is correct for a script that wraps anywhere.
50
+ const hardCut = flat.slice(0, generateMax);
51
+ const lastSpace = hardCut.lastIndexOf(' ');
52
+ return `${(lastSpace > generateMax * 0.6 ? hardCut.slice(0, lastSpace) : hardCut).replace(/[\s,;:.]+$/, '')}…`;
53
+ }
@@ -95,6 +95,7 @@ import { parseLocalePath } from '@/lib/locale-fallback';
95
95
  import { DocsLocaleProvider } from '@/components/mdx/docs-locale-context';
96
96
  import { findFirstNavPage } from '@/lib/find-first-nav-page';
97
97
  import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
98
+ import { squeezeToPageDescription } from '@/lib/openapi/squeeze-page-description';
98
99
  import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
99
100
  import { AIActionsMenu } from '@/components/AIActionsMenu';
100
101
  import { getContextualOptions } from '@/lib/contextual-defaults';
@@ -105,6 +106,23 @@ import { resolveAutoNewsletter, mdxHasEmailSubscribe } from '@/lib/email-subscri
105
106
 
106
107
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
107
108
 
109
+ /**
110
+ * The page subtitle — the frontmatter `description` rendered under the h1.
111
+ *
112
+ * Hoisted because this page has TWO subtitle sites (the API-page branch and the
113
+ * standard one) rendering the same string through the same markdown pipeline.
114
+ * They had drifted: the standard branch carried no block structure at all, so a
115
+ * list, table or blockquote in a plain page's description lost its markers and
116
+ * borders to Tailwind preflight while the identical description on an API page
117
+ * rendered correctly.
118
+ *
119
+ * `[&>p:only-child]:m-0` rather than `[&>p]:m-0`: a one-paragraph description —
120
+ * almost all of them — sits tight under the heading, but a multi-paragraph one
121
+ * still needs the spacing MARKDOWN_BLOCK_STRUCTURE gives it, and a blanket
122
+ * `m-0` would also collide with that rule at equal specificity.
123
+ */
124
+ const SUBTITLE_PROSE = `text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3 ${MARKDOWN_BLOCK_STRUCTURE} [&>p:only-child]:m-0 [&_code]:text-[0.9em] [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded`;
125
+
108
126
  const DEFAULT_SITE_URL = 'https://docs.example.com';
109
127
 
110
128
  /**
@@ -778,10 +796,43 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
778
796
  // string actually rendered above, and a non-string frontmatter description
779
797
  // would make `.replace` throw here too.
780
798
  const flattenText = (s: string) => s.replace(/\s+/g, ' ').trim();
799
+ const flatSubtitle = subtitleMarkdown ? flattenText(subtitleMarkdown) : '';
800
+ // Spec input, not ours: a `description:` that YAML hands back as a number or
801
+ // a Date must not take the page down ("docs never 500"). Narrowed once here
802
+ // so none of the string work below can throw.
803
+ const operationDescription =
804
+ typeof openApiEndpointData?.description === 'string'
805
+ ? openApiEndpointData.description
806
+ : '';
807
+ const flatOperation = operationDescription ? flattenText(operationDescription) : '';
781
808
  const descriptionAlreadyShown = Boolean(
782
- subtitleMarkdown &&
783
- openApiEndpointData?.description &&
784
- flattenText(subtitleMarkdown) === flattenText(openApiEndpointData.description),
809
+ flatSubtitle && flatOperation && flatSubtitle === flatOperation,
810
+ );
811
+
812
+ // The mirror case: the subtitle is a SHORTENED copy of the operation
813
+ // description rather than the whole of it, so suppressing the endpoint block
814
+ // would delete everything the shortening dropped. Here the SUBTITLE is the
815
+ // redundant half — the block below opens with the same words and then carries
816
+ // the rest — so drop the subtitle and keep the block. Across the specs in
817
+ // this repo a third of operations land here, and the page showed a visibly
818
+ // cut-off `…` paragraph directly above the same text in full.
819
+ //
820
+ // Asking the generator itself rather than pattern-matching its output: a
821
+ // page generated from a spec gets `squeezeToPageDescription(operation)` as
822
+ // its frontmatter description, so "the subtitle is that squeeze" is exactly
823
+ // the redundant case and nothing else is. A hand-written matcher for the
824
+ // shapes the squeeze happens to emit today would silently stop matching the
825
+ // day the squeeze changes — and an author's own short lead-in that merely
826
+ // opens like the operation is still theirs, which a looser prefix test would
827
+ // have deleted.
828
+ //
829
+ // Nothing is lost by dropping it — the frontmatter description's real job is
830
+ // <meta>/OG/llms.txt, and those read `data.description` directly.
831
+ const subtitleRedundant = Boolean(
832
+ flatSubtitle &&
833
+ flatOperation &&
834
+ !descriptionAlreadyShown &&
835
+ flatSubtitle === flattenText(squeezeToPageDescription(operationDescription)),
785
836
  );
786
837
 
787
838
  // ── Single pre-render compile pass ("docs never 500") ──────────────────────
@@ -998,8 +1049,8 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
998
1049
  and THIS is the only place that markdown renders. Without it
999
1050
  a table or list here renders bare. `:only-child` keeps the
1000
1051
  ordinary one-paragraph subtitle flush. */}
1001
- {data.description && (
1002
- <div className={`text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3 ${MARKDOWN_BLOCK_STRUCTURE} [&>p:only-child]:m-0 [&_code]:text-[0.9em] [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded`}>
1052
+ {data.description && !subtitleRedundant && (
1053
+ <div className={SUBTITLE_PROSE}>
1003
1054
  <ReactMarkdown remarkPlugins={[[remarkGfm, { singleTilde: false }]]}>{subtitleMarkdown}</ReactMarkdown>
1004
1055
  </div>
1005
1056
  )}
@@ -1121,7 +1172,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
1121
1172
  {hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
1122
1173
  </div>
1123
1174
  {data.description && (
1124
- <div className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3 [&>p]:m-0 [&_code]:text-[0.9em] [&_code]:bg-[var(--color-bg-tertiary)] [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded">
1175
+ <div className={SUBTITLE_PROSE}>
1125
1176
  <ReactMarkdown remarkPlugins={[[remarkGfm, { singleTilde: false }]]}>{subtitleMarkdown}</ReactMarkdown>
1126
1177
  </div>
1127
1178
  )}
@@ -686,7 +686,15 @@ export function generateLlmsFullTxt(options: LlmsFullTxtOptions): string {
686
686
  // therefore never matched for any soft-wrapped description — the one case
687
687
  // this dedup exists for.
688
688
  const flat = (t: string) => t.replace(/\s+/g, ' ');
689
- if (description && !flat(content).includes(flat(description))) {
689
+ // The trailing ellipsis is squeezeToPageDescription's truncation marker, and
690
+ // the body it was cut from never contains it — so `includes` missed exactly
691
+ // the shape this dedup exists for and emitted a visibly cut-off line above a
692
+ // body repeating it in full. A third of real operations squeeze that way.
693
+ // Dropping the marker before the test costs authored prose nothing: an
694
+ // authored description only loses its line when the body already says it,
695
+ // which is the rule this check already applies to every other page.
696
+ const spoken = description.replace(/\u2026$/, '');
697
+ if (spoken && !flat(content).includes(flat(spoken))) {
690
698
  parts.push(`*${description}*\n\n`);
691
699
  }
692
700