jamdesk 1.1.148 → 1.1.150
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/README.md +28 -0
- package/dist/__tests__/integration/fix-scan-integration.test.d.ts +2 -0
- package/dist/__tests__/integration/fix-scan-integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/fix-scan-integration.test.js +77 -0
- package/dist/__tests__/integration/fix-scan-integration.test.js.map +1 -0
- package/dist/__tests__/unit/ai-fix-client.test.d.ts +2 -0
- package/dist/__tests__/unit/ai-fix-client.test.d.ts.map +1 -0
- package/dist/__tests__/unit/ai-fix-client.test.js +64 -0
- package/dist/__tests__/unit/ai-fix-client.test.js.map +1 -0
- package/dist/__tests__/unit/docs-config.test.js +66 -0
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/fix-ai-fallback.test.d.ts +2 -0
- package/dist/__tests__/unit/fix-ai-fallback.test.d.ts.map +1 -0
- package/dist/__tests__/unit/fix-ai-fallback.test.js +82 -0
- package/dist/__tests__/unit/fix-ai-fallback.test.js.map +1 -0
- package/dist/__tests__/unit/fix-engine-sync.test.d.ts +2 -0
- package/dist/__tests__/unit/fix-engine-sync.test.d.ts.map +1 -0
- package/dist/__tests__/unit/fix-engine-sync.test.js +16 -0
- package/dist/__tests__/unit/fix-engine-sync.test.js.map +1 -0
- package/dist/__tests__/unit/fix.test.d.ts +2 -0
- package/dist/__tests__/unit/fix.test.d.ts.map +1 -0
- package/dist/__tests__/unit/fix.test.js +61 -0
- package/dist/__tests__/unit/fix.test.js.map +1 -0
- package/dist/__tests__/unit/frontmatter-write.test.d.ts +2 -0
- package/dist/__tests__/unit/frontmatter-write.test.d.ts.map +1 -0
- package/dist/__tests__/unit/frontmatter-write.test.js +31 -0
- package/dist/__tests__/unit/frontmatter-write.test.js.map +1 -0
- package/dist/__tests__/unit/heading-extractor-sync.test.d.ts +2 -0
- package/dist/__tests__/unit/heading-extractor-sync.test.d.ts.map +1 -0
- package/dist/__tests__/unit/heading-extractor-sync.test.js +16 -0
- package/dist/__tests__/unit/heading-extractor-sync.test.js.map +1 -0
- package/dist/__tests__/unit/missing-description-scan.test.d.ts +2 -0
- package/dist/__tests__/unit/missing-description-scan.test.d.ts.map +1 -0
- package/dist/__tests__/unit/missing-description-scan.test.js +36 -0
- package/dist/__tests__/unit/missing-description-scan.test.js.map +1 -0
- package/dist/__tests__/unit/seo-ai-schema.test.d.ts +2 -0
- package/dist/__tests__/unit/seo-ai-schema.test.d.ts.map +1 -0
- package/dist/__tests__/unit/seo-ai-schema.test.js +40 -0
- package/dist/__tests__/unit/seo-ai-schema.test.js.map +1 -0
- package/dist/commands/fix.d.ts +97 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +331 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/ai-fix-client.d.ts +27 -0
- package/dist/lib/ai-fix-client.d.ts.map +1 -0
- package/dist/lib/ai-fix-client.js +45 -0
- package/dist/lib/ai-fix-client.js.map +1 -0
- package/dist/lib/deps.js +1 -1
- package/dist/lib/deps.js.map +1 -1
- package/dist/lib/docs-config.d.ts +1 -1
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +59 -4
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/fix-engine.d.ts +89 -0
- package/dist/lib/fix-engine.d.ts.map +1 -0
- package/dist/lib/fix-engine.js +236 -0
- package/dist/lib/fix-engine.js.map +1 -0
- package/dist/lib/frontmatter-write.d.ts +4 -0
- package/dist/lib/frontmatter-write.d.ts.map +1 -0
- package/dist/lib/frontmatter-write.js +10 -0
- package/dist/lib/frontmatter-write.js.map +1 -0
- package/dist/lib/heading-extractor.d.ts +63 -0
- package/dist/lib/heading-extractor.d.ts.map +1 -0
- package/dist/lib/heading-extractor.js +171 -0
- package/dist/lib/heading-extractor.js.map +1 -0
- package/dist/lib/missing-description-scan.d.ts +7 -0
- package/dist/lib/missing-description-scan.d.ts.map +1 -0
- package/dist/lib/missing-description-scan.js +20 -0
- package/dist/lib/missing-description-scan.js.map +1 -0
- package/package.json +3 -3
- package/vendored/components/mdx/Accordion.tsx +6 -1
- package/vendored/components/mdx/ApiEndpoint.tsx +1 -1
- package/vendored/components/mdx/MDXComponents.tsx +14 -1
- package/vendored/components/mdx/MermaidInner.tsx +2 -8
- package/vendored/components/mdx/OpenApiEndpoint.tsx +7 -2
- package/vendored/components/navigation/SocialFooter.tsx +9 -1
- package/vendored/lib/build/error-parser.ts +6 -3
- package/vendored/lib/docs-types.ts +1 -0
- package/vendored/lib/extract-faq.ts +54 -0
- package/vendored/lib/firestore-helpers.ts +2 -3
- package/vendored/lib/fix-engine.ts +293 -0
- package/vendored/lib/health-checks.ts +2 -2
- package/vendored/lib/json-ld.ts +113 -10
- package/vendored/lib/layout-helpers.tsx +14 -15
- package/vendored/lib/llms-flag.ts +5 -0
- package/vendored/lib/logo-warning.ts +35 -0
- package/vendored/lib/noindex-warning.ts +14 -0
- package/vendored/lib/r2-cleanup.ts +26 -0
- package/vendored/lib/render-doc-page.tsx +11 -2
- package/vendored/lib/search.ts +2 -2
- package/vendored/lib/seo.ts +49 -19
- package/vendored/lib/static-artifacts.ts +2 -2
- package/vendored/lib/ui-strings.ts +6 -0
- package/vendored/lib/validate-config.ts +63 -4
- package/vendored/lib/validate-page-frontmatter.ts +30 -0
- package/vendored/schema/docs-schema.json +10 -0
- package/vendored/scripts/build-search-index.cjs +2 -2
- package/vendored/shared/status-reporter.ts +27 -1
- package/vendored/shared/types.ts +1 -0
- package/vendored/workspace-package-lock.json +39 -39
package/vendored/lib/json-ld.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON-LD Structured Data for Documentation Pages
|
|
3
3
|
*
|
|
4
|
-
* Generates WebSite + BreadcrumbList schemas for search engine rich results.
|
|
4
|
+
* Generates WebSite + Organization + BreadcrumbList schemas for search engine rich results.
|
|
5
5
|
* Rendered as <script type="application/ld+json"> in page.tsx.
|
|
6
6
|
*
|
|
7
7
|
* Breadcrumb path extraction is server-side only (pure functions, no hooks).
|
|
8
8
|
* Intentionally separate from Breadcrumb.tsx which is a 'use client' component.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { DocsConfig, NavigationPage, GroupConfig } from './docs-types';
|
|
12
|
-
import { normalizeNavPage } from './docs-types';
|
|
11
|
+
import type { DocsConfig, NavigationPage, GroupConfig, LanguageCode } from './docs-types';
|
|
12
|
+
import { normalizeNavPage, normalizeLogo } from './docs-types';
|
|
13
|
+
import { normalizeLanguageList, deriveChunkLocale } from './locale-helpers';
|
|
14
|
+
import { getUiStrings } from './ui-strings';
|
|
15
|
+
import type { FaqPair } from './extract-faq';
|
|
13
16
|
|
|
14
17
|
interface BreadcrumbEntry {
|
|
15
18
|
name: string;
|
|
@@ -21,6 +24,11 @@ export interface JsonLdOptions {
|
|
|
21
24
|
pagePath: string;
|
|
22
25
|
pageTitle: string;
|
|
23
26
|
baseUrl: string;
|
|
27
|
+
faqPairs?: FaqPair[];
|
|
28
|
+
isRootAlias?: boolean;
|
|
29
|
+
ogImageUrl?: string;
|
|
30
|
+
dateModified?: string;
|
|
31
|
+
author?: string;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export interface JsonLdGraph {
|
|
@@ -67,7 +75,14 @@ export function findBreadcrumbPath(
|
|
|
67
75
|
config: DocsConfig, targetSlug: string, baseUrl: string, pageTitle?: string
|
|
68
76
|
): BreadcrumbEntry[] {
|
|
69
77
|
const nav = config.navigation;
|
|
70
|
-
|
|
78
|
+
// Localize the root crumb label so a Spanish/French page's breadcrumb rich
|
|
79
|
+
// result reads "Inicio"/"Accueil", not "Home". deriveChunkLocale only treats
|
|
80
|
+
// the leading slug segment as a locale when it's a configured language (and
|
|
81
|
+
// falls back to the tenant's default language), so single-language sites are
|
|
82
|
+
// unaffected. The visual breadcrumb uses a house icon, so this is JSON-LD only.
|
|
83
|
+
const locale = deriveChunkLocale(targetSlug, normalizeLanguageList(nav?.languages));
|
|
84
|
+
const homeLabel = getUiStrings((locale ?? undefined) as LanguageCode | undefined).breadcrumbHome;
|
|
85
|
+
const home: BreadcrumbEntry = { name: homeLabel, url: baseUrl };
|
|
71
86
|
const pageEntry: BreadcrumbEntry = {
|
|
72
87
|
name: pageTitle || formatSlugAsTitle(targetSlug),
|
|
73
88
|
url: `${baseUrl}/${targetSlug}`,
|
|
@@ -106,7 +121,7 @@ export function findBreadcrumbPath(
|
|
|
106
121
|
// Search all navigation structures (mirrors Breadcrumb.tsx order)
|
|
107
122
|
let trail: BreadcrumbEntry[] | null = null;
|
|
108
123
|
|
|
109
|
-
if (!trail && nav.languages) {
|
|
124
|
+
if (nav && !trail && nav.languages) {
|
|
110
125
|
for (const lang of nav.languages) {
|
|
111
126
|
if (!lang.tabs) continue;
|
|
112
127
|
for (const tab of lang.tabs) {
|
|
@@ -116,19 +131,19 @@ export function findBreadcrumbPath(
|
|
|
116
131
|
if (trail) break;
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
|
-
if (!trail && nav.anchors) {
|
|
134
|
+
if (nav && !trail && nav.anchors) {
|
|
120
135
|
for (const anchor of nav.anchors) {
|
|
121
136
|
if (anchor.groups) trail = searchGroups(anchor.groups);
|
|
122
137
|
if (trail) break;
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
|
-
if (!trail && nav.tabs) {
|
|
140
|
+
if (nav && !trail && nav.tabs) {
|
|
126
141
|
for (const tab of nav.tabs) {
|
|
127
142
|
if (tab.groups) trail = searchGroups(tab.groups);
|
|
128
143
|
if (trail) break;
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
|
-
if (!trail && nav.groups) {
|
|
146
|
+
if (nav && !trail && nav.groups) {
|
|
132
147
|
trail = searchGroups(nav.groups);
|
|
133
148
|
}
|
|
134
149
|
|
|
@@ -140,25 +155,102 @@ export function findBreadcrumbPath(
|
|
|
140
155
|
);
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
// sameAs is for identity/disambiguation PROFILES, not the company homepage.
|
|
159
|
+
// `website` and `earth-americas` are both homepage-link social keys — excluded
|
|
160
|
+
// (along with any empty values) so the entity isn't muddied with a non-profile URL.
|
|
161
|
+
const NON_PROFILE_SOCIALS = new Set(['website', 'earth-americas']);
|
|
162
|
+
|
|
163
|
+
/** Absolute brand-logo URL for the Organization node, or undefined if no logo. */
|
|
164
|
+
function resolveLogoUrl(config: DocsConfig, baseUrl: string): string | undefined {
|
|
165
|
+
const norm = normalizeLogo(config.logo);
|
|
166
|
+
const path = norm?.light || norm?.dark;
|
|
167
|
+
if (!path) return undefined;
|
|
168
|
+
if (path.startsWith('http')) return path;
|
|
169
|
+
const rootUrl = baseUrl.replace(/\/docs$/, '');
|
|
170
|
+
return `${rootUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
143
173
|
/**
|
|
144
174
|
* Build JSON-LD structured data for a documentation page.
|
|
145
|
-
* Returns a schema.org @graph with WebSite + BreadcrumbList.
|
|
175
|
+
* Returns a schema.org @graph with WebSite + Organization + BreadcrumbList (+ optional FAQPage).
|
|
146
176
|
*/
|
|
147
177
|
export function buildJsonLd(options: JsonLdOptions): JsonLdGraph {
|
|
148
|
-
const {
|
|
178
|
+
const {
|
|
179
|
+
config, pagePath, pageTitle, baseUrl, faqPairs,
|
|
180
|
+
isRootAlias = false, ogImageUrl, dateModified, author: pageAuthor,
|
|
181
|
+
} = options;
|
|
149
182
|
const breadcrumbs = findBreadcrumbPath(config, pagePath, baseUrl, pageTitle);
|
|
183
|
+
const pageUrl = breadcrumbs.length ? breadcrumbs[breadcrumbs.length - 1].url : baseUrl;
|
|
184
|
+
|
|
185
|
+
const websiteId = `${baseUrl}#website`;
|
|
186
|
+
const orgId = `${baseUrl}#organization`;
|
|
187
|
+
const breadcrumbId = `${pageUrl}#breadcrumb`;
|
|
188
|
+
const faqId = `${pageUrl}#faq`;
|
|
189
|
+
|
|
190
|
+
const logoUrl = resolveLogoUrl(config, baseUrl);
|
|
191
|
+
const socials = config.footer?.socials
|
|
192
|
+
? Object.entries(config.footer.socials)
|
|
193
|
+
.filter(([key, url]) => Boolean(url) && !NON_PROFILE_SOCIALS.has(key))
|
|
194
|
+
.map(([, url]) => url as string)
|
|
195
|
+
: [];
|
|
196
|
+
|
|
197
|
+
const organization: Record<string, unknown> = {
|
|
198
|
+
'@type': 'Organization',
|
|
199
|
+
'@id': orgId,
|
|
200
|
+
name: config.name,
|
|
201
|
+
url: baseUrl,
|
|
202
|
+
...(logoUrl && { logo: { '@type': 'ImageObject', url: logoUrl } }),
|
|
203
|
+
...(socials.length && { sameAs: socials }),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const mainId = `${pageUrl}#${isRootAlias ? 'webpage' : 'article'}`;
|
|
207
|
+
const author = pageAuthor
|
|
208
|
+
? { '@type': 'Person', name: pageAuthor }
|
|
209
|
+
: { '@id': orgId };
|
|
210
|
+
|
|
211
|
+
const mainNode: Record<string, unknown> = isRootAlias
|
|
212
|
+
? {
|
|
213
|
+
'@type': 'WebPage',
|
|
214
|
+
'@id': mainId,
|
|
215
|
+
name: pageTitle,
|
|
216
|
+
url: pageUrl,
|
|
217
|
+
isPartOf: { '@id': websiteId },
|
|
218
|
+
// `breadcrumb` is a WebPage-only schema.org property, so it belongs here but
|
|
219
|
+
// NOT on the TechArticle branch below — the standalone BreadcrumbList node
|
|
220
|
+
// carries the trail for article pages (putting it on an Article fails validation).
|
|
221
|
+
breadcrumb: { '@id': breadcrumbId },
|
|
222
|
+
...(ogImageUrl && { image: ogImageUrl }),
|
|
223
|
+
...(dateModified && { dateModified }),
|
|
224
|
+
}
|
|
225
|
+
: {
|
|
226
|
+
'@type': 'TechArticle',
|
|
227
|
+
'@id': mainId,
|
|
228
|
+
headline: pageTitle,
|
|
229
|
+
url: pageUrl,
|
|
230
|
+
isPartOf: { '@id': websiteId },
|
|
231
|
+
mainEntityOfPage: pageUrl,
|
|
232
|
+
author,
|
|
233
|
+
publisher: { '@id': orgId },
|
|
234
|
+
...(ogImageUrl && { image: ogImageUrl }),
|
|
235
|
+
...(dateModified && { dateModified }),
|
|
236
|
+
};
|
|
150
237
|
|
|
151
238
|
return {
|
|
152
239
|
'@context': 'https://schema.org',
|
|
153
240
|
'@graph': [
|
|
154
241
|
{
|
|
155
242
|
'@type': 'WebSite',
|
|
243
|
+
'@id': websiteId,
|
|
156
244
|
name: config.name,
|
|
157
245
|
url: baseUrl,
|
|
158
246
|
...(config.description && { description: config.description }),
|
|
247
|
+
publisher: { '@id': orgId },
|
|
159
248
|
},
|
|
249
|
+
organization,
|
|
250
|
+
mainNode,
|
|
160
251
|
{
|
|
161
252
|
'@type': 'BreadcrumbList',
|
|
253
|
+
'@id': breadcrumbId,
|
|
162
254
|
itemListElement: breadcrumbs.map((item, i) => ({
|
|
163
255
|
'@type': 'ListItem',
|
|
164
256
|
position: i + 1,
|
|
@@ -166,6 +258,17 @@ export function buildJsonLd(options: JsonLdOptions): JsonLdGraph {
|
|
|
166
258
|
item: item.url,
|
|
167
259
|
})),
|
|
168
260
|
},
|
|
261
|
+
...(faqPairs && faqPairs.length
|
|
262
|
+
? [{
|
|
263
|
+
'@type': 'FAQPage',
|
|
264
|
+
'@id': faqId,
|
|
265
|
+
isPartOf: { '@id': websiteId },
|
|
266
|
+
mainEntity: faqPairs.map((p) => ({
|
|
267
|
+
'@type': 'Question', name: p.question,
|
|
268
|
+
acceptedAnswer: { '@type': 'Answer', text: p.answer },
|
|
269
|
+
})),
|
|
270
|
+
}]
|
|
271
|
+
: []),
|
|
169
272
|
],
|
|
170
273
|
};
|
|
171
274
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// or — in non-ISR dev/tests — from URL params; this module owns the
|
|
4
4
|
// rendering once those have resolved.
|
|
5
5
|
import { Inter, JetBrains_Mono } from 'next/font/google';
|
|
6
|
-
import {
|
|
6
|
+
import { preinit } from 'react-dom';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { ThemeProvider } from '@/components/theme/ThemeProvider';
|
|
@@ -454,22 +454,21 @@ export async function DocsChrome({
|
|
|
454
454
|
const bannerNoFlash = !embed && !!bannerContent && config.banner?.dismissible === true && !!resolvedProjectSlug;
|
|
455
455
|
|
|
456
456
|
// Font Awesome CSS uses preinit (not preload) so React 19 emits the
|
|
457
|
-
// stylesheet link in <head> at SSR time.
|
|
458
|
-
// beforeInteractive Script
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
// are matched to their @font-face declarations as soon as the stylesheet
|
|
463
|
-
// parses, which now starts at HTML-parse time alongside the preloads.
|
|
457
|
+
// stylesheet link in <head> at SSR time. preinit (rather than a runtime
|
|
458
|
+
// beforeInteractive <Script>/<link> injection) is what gets the @font-face
|
|
459
|
+
// rules parsed early: the previous Script that ran document.createElement at
|
|
460
|
+
// parse time delayed @font-face parsing, so the actual woff2 fetch started
|
|
461
|
+
// late. preinit hoists the stylesheet at HTML-parse time instead.
|
|
464
462
|
//
|
|
465
|
-
// The
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
463
|
+
// The individual woff2 weights are deliberately NOT preloaded — they load on
|
|
464
|
+
// demand when the stylesheet's @font-face rules are matched to the icons a
|
|
465
|
+
// page actually uses. The default docs icon weight is solid, so the previous
|
|
466
|
+
// unconditional fa-light-300 / fa-brands-400 preloads were almost never used
|
|
467
|
+
// and Chrome flagged them "preloaded but not used within a few seconds."
|
|
468
|
+
// The <noscript> fallback below uses dangerouslySetInnerHTML because a JSX
|
|
469
|
+
// <link rel=stylesheet> inside noscript was being hoisted into <head> as a
|
|
470
|
+
// real stylesheet by React 19.
|
|
470
471
|
preinit(FA_CSS_HREF, { as: 'style' });
|
|
471
|
-
preload('/_jd/fonts/fontawesome/webfonts/fa-light-300.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
|
|
472
|
-
preload('/_jd/fonts/fontawesome/webfonts/fa-brands-400.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
|
|
473
472
|
|
|
474
473
|
return (
|
|
475
474
|
<html lang={toHreflang(lang)} dir={dir} suppressHydrationWarning data-scroll-behavior="smooth" data-scroll-locked="true">
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
|
|
2
|
+
import type { Logo } from './docs-types.js';
|
|
3
|
+
|
|
4
|
+
const MAX_LOGO_BYTES = 100 * 1024;
|
|
5
|
+
const MAX_REASONABLE_WIDTH = 720; // 2x the LOGO_MAX_WIDTH target
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect the distinct image refs from a logo config (string or {light,dark}),
|
|
9
|
+
* in light-then-dark order with empties dropped.
|
|
10
|
+
*
|
|
11
|
+
* Used to SNAPSHOT the originals before the convertToWebp pass can rewrite them:
|
|
12
|
+
* an object {light,dark} logo shares its nested object across the enhancedConfig
|
|
13
|
+
* shallow spread, so a later in-place ".webp" rewrite mutates docsConfig.logo too —
|
|
14
|
+
* destroying the ".png"/".jpg" refs the always-on downscale needs. Reading from a
|
|
15
|
+
* pre-convert snapshot makes the downscale work for object logos, not just strings.
|
|
16
|
+
*/
|
|
17
|
+
export function collectLogoRefs(logo: Logo | undefined): string[] {
|
|
18
|
+
const refs: string[] = [];
|
|
19
|
+
if (typeof logo === 'string') {
|
|
20
|
+
refs.push(logo);
|
|
21
|
+
} else if (logo && typeof logo === 'object') {
|
|
22
|
+
if (logo.light) refs.push(logo.light);
|
|
23
|
+
if (logo.dark) refs.push(logo.dark);
|
|
24
|
+
}
|
|
25
|
+
return [...new Set(refs.filter(Boolean))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildLogoWarning(logo: { path: string; width: number; height: number; bytes: number }): BuildWarning | null {
|
|
29
|
+
if (logo.bytes <= MAX_LOGO_BYTES && logo.width <= MAX_REASONABLE_WIDTH) return null;
|
|
30
|
+
return {
|
|
31
|
+
type: 'logo_oversized',
|
|
32
|
+
file: logo.path,
|
|
33
|
+
message: `Logo is ${logo.width}×${logo.height} (${Math.round(logo.bytes / 1024)} KB). It's auto-downscaled, but uploading a logo ≤ ${MAX_REASONABLE_WIDTH}px wide / ≤ 100 KB avoids shipping a large source.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// NOTE: lib/*.ts import the VENDORED shared copy at build-service/shared via
|
|
2
|
+
// '../shared/...js' (see other lib files). '../../shared' would resolve outside the
|
|
3
|
+
// build-service package and break tsc + the prod build.
|
|
4
|
+
import type { BuildWarning } from '../shared/status-reporter.js';
|
|
5
|
+
|
|
6
|
+
export function buildNoindexWarning(noindex: boolean): BuildWarning | null {
|
|
7
|
+
if (!noindex) return null;
|
|
8
|
+
return {
|
|
9
|
+
type: 'site_noindex',
|
|
10
|
+
file: 'docs.json',
|
|
11
|
+
message:
|
|
12
|
+
'This site is set to noindex: robots.txt is "Disallow: /" and the sitemap is empty, so it will not be indexed by any search engines. AI crawlers are still served via llms.txt (set seo.ai.llmsTxt: false to disable). Remove "noindex" from seo.metatags.robots to allow search indexing.',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -442,6 +442,32 @@ export async function sweepStaleLocaleLlmsTxt(
|
|
|
442
442
|
return staleKeys;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Delete a project's AI-context files: the root `llms.txt` + `llms-full.txt` and
|
|
447
|
+
* every per-locale `<locale>/llms.txt`. Called on every build where `seo.ai.llmsTxt`
|
|
448
|
+
* is false so a previously published llms.txt stops serving after a customer opts
|
|
449
|
+
* out — the build gate stops REGENERATING these files but otherwise leaves the
|
|
450
|
+
* already-uploaded copies in place. The deletes are idempotent (DeleteObject is a
|
|
451
|
+
* no-op-success on a missing key), so re-running on later disabled-llms builds is
|
|
452
|
+
* harmless and self-healing. Per-locale files are discovered and deleted by
|
|
453
|
+
* sweepStaleLocaleLlmsTxt with an EMPTY active-locale set (every known-language
|
|
454
|
+
* locale then counts as removable). Note the returned array lists the keys
|
|
455
|
+
* TARGETED, not necessarily ones that existed. Non-fatal by contract: callers treat
|
|
456
|
+
* a throw as a warning — a stale llms.txt is a cosmetic orphan, never build-failing.
|
|
457
|
+
*/
|
|
458
|
+
export async function deleteLlmsTxtArtifacts(projectSlug: string): Promise<string[]> {
|
|
459
|
+
const localeKeys = await sweepStaleLocaleLlmsTxt(projectSlug, new Set());
|
|
460
|
+
const client = getR2S3Client();
|
|
461
|
+
const { bucketName } = getR2Config();
|
|
462
|
+
const rootKeys = [`${projectSlug}/llms.txt`, `${projectSlug}/llms-full.txt`];
|
|
463
|
+
// Independent keys — delete concurrently (matches deleteRemovedR2Objects' Promise.all).
|
|
464
|
+
// A rejection propagates so the caller's non-fatal try/catch logs it; see doc above.
|
|
465
|
+
await Promise.all(
|
|
466
|
+
rootKeys.map((key) => client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: key }))),
|
|
467
|
+
);
|
|
468
|
+
return [...localeKeys, ...rootKeys];
|
|
469
|
+
}
|
|
470
|
+
|
|
445
471
|
/**
|
|
446
472
|
* Delete EVERY R2 object under the `{slug}/` prefix.
|
|
447
473
|
*
|
|
@@ -61,8 +61,9 @@ import { buildEndpointFromMdx } from '@/lib/build-endpoint-from-mdx';
|
|
|
61
61
|
import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
62
62
|
import { getContentDir } from '@/lib/docs';
|
|
63
63
|
import type { DocsConfig } from '@/lib/docs-types';
|
|
64
|
-
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
64
|
+
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle, buildPageOgImageUrl } from '@/lib/seo';
|
|
65
65
|
import { buildJsonLd } from '@/lib/json-ld';
|
|
66
|
+
import { extractFaqPairs } from '@/lib/extract-faq';
|
|
66
67
|
import {
|
|
67
68
|
getContentLoader,
|
|
68
69
|
isIsrMode,
|
|
@@ -317,7 +318,8 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
317
318
|
const highlighterP = getHighlighter();
|
|
318
319
|
|
|
319
320
|
const normalizedSlug = normalizeSlugForContent(slugInput || [], hostAtDocs);
|
|
320
|
-
const
|
|
321
|
+
const isRootAlias = needsSlugRewrite(normalizedSlug);
|
|
322
|
+
const slug = isRootAlias
|
|
321
323
|
? resolveSlug(normalizedSlug, await configP)
|
|
322
324
|
: normalizedSlug;
|
|
323
325
|
const pagePath = slug.join('/');
|
|
@@ -337,11 +339,18 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
337
339
|
const rawContent = parsed.content;
|
|
338
340
|
|
|
339
341
|
const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
342
|
+
const faqPairs = extractFaqPairs(rawContent);
|
|
343
|
+
const ogImageUrl = buildPageOgImageUrl(config, data, baseUrl);
|
|
340
344
|
const jsonLd = buildJsonLd({
|
|
341
345
|
config,
|
|
342
346
|
pagePath,
|
|
343
347
|
pageTitle: data.title || pagePath,
|
|
344
348
|
baseUrl,
|
|
349
|
+
faqPairs,
|
|
350
|
+
isRootAlias,
|
|
351
|
+
ogImageUrl,
|
|
352
|
+
dateModified: typeof data.lastUpdated === 'string' ? data.lastUpdated : undefined,
|
|
353
|
+
author: typeof data.author === 'string' ? data.author : undefined,
|
|
345
354
|
});
|
|
346
355
|
const jsonLdScript = renderJsonLdScript(jsonLd);
|
|
347
356
|
|
package/vendored/lib/search.ts
CHANGED
|
@@ -23,8 +23,8 @@ let searchDocuments: SearchResult[] = [];
|
|
|
23
23
|
|
|
24
24
|
function stripMarkdown(text: string): string {
|
|
25
25
|
return text
|
|
26
|
-
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
27
|
-
.replace(/`[^`]
|
|
26
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks (too noisy to index)
|
|
27
|
+
.replace(/`([^`]+)`/g, '$1') // Inline code → keep text, drop backticks (config keys like starterQuestions stay searchable)
|
|
28
28
|
.replace(/#{1,6}\s/g, '') // Remove heading markers
|
|
29
29
|
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold
|
|
30
30
|
.replace(/\*([^*]+)\*/g, '$1') // Remove italic
|
package/vendored/lib/seo.ts
CHANGED
|
@@ -132,6 +132,34 @@ function buildOgImageUrl(
|
|
|
132
132
|
return `${rootUrl}${ogPath}?${params.toString()}`;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Public OG-card URL builder. Mirrors the global+page metatag merge that
|
|
137
|
+
* buildSeoMetadata performs, so callers outside the metadata path (the JSON-LD
|
|
138
|
+
* layer) can set an article image to the exact same card the og:image points at.
|
|
139
|
+
*/
|
|
140
|
+
export function buildPageOgImageUrl(
|
|
141
|
+
config: DocsConfig,
|
|
142
|
+
frontmatter: PageFrontmatter,
|
|
143
|
+
baseUrl: string,
|
|
144
|
+
): string {
|
|
145
|
+
const globalMeta = config.seo?.metatags || {};
|
|
146
|
+
const pageMeta = normalizePageMetatags(frontmatter as Record<string, unknown>);
|
|
147
|
+
const metatags: Record<string, string> = { ...globalMeta, ...pageMeta };
|
|
148
|
+
const customImage = metatags['og:image']?.trim();
|
|
149
|
+
return buildOgImageUrl(
|
|
150
|
+
baseUrl,
|
|
151
|
+
metatags['og:title'] || frontmatter.title || 'Documentation',
|
|
152
|
+
config.name,
|
|
153
|
+
{
|
|
154
|
+
description: metatags['og:description'] || frontmatter.description,
|
|
155
|
+
section: frontmatter.section,
|
|
156
|
+
logo: config.logo,
|
|
157
|
+
favicon: config.favicon,
|
|
158
|
+
bg: customImage || undefined,
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
/**
|
|
136
164
|
* Coerce a raw frontmatter value into a meta-tag string.
|
|
137
165
|
* Mintlify allows numbers (`og:image:width: 1200`) and arrays
|
|
@@ -615,6 +643,18 @@ function derivePageLocale(pagePath: string, languages?: LanguageConfig[]): strin
|
|
|
615
643
|
* the full hreflang cluster, so language discovery is preserved.
|
|
616
644
|
* @returns Partial<Metadata> to spread into generateMetadata return
|
|
617
645
|
*/
|
|
646
|
+
|
|
647
|
+
// Next.js Metadata.robots can be a string OR an object. When we must ALSO emit
|
|
648
|
+
// googlebot (object-only), a raw string robots directive would be lost — convert
|
|
649
|
+
// it to object form first so the general directive survives the merge.
|
|
650
|
+
function parseRobotsString(robots: string): { index: boolean; follow: boolean } {
|
|
651
|
+
const tokens = robots.toLowerCase().split(',').map((t) => t.trim());
|
|
652
|
+
return {
|
|
653
|
+
index: !tokens.includes('noindex'),
|
|
654
|
+
follow: !tokens.includes('nofollow'),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
618
658
|
export function buildSeoMetadata(
|
|
619
659
|
config: DocsConfig,
|
|
620
660
|
frontmatter: PageFrontmatter,
|
|
@@ -653,12 +693,14 @@ export function buildSeoMetadata(
|
|
|
653
693
|
|
|
654
694
|
// 3. Googlebot (separate from robots)
|
|
655
695
|
if (metatags.googlebot) {
|
|
656
|
-
// Merge googlebot with existing robots config
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
696
|
+
// Merge googlebot with existing robots config. When metadata.robots is a string
|
|
697
|
+
// (set above from metatags.robots), coerce it to object form first so the general
|
|
698
|
+
// directive (e.g. noindex) is not silently dropped by the spread.
|
|
699
|
+
const base =
|
|
700
|
+
typeof metadata.robots === 'string'
|
|
701
|
+
? parseRobotsString(metadata.robots)
|
|
702
|
+
: (metadata.robots ?? {});
|
|
703
|
+
metadata.robots = { ...base, googleBot: metatags.googlebot };
|
|
662
704
|
}
|
|
663
705
|
|
|
664
706
|
// 4. Google site verification
|
|
@@ -723,19 +765,7 @@ export function buildSeoMetadata(
|
|
|
723
765
|
// than a bare static image, so the emitted OG image is always the generated card.
|
|
724
766
|
// The card text follows og:title/og:description overrides so the image matches
|
|
725
767
|
// the link-card text built in buildOpenGraphMetadata.
|
|
726
|
-
const
|
|
727
|
-
const ogImageUrl = buildOgImageUrl(
|
|
728
|
-
baseUrl,
|
|
729
|
-
metatags['og:title'] || frontmatter.title || 'Documentation',
|
|
730
|
-
config.name,
|
|
731
|
-
{
|
|
732
|
-
description: metatags['og:description'] || frontmatter.description,
|
|
733
|
-
section: frontmatter.section,
|
|
734
|
-
logo: config.logo,
|
|
735
|
-
favicon: config.favicon,
|
|
736
|
-
bg: customImage || undefined,
|
|
737
|
-
},
|
|
738
|
-
);
|
|
768
|
+
const ogImageUrl = buildPageOgImageUrl(config, frontmatter, baseUrl);
|
|
739
769
|
metadata.openGraph = buildOpenGraphMetadata(metatags, {
|
|
740
770
|
title: frontmatter.title,
|
|
741
771
|
description: frontmatter.description,
|
|
@@ -1003,8 +1003,8 @@ function inferPageType(slug: string): string {
|
|
|
1003
1003
|
*/
|
|
1004
1004
|
export function stripMarkdown(text: string): string {
|
|
1005
1005
|
return text
|
|
1006
|
-
.replace(/```[\s\S]*?```/g, '') // code blocks
|
|
1007
|
-
.replace(/`[^`]
|
|
1006
|
+
.replace(/```[\s\S]*?```/g, '') // code blocks (dropped — too noisy to index)
|
|
1007
|
+
.replace(/`([^`]+)`/g, '$1') // inline code → keep text, drop backticks (config keys like starterQuestions stay searchable)
|
|
1008
1008
|
.replace(/#{1,6}\s/g, '') // headings
|
|
1009
1009
|
.replace(/\*\*([^*]+)\*\*/g, '$1') // bold
|
|
1010
1010
|
.replace(/\*([^*]+)\*/g, '$1') // italic
|
|
@@ -6,6 +6,8 @@ export interface UiStrings {
|
|
|
6
6
|
more: string;
|
|
7
7
|
toggleMenu: string;
|
|
8
8
|
selectLanguage: string;
|
|
9
|
+
/** Root crumb label in the JSON-LD BreadcrumbList (Google rich result). */
|
|
10
|
+
breadcrumbHome: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const EN: UiStrings = {
|
|
@@ -14,6 +16,7 @@ const EN: UiStrings = {
|
|
|
14
16
|
more: 'More',
|
|
15
17
|
toggleMenu: 'Toggle menu',
|
|
16
18
|
selectLanguage: 'Select language',
|
|
19
|
+
breadcrumbHome: 'Home',
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
const ZH: Partial<UiStrings> = {
|
|
@@ -22,6 +25,7 @@ const ZH: Partial<UiStrings> = {
|
|
|
22
25
|
more: '更多',
|
|
23
26
|
toggleMenu: '切换菜单',
|
|
24
27
|
selectLanguage: '选择语言',
|
|
28
|
+
breadcrumbHome: '首页',
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
const OVERRIDES: Partial<Record<LanguageCode, Partial<UiStrings>>> = {
|
|
@@ -31,6 +35,7 @@ const OVERRIDES: Partial<Record<LanguageCode, Partial<UiStrings>>> = {
|
|
|
31
35
|
more: 'Plus',
|
|
32
36
|
toggleMenu: 'Basculer le menu',
|
|
33
37
|
selectLanguage: 'Choisir la langue',
|
|
38
|
+
breadcrumbHome: 'Accueil',
|
|
34
39
|
},
|
|
35
40
|
es: {
|
|
36
41
|
search: 'Buscar',
|
|
@@ -38,6 +43,7 @@ const OVERRIDES: Partial<Record<LanguageCode, Partial<UiStrings>>> = {
|
|
|
38
43
|
more: 'Más',
|
|
39
44
|
toggleMenu: 'Alternar menú',
|
|
40
45
|
selectLanguage: 'Elegir idioma',
|
|
46
|
+
breadcrumbHome: 'Inicio',
|
|
41
47
|
},
|
|
42
48
|
zh: ZH,
|
|
43
49
|
cn: ZH,
|
|
@@ -257,10 +257,65 @@ function deduplicateErrors(errors: ErrorObject[]): ErrorObject[] {
|
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
const NAV_BRANCH_KEYS = [
|
|
261
|
+
'products', 'languages', 'versions', 'tabs',
|
|
262
|
+
'dropdowns', 'groups', 'pages',
|
|
263
|
+
] as const;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Collapse the navigation anyOf noise. When the user's navigation clearly uses
|
|
267
|
+
* one discriminator key (e.g. `tabs`), drop the sibling-branch
|
|
268
|
+
* `required`/`additionalProperties` errors AT `/navigation` and keep the deeper
|
|
269
|
+
* child errors (e.g. /navigation/tabs/0) that say what's actually wrong. No-op
|
|
270
|
+
* when navigation is absent or the intent is ambiguous (0 or >1 known keys).
|
|
271
|
+
*
|
|
272
|
+
* IMPORTANT: key the predicate on `instancePath` + `keyword` + `params`, NOT
|
|
273
|
+
* `schemaPath`. Ajv resolves the `$ref: "#/definitions/navigationSchema"`, so
|
|
274
|
+
* the nav-branch errors carry schemaPaths like `#/anyOf/0/required` — the
|
|
275
|
+
* string "navigationSchema" never appears in them. (Verified by compiling the
|
|
276
|
+
* real schema against a malformed-tabs config.) `instancePath` is the stable
|
|
277
|
+
* signal: every sibling-branch red herring sits exactly at `/navigation`,
|
|
278
|
+
* while the actionable child error sits at `/navigation/<key>/...`.
|
|
279
|
+
* Runs BEFORE `filterAnyOfNoise`, so schemaPaths are still `#/anyOf/N/...` here.
|
|
280
|
+
*/
|
|
281
|
+
export function filterNavigationAnyOfNoise(
|
|
282
|
+
errors: ErrorObject[],
|
|
283
|
+
navigation: Record<string, unknown> | undefined,
|
|
284
|
+
): ErrorObject[] {
|
|
285
|
+
if (!navigation) return errors;
|
|
286
|
+
const present = NAV_BRANCH_KEYS.filter((k) => k in navigation);
|
|
287
|
+
// Only collapse noise when the user's intent is unambiguous — exactly one
|
|
288
|
+
// discriminator key. Zero keys or multiple keys (itself invalid) → leave
|
|
289
|
+
// the raw errors so we never hide the real problem.
|
|
290
|
+
if (present.length !== 1) return errors;
|
|
291
|
+
const usedKey = present[0];
|
|
292
|
+
|
|
293
|
+
return errors.filter((err) => {
|
|
294
|
+
// Keep everything except the two red-herring classes AT /navigation.
|
|
295
|
+
// Child errors like /navigation/tabs/0 have a deeper instancePath -> kept.
|
|
296
|
+
if (err.instancePath !== '/navigation') return true;
|
|
297
|
+
// Drop sibling-branch "missing required <otherKey>" noise (e.g. products).
|
|
298
|
+
if (err.keyword === 'required') {
|
|
299
|
+
const missing =
|
|
300
|
+
(err.params as { missingProperty?: string }).missingProperty;
|
|
301
|
+
return !(missing && missing !== usedKey &&
|
|
302
|
+
(NAV_BRANCH_KEYS as readonly string[]).includes(missing));
|
|
303
|
+
}
|
|
304
|
+
// Drop "unknown property <usedKey>" flagged by non-matching sibling
|
|
305
|
+
// branches (e.g. additionalProperty "tabs").
|
|
306
|
+
if (err.keyword === 'additionalProperties') {
|
|
307
|
+
const extra =
|
|
308
|
+
(err.params as { additionalProperty?: string }).additionalProperty;
|
|
309
|
+
return extra !== usedKey;
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
260
315
|
/**
|
|
261
316
|
* Format validation errors into user-friendly messages
|
|
262
317
|
*/
|
|
263
|
-
export function formatValidationErrors(errors: ErrorObject[] | null | undefined): string {
|
|
318
|
+
export function formatValidationErrors(errors: ErrorObject[] | null | undefined, navigation?: Record<string, unknown>): string {
|
|
264
319
|
if (!errors || errors.length === 0) {
|
|
265
320
|
return 'Unknown validation error';
|
|
266
321
|
}
|
|
@@ -270,8 +325,9 @@ export function formatValidationErrors(errors: ErrorObject[] | null | undefined)
|
|
|
270
325
|
// Safe only because validateConfig pre-checks `config.theme` before Ajv,
|
|
271
326
|
// so a fully-noise error list is unreachable in practice. A new caller
|
|
272
327
|
// that skips the theme pre-check would break this invariant.
|
|
273
|
-
const
|
|
274
|
-
const
|
|
328
|
+
const navFiltered = filterNavigationAnyOfNoise(errors, navigation);
|
|
329
|
+
const filteredErrors = filterAnyOfNoise(navFiltered);
|
|
330
|
+
const effectiveErrors = filteredErrors.length > 0 ? filteredErrors : navFiltered;
|
|
275
331
|
|
|
276
332
|
const messages = effectiveErrors.slice(0, 3).map(err => {
|
|
277
333
|
const path = err.instancePath || 'root';
|
|
@@ -548,7 +604,10 @@ export async function validateConfig(
|
|
|
548
604
|
const valid = validate(config);
|
|
549
605
|
|
|
550
606
|
if (!valid) {
|
|
551
|
-
const errorMessage = formatValidationErrors(
|
|
607
|
+
const errorMessage = formatValidationErrors(
|
|
608
|
+
validate.errors,
|
|
609
|
+
config.navigation as Record<string, unknown> | undefined,
|
|
610
|
+
);
|
|
552
611
|
return {
|
|
553
612
|
valid: false,
|
|
554
613
|
error: `Invalid docs.json: ${errorMessage}`,
|