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.
- package/dist/__tests__/unit/frontmatter-sync.test.d.ts +2 -1
- package/dist/__tests__/unit/frontmatter-sync.test.d.ts.map +1 -1
- package/dist/__tests__/unit/frontmatter-sync.test.js +17 -2
- package/dist/__tests__/unit/frontmatter-sync.test.js.map +1 -1
- package/dist/lib/frontmatter-text.d.ts +24 -0
- package/dist/lib/frontmatter-text.d.ts.map +1 -0
- package/dist/lib/frontmatter-text.js +41 -0
- package/dist/lib/frontmatter-text.js.map +1 -0
- package/dist/lib/frontmatter-utils.d.ts +1 -18
- package/dist/lib/frontmatter-utils.d.ts.map +1 -1
- package/dist/lib/frontmatter-utils.js +5 -24
- package/dist/lib/frontmatter-utils.js.map +1 -1
- package/package.json +1 -1
- package/vendored/components/mdx/ApiMarkdown.tsx +19 -3
- package/vendored/components/mdx/OpenApiEndpoint.tsx +2 -2
- package/vendored/components/mdx/markdown-classes.ts +60 -0
- package/vendored/lib/frontmatter-text.ts +39 -0
- package/vendored/lib/frontmatter-utils.ts +5 -22
- package/vendored/lib/openapi/expand-spec-pages.ts +2 -32
- package/vendored/lib/openapi/squeeze-page-description.ts +53 -0
- package/vendored/lib/render-doc-page.tsx +57 -6
- package/vendored/lib/static-artifacts.ts +9 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sync Verification Test
|
|
3
3
|
*
|
|
4
|
-
* Ensures the frontmatter-utils.ts file
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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={
|
|
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=
|
|
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
|
-
|
|
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
|
|