jamdesk 1.1.49 → 1.1.50
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/deps-sync.test.js +9 -5
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/package.json +1 -1
- package/vendored/app/(unlock)/layout.tsx +43 -0
- package/vendored/app/[[...slug]]/layout.tsx +119 -0
- package/vendored/app/[[...slug]]/page.tsx +56 -751
- package/vendored/app/layout.tsx +11 -606
- package/vendored/app/not-found.tsx +27 -43
- package/vendored/components/AIActionsMenu.tsx +1 -1
- package/vendored/components/FontAwesomeLoader.tsx +7 -11
- package/vendored/components/HtmlLangSync.tsx +1 -1
- package/vendored/components/errors/NotFoundContent.tsx +5 -1
- package/vendored/components/layout/LayoutWrapper.tsx +5 -21
- package/vendored/components/layout/PageColumns.tsx +24 -1
- package/vendored/components/navigation/Header.tsx +1 -1
- package/vendored/components/navigation/LanguageSelector.tsx +2 -2
- package/vendored/components/navigation/Sidebar.tsx +38 -9
- package/vendored/components/navigation/TabsNav.tsx +1 -1
- package/vendored/components/search/SearchModal.tsx +1 -1
- package/vendored/lib/layout-helpers.tsx +464 -0
- package/vendored/lib/middleware-helpers.ts +0 -78
- package/vendored/lib/page-isr-helpers.ts +16 -0
- package/vendored/lib/project-resolver.ts +28 -1
- package/vendored/lib/r2-content.ts +8 -0
- package/vendored/lib/render-doc-page.tsx +595 -0
- package/vendored/lib/seo.ts +21 -0
- package/vendored/workspace-package-lock.json +7 -7
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
// Header-free render path for documentation pages. Called from
|
|
2
|
+
// `app/[[...slug]]/page.tsx` only; this module and its imports do not
|
|
3
|
+
// touch `next/headers` so the page can pass header data in (or null) and
|
|
4
|
+
// drive base-URL resolution explicitly.
|
|
5
|
+
//
|
|
6
|
+
// Caller contract (both branches live in the catch-all page):
|
|
7
|
+
// - Header-resolved branch (middleware-set `x-project-slug`): passes
|
|
8
|
+
// `requestHeaders` so `resolveBaseUrl` derives a forwarded-host
|
|
9
|
+
// canonical.
|
|
10
|
+
// - `projectFromParams` branch (direct `/[project]/...` URLs and
|
|
11
|
+
// non-ISR local dev): passes `requestHeaders=null` and gets a
|
|
12
|
+
// subdomain-canonical via `getBaseUrlFromConfig`.
|
|
13
|
+
import { notFound } from 'next/navigation';
|
|
14
|
+
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
15
|
+
import type { Metadata } from 'next';
|
|
16
|
+
import type { ReactElement } from 'react';
|
|
17
|
+
import { MDXComponents } from '@/components/mdx/MDXComponents';
|
|
18
|
+
import { Breadcrumb } from '@/components/navigation/Breadcrumb';
|
|
19
|
+
import { TableOfContents } from '@/components/navigation/TableOfContents';
|
|
20
|
+
import { PageColumns } from '@/components/layout/PageColumns';
|
|
21
|
+
import { PageNavigation } from '@/components/navigation/PageNavigation';
|
|
22
|
+
import { SocialFooter } from '@/components/navigation/SocialFooter';
|
|
23
|
+
import { ApiPageWrapper } from '@/components/mdx/ApiPage';
|
|
24
|
+
import { OpenApiEndpoint } from '@/components/mdx/OpenApiEndpoint';
|
|
25
|
+
import { OpenApiError } from '@/components/openapi/OpenApiError';
|
|
26
|
+
import { getHighlighter } from '@/lib/shiki-highlighter';
|
|
27
|
+
import { createShikiRehypePlugin } from '@/lib/shiki-config';
|
|
28
|
+
import rehypeSlug from 'rehype-slug';
|
|
29
|
+
import remarkGfm from 'remark-gfm';
|
|
30
|
+
import { rehypeCodeMeta, rehypeRestoreDataTitle } from '@/lib/rehype-code-meta';
|
|
31
|
+
import { rehypeClassToClassName } from '@/lib/rehype-class-to-classname';
|
|
32
|
+
import { rehypeNoZoomToData } from '@/lib/rehype-nozoom-to-data';
|
|
33
|
+
import { preprocessMdx, containsPanel, containsView, buildSnippetAliasMap } from '@/lib/preprocess-mdx';
|
|
34
|
+
import { loadSnippetsForIsr } from '@/lib/snippet-loader-isr';
|
|
35
|
+
import { PanelWrapper } from '@/components/mdx/PanelWrapper';
|
|
36
|
+
import { ViewWrapper } from '@/components/mdx/View';
|
|
37
|
+
import { getLatexRemarkPlugins, getLatexRehypePlugins } from '@/lib/latex-config';
|
|
38
|
+
import { getTypographyRemarkPlugins } from '@/lib/typography-config';
|
|
39
|
+
import { remarkVisibility } from '@/lib/remark-visibility';
|
|
40
|
+
import { recmaCompoundComponents } from '@/lib/recma-compound-components';
|
|
41
|
+
import { extractInlineComponents } from '@/lib/process-mdx-with-exports';
|
|
42
|
+
import { extractHeadings } from '@/lib/heading-extractor';
|
|
43
|
+
import { StepSlugProvider, type StepSlugEntry } from '@/components/mdx/StepSlugContext';
|
|
44
|
+
import { buildEndpointFromMdx } from '@/lib/build-endpoint-from-mdx';
|
|
45
|
+
import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
46
|
+
import { getContentDir } from '@/lib/docs';
|
|
47
|
+
import type { DocsConfig } from '@/lib/docs-types';
|
|
48
|
+
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
49
|
+
import { buildJsonLd } from '@/lib/json-ld';
|
|
50
|
+
import {
|
|
51
|
+
getContentLoader,
|
|
52
|
+
isIsrMode,
|
|
53
|
+
normalizeSlugForContent,
|
|
54
|
+
parseFrontmatter,
|
|
55
|
+
projectExists,
|
|
56
|
+
} from '@/lib/content-loader';
|
|
57
|
+
import { getBaseUrl, getBaseUrlFromConfig, pathToSlug } from '@/lib/page-isr-helpers';
|
|
58
|
+
import {
|
|
59
|
+
parseOpenApiFrontmatter,
|
|
60
|
+
getCachedSpec,
|
|
61
|
+
parseEndpoint,
|
|
62
|
+
generateCodeExamples,
|
|
63
|
+
formatOpenApiWarning,
|
|
64
|
+
deriveAuthFromSecurity,
|
|
65
|
+
type OpenApiEndpointData,
|
|
66
|
+
type CodeExample,
|
|
67
|
+
type AuthMethod,
|
|
68
|
+
} from '@/lib/openapi';
|
|
69
|
+
import { classifyOpenApiLoadError } from '@/lib/openapi/classify-load-error';
|
|
70
|
+
import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
|
|
71
|
+
import { findFirstNavPage } from '@/lib/find-first-nav-page';
|
|
72
|
+
import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
73
|
+
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
74
|
+
import { ImagePriorityProvider } from '@/components/mdx/ImagePriorityProvider';
|
|
75
|
+
import { AIActionsMenu } from '@/components/AIActionsMenu';
|
|
76
|
+
import { getContextualOptions } from '@/lib/contextual-defaults';
|
|
77
|
+
import { SnippetComponents } from '@/components/snippets/ProjectSnippets';
|
|
78
|
+
|
|
79
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
80
|
+
|
|
81
|
+
const DEFAULT_SITE_URL = 'https://docs.example.com';
|
|
82
|
+
|
|
83
|
+
export interface RenderInput {
|
|
84
|
+
slug: string[];
|
|
85
|
+
projectSlug: string | null;
|
|
86
|
+
hostAtDocs: boolean;
|
|
87
|
+
requestHeaders: Headers | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface FrontmatterData {
|
|
91
|
+
title?: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
api?: string;
|
|
94
|
+
openapi?: string;
|
|
95
|
+
playground?: string;
|
|
96
|
+
mode?: string;
|
|
97
|
+
hideFooter?: boolean;
|
|
98
|
+
rss?: boolean;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseMdxApiField(apiField: string): { method: HttpMethod; path: string } | null {
|
|
103
|
+
if (!apiField || typeof apiField !== 'string') return null;
|
|
104
|
+
const trimmed = apiField.trim();
|
|
105
|
+
const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE'];
|
|
106
|
+
for (const method of methods) {
|
|
107
|
+
if (trimmed.toUpperCase().startsWith(method)) {
|
|
108
|
+
const path = trimmed.slice(method.length).trim();
|
|
109
|
+
return { method, path };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveBaseUrl(
|
|
116
|
+
requestHeaders: Headers | null,
|
|
117
|
+
projectSlug: string | null,
|
|
118
|
+
hostAtDocs: boolean,
|
|
119
|
+
): string {
|
|
120
|
+
if (!projectSlug) return process.env.SITE_URL || DEFAULT_SITE_URL;
|
|
121
|
+
if (requestHeaders) return getBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
122
|
+
return getBaseUrlFromConfig(projectSlug, hostAtDocs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveAuth(
|
|
126
|
+
endpoint: OpenApiEndpointData | null | undefined,
|
|
127
|
+
config: DocsConfig,
|
|
128
|
+
): { method?: AuthMethod; headerName?: string } {
|
|
129
|
+
const override = config.api?.mdx?.auth;
|
|
130
|
+
if (override?.method) {
|
|
131
|
+
return { method: override.method, headerName: override.name };
|
|
132
|
+
}
|
|
133
|
+
return endpoint ? deriveAuthFromSecurity(endpoint.security) : {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findFirstPage(config: DocsConfig, lang?: string): string {
|
|
137
|
+
const nav = config.navigation;
|
|
138
|
+
const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
|
|
139
|
+
const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
|
|
140
|
+
return result ? result.replace(/^\//, '') : 'introduction';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function needsSlugRewrite(slug: string[]): boolean {
|
|
144
|
+
return slug.length === 0 || (slug.length === 1 && isValidLanguageCode(slug[0]));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
|
|
148
|
+
if (normalizedSlug.length === 0) return pathToSlug(findFirstPage(config));
|
|
149
|
+
if (normalizedSlug.length === 1 && isValidLanguageCode(normalizedSlug[0])) {
|
|
150
|
+
return pathToSlug(findFirstPage(config, normalizedSlug[0]));
|
|
151
|
+
}
|
|
152
|
+
return normalizedSlug;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
|
|
156
|
+
const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
|
|
157
|
+
|
|
158
|
+
if (isIsrMode()) {
|
|
159
|
+
if (!projectSlug) return { title: 'Not Found' };
|
|
160
|
+
const exists = await projectExists(projectSlug);
|
|
161
|
+
if (!exists) return { title: 'Not Found' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const loader = getContentLoader(projectSlug ?? undefined);
|
|
165
|
+
const configP = loader.getConfig();
|
|
166
|
+
|
|
167
|
+
const normalizedSlug = normalizeSlugForContent(slugInput || [], hostAtDocs);
|
|
168
|
+
const slug = needsSlugRewrite(normalizedSlug)
|
|
169
|
+
? resolveSlug(normalizedSlug, await configP)
|
|
170
|
+
: normalizedSlug;
|
|
171
|
+
const isRoot = normalizedSlug.length === 0;
|
|
172
|
+
const pagePath = slug.join('/');
|
|
173
|
+
|
|
174
|
+
const [fileContents, config] = await Promise.all([
|
|
175
|
+
loader.getContent(pagePath).catch(() => null),
|
|
176
|
+
configP,
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (!fileContents) {
|
|
180
|
+
return { title: 'Not Found' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parsed = parseFrontmatter(fileContents);
|
|
184
|
+
const data = parsed.data as FrontmatterData;
|
|
185
|
+
|
|
186
|
+
if (!data.description) {
|
|
187
|
+
data.description = generateAutoDescription(parsed.content);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
191
|
+
const languages = config.navigation?.languages;
|
|
192
|
+
|
|
193
|
+
const seoMetadata = buildSeoMetadata(config, data, pagePath, baseUrl, languages);
|
|
194
|
+
|
|
195
|
+
const titleValue = data.title
|
|
196
|
+
? (data.title === config.name ? { absolute: data.title } : data.title)
|
|
197
|
+
: { absolute: buildSiteTitle(config.name) };
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
title: titleValue,
|
|
201
|
+
description: data.description || '',
|
|
202
|
+
...seoMetadata,
|
|
203
|
+
...(isRoot && { robots: { index: false, follow: true } }),
|
|
204
|
+
...(data.rss ? {
|
|
205
|
+
alternates: {
|
|
206
|
+
...seoMetadata.alternates,
|
|
207
|
+
types: {
|
|
208
|
+
'application/rss+xml': hostAtDocs ? '/docs/feed.xml' : '/feed.xml',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
} : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
216
|
+
const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
|
|
217
|
+
|
|
218
|
+
if (isIsrMode()) {
|
|
219
|
+
if (!projectSlug) notFound();
|
|
220
|
+
const exists = await projectExists(projectSlug);
|
|
221
|
+
if (!exists) notFound();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const loader = getContentLoader(projectSlug ?? undefined);
|
|
225
|
+
const configP = loader.getConfig();
|
|
226
|
+
|
|
227
|
+
const normalizedSlug = normalizeSlugForContent(slugInput || [], hostAtDocs);
|
|
228
|
+
const slug = needsSlugRewrite(normalizedSlug)
|
|
229
|
+
? resolveSlug(normalizedSlug, await configP)
|
|
230
|
+
: normalizedSlug;
|
|
231
|
+
const pagePath = slug.join('/');
|
|
232
|
+
const currentLang = extractLanguageFromPath(`/${pagePath}`);
|
|
233
|
+
const [fileContents, config] = await Promise.all([
|
|
234
|
+
loader.getContent(pagePath).catch(() => null),
|
|
235
|
+
configP,
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
if (!fileContents) {
|
|
239
|
+
notFound();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const parsed = parseFrontmatter(fileContents);
|
|
243
|
+
const data = parsed.data as FrontmatterData;
|
|
244
|
+
const rawContent = parsed.content;
|
|
245
|
+
|
|
246
|
+
const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
247
|
+
const jsonLd = buildJsonLd({
|
|
248
|
+
config,
|
|
249
|
+
pagePath,
|
|
250
|
+
pageTitle: data.title || pagePath,
|
|
251
|
+
baseUrl,
|
|
252
|
+
});
|
|
253
|
+
const jsonLdScript = renderJsonLdScript(jsonLd);
|
|
254
|
+
|
|
255
|
+
const highlighter = await getHighlighter();
|
|
256
|
+
|
|
257
|
+
let snippetAliases: Record<string, React.ComponentType<unknown>> = {};
|
|
258
|
+
if (isIsrMode() && projectSlug) {
|
|
259
|
+
snippetAliases = await loadSnippetsForIsr(projectSlug, rawContent, MDXComponents);
|
|
260
|
+
} else {
|
|
261
|
+
snippetAliases = buildSnippetAliasMap(rawContent, SnippetComponents);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const content = preprocessMdx(rawContent, { assetVersion: config.assetVersion });
|
|
265
|
+
|
|
266
|
+
const stepEntries: StepSlugEntry[] = extractHeadings(content)
|
|
267
|
+
.filter(h => typeof h.stepNumber === 'number')
|
|
268
|
+
.map(h => ({ title: h.text, slug: h.id }));
|
|
269
|
+
|
|
270
|
+
const { inlineComponents, paramFields } = await extractInlineComponents(content, MDXComponents);
|
|
271
|
+
|
|
272
|
+
const overriddenComponents = Object.keys(inlineComponents).filter(
|
|
273
|
+
(name) => name in MDXComponents,
|
|
274
|
+
);
|
|
275
|
+
if (overriddenComponents.length > 0) {
|
|
276
|
+
console.warn(
|
|
277
|
+
`[MDX] Inline component(s) override built-in: ${overriddenComponents.join(', ')} in ${slug.join('/')}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const AllComponentsWithInline = {
|
|
282
|
+
...MDXComponents,
|
|
283
|
+
...SnippetComponents,
|
|
284
|
+
...snippetAliases,
|
|
285
|
+
...inlineComponents,
|
|
286
|
+
...(hostAtDocs ? {
|
|
287
|
+
a: ({ ariaLabel, href, ...props }: any) => {
|
|
288
|
+
const needsPrefix = href?.startsWith('/') && !href.startsWith('/docs/') && href !== '/docs';
|
|
289
|
+
return (
|
|
290
|
+
<a
|
|
291
|
+
className="text-theme-accent hover:text-theme-accent-hover transition-colors"
|
|
292
|
+
aria-label={ariaLabel}
|
|
293
|
+
href={needsPrefix ? `/docs${href}` : href}
|
|
294
|
+
{...props}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
},
|
|
298
|
+
} : {}),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const isApiPage = !!data.api || !!data.openapi;
|
|
302
|
+
const isWideMode = data.mode === 'wide';
|
|
303
|
+
const hasPanel = containsPanel(content);
|
|
304
|
+
const hasView = containsView(content);
|
|
305
|
+
|
|
306
|
+
let openApiEndpointData: OpenApiEndpointData | null = null;
|
|
307
|
+
let openApiCodeExamples: CodeExample[] | null = null;
|
|
308
|
+
let openApiError: string | null = null;
|
|
309
|
+
|
|
310
|
+
let lastFailure: { err: unknown; specPath: string } | null = null;
|
|
311
|
+
if (data.openapi && typeof data.openapi === 'string') {
|
|
312
|
+
try {
|
|
313
|
+
const openApiConfig = config.api?.openapi;
|
|
314
|
+
const allSpecPaths: string[] = typeof openApiConfig === 'string'
|
|
315
|
+
? [openApiConfig]
|
|
316
|
+
: Array.isArray(openApiConfig)
|
|
317
|
+
? openApiConfig
|
|
318
|
+
: [];
|
|
319
|
+
|
|
320
|
+
const parsed = parseOpenApiFrontmatter(
|
|
321
|
+
data.openapi,
|
|
322
|
+
allSpecPaths.length > 0 ? allSpecPaths : undefined,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const baseSpecs = parsed.isShortFormat && allSpecPaths.length > 1
|
|
326
|
+
? allSpecPaths
|
|
327
|
+
: [parsed.specPath];
|
|
328
|
+
const specsToTry = baseSpecs.flatMap(p => candidateSpecPaths(p, currentLang));
|
|
329
|
+
|
|
330
|
+
const useIsr = isIsrMode() && !!projectSlug;
|
|
331
|
+
const resolveSpec = useIsr
|
|
332
|
+
? (await import('@/lib/openapi-isr')).resolveOpenApiSpec
|
|
333
|
+
: null;
|
|
334
|
+
const contentDir = useIsr ? null : getContentDir();
|
|
335
|
+
|
|
336
|
+
for (let i = 0; i < specsToTry.length; i++) {
|
|
337
|
+
const specPath = specsToTry[i];
|
|
338
|
+
try {
|
|
339
|
+
if (resolveSpec && projectSlug) {
|
|
340
|
+
const spec = await resolveSpec(projectSlug, specPath);
|
|
341
|
+
openApiEndpointData = parseEndpoint(
|
|
342
|
+
spec as Parameters<typeof parseEndpoint>[0],
|
|
343
|
+
parsed.method,
|
|
344
|
+
parsed.path,
|
|
345
|
+
specPath,
|
|
346
|
+
);
|
|
347
|
+
} else {
|
|
348
|
+
const { api } = await getCachedSpec(specPath, contentDir!);
|
|
349
|
+
openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, specPath);
|
|
350
|
+
}
|
|
351
|
+
lastFailure = null;
|
|
352
|
+
break;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
lastFailure = { err, specPath };
|
|
355
|
+
const isLast = i === specsToTry.length - 1;
|
|
356
|
+
if (!isLast) {
|
|
357
|
+
console.warn(
|
|
358
|
+
`[openapi] spec candidate "${specPath}" failed; trying next: ${(err as Error).message}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (lastFailure) throw lastFailure.err;
|
|
365
|
+
|
|
366
|
+
if (openApiEndpointData) {
|
|
367
|
+
const { method: authMethod, headerName: authHeaderName } = resolveAuth(openApiEndpointData, config);
|
|
368
|
+
const languages = config.api?.examples?.languages;
|
|
369
|
+
openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, authHeaderName, languages });
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const typed = classifyOpenApiLoadError(err, lastFailure?.specPath ?? null);
|
|
373
|
+
console.warn(formatOpenApiWarning(typed));
|
|
374
|
+
openApiError = typed.suggestion
|
|
375
|
+
? `${typed.message} — ${typed.suggestion}`
|
|
376
|
+
: typed.message;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let mdxApiMethod: HttpMethod | null = null;
|
|
381
|
+
let mdxApiPath: string | null = null;
|
|
382
|
+
if (data.api && typeof data.api === 'string' && !data.openapi) {
|
|
383
|
+
const parsed = parseMdxApiField(data.api);
|
|
384
|
+
if (parsed) {
|
|
385
|
+
mdxApiMethod = parsed.method;
|
|
386
|
+
mdxApiPath = parsed.path;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const rssIcon = data.rss ? (
|
|
391
|
+
<a
|
|
392
|
+
href={hostAtDocs ? '/docs/feed.xml' : '/feed.xml'}
|
|
393
|
+
target="_blank"
|
|
394
|
+
rel="noopener noreferrer"
|
|
395
|
+
aria-label="Subscribe to RSS feed"
|
|
396
|
+
title="RSS feed"
|
|
397
|
+
className="flex-shrink-0 text-[var(--color-text-muted)] hover:text-[var(--color-primary)] transition-colors cursor-pointer"
|
|
398
|
+
>
|
|
399
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
400
|
+
<circle cx="6.18" cy="17.82" r="2.18" />
|
|
401
|
+
<path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z" />
|
|
402
|
+
</svg>
|
|
403
|
+
</a>
|
|
404
|
+
) : null;
|
|
405
|
+
|
|
406
|
+
const contextualOptions = getContextualOptions(config);
|
|
407
|
+
const hasAiActions = contextualOptions.length > 0;
|
|
408
|
+
|
|
409
|
+
const proseClasses = 'prose max-w-none';
|
|
410
|
+
|
|
411
|
+
const hasApiEndpoint = openApiEndpointData || (mdxApiMethod && mdxApiPath);
|
|
412
|
+
const playgroundDisplay = hasApiEndpoint
|
|
413
|
+
? ((data.playground as 'interactive' | 'simple' | 'none' | undefined)
|
|
414
|
+
|| config.api?.playground?.display || 'interactive') as 'interactive' | 'simple' | 'none'
|
|
415
|
+
: 'none';
|
|
416
|
+
const mdxServerConfig = config.api?.mdx?.server;
|
|
417
|
+
const fallbackServerUrl = Array.isArray(mdxServerConfig) ? mdxServerConfig[0] : mdxServerConfig;
|
|
418
|
+
const proxyEnabled = hostAtDocs
|
|
419
|
+
|| config.api?.playground?.proxy
|
|
420
|
+
|| (config.api?.playground?.proxy == null && playgroundDisplay === 'interactive');
|
|
421
|
+
|
|
422
|
+
let mdxEndpointData: OpenApiEndpointData | undefined;
|
|
423
|
+
if (!openApiEndpointData && mdxApiMethod && mdxApiPath && playgroundDisplay !== 'none') {
|
|
424
|
+
mdxEndpointData = buildEndpointFromMdx(mdxApiMethod, mdxApiPath, paramFields, fallbackServerUrl);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const resolvedMdxAuth = resolveAuth(mdxEndpointData, config);
|
|
428
|
+
const resolvedOpenApiAuth = resolveAuth(openApiEndpointData, config);
|
|
429
|
+
|
|
430
|
+
if (isApiPage) {
|
|
431
|
+
return (
|
|
432
|
+
<>{jsonLdScript}<ApiPageWrapper>
|
|
433
|
+
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10 flex-1 min-w-0">
|
|
434
|
+
<Breadcrumb slug={slug} config={config} />
|
|
435
|
+
|
|
436
|
+
{data.title && (
|
|
437
|
+
<header className="mb-4 sm:mb-6">
|
|
438
|
+
<div className="flex items-center gap-3">
|
|
439
|
+
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-theme-text-primary tracking-tight">
|
|
440
|
+
{data.title}
|
|
441
|
+
</h1>
|
|
442
|
+
{rssIcon}
|
|
443
|
+
{hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
444
|
+
</div>
|
|
445
|
+
{data.description && (
|
|
446
|
+
<p className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3">
|
|
447
|
+
{data.description}
|
|
448
|
+
</p>
|
|
449
|
+
)}
|
|
450
|
+
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
451
|
+
</header>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
<div className={proseClasses}>
|
|
455
|
+
{mdxApiMethod && mdxApiPath && (
|
|
456
|
+
mdxEndpointData && playgroundDisplay !== 'none' ? (
|
|
457
|
+
<OpenApiEndpoint
|
|
458
|
+
endpoint={mdxEndpointData}
|
|
459
|
+
playgroundOnly
|
|
460
|
+
playgroundDisplay={playgroundDisplay}
|
|
461
|
+
authMethod={resolvedMdxAuth.method}
|
|
462
|
+
authHeaderName={resolvedMdxAuth.headerName}
|
|
463
|
+
serverUrl={fallbackServerUrl}
|
|
464
|
+
proxyEnabled={proxyEnabled}
|
|
465
|
+
languages={config.api?.examples?.languages}
|
|
466
|
+
/>
|
|
467
|
+
) : (
|
|
468
|
+
<ApiEndpoint
|
|
469
|
+
method={mdxApiMethod}
|
|
470
|
+
path={mdxApiPath}
|
|
471
|
+
baseUrl={fallbackServerUrl}
|
|
472
|
+
/>
|
|
473
|
+
)
|
|
474
|
+
)}
|
|
475
|
+
|
|
476
|
+
{openApiEndpointData && (
|
|
477
|
+
<OpenApiEndpoint
|
|
478
|
+
endpoint={openApiEndpointData}
|
|
479
|
+
codeExamples={openApiCodeExamples || undefined}
|
|
480
|
+
playgroundDisplay={playgroundDisplay}
|
|
481
|
+
authMethod={resolvedOpenApiAuth.method}
|
|
482
|
+
authHeaderName={resolvedOpenApiAuth.headerName}
|
|
483
|
+
serverUrl={fallbackServerUrl}
|
|
484
|
+
proxyEnabled={proxyEnabled}
|
|
485
|
+
languages={config.api?.examples?.languages}
|
|
486
|
+
/>
|
|
487
|
+
)}
|
|
488
|
+
|
|
489
|
+
{!openApiEndpointData && openApiError && (
|
|
490
|
+
<OpenApiError message={openApiError} slug={slug.join('/')} />
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
<ImagePriorityProvider>
|
|
494
|
+
<StepSlugProvider entries={stepEntries}>
|
|
495
|
+
<MDXRemote
|
|
496
|
+
source={openApiEndpointData
|
|
497
|
+
? content.replace(/<ResponseExample>[\s\S]*?<\/ResponseExample>/g, '')
|
|
498
|
+
: content}
|
|
499
|
+
components={AllComponentsWithInline}
|
|
500
|
+
options={{
|
|
501
|
+
...mdxSecurityOptions,
|
|
502
|
+
mdxOptions: {
|
|
503
|
+
remarkPlugins: [remarkGfm, [remarkVisibility, { audience: 'humans' }], ...getTypographyRemarkPlugins(config), ...getLatexRemarkPlugins(config)],
|
|
504
|
+
rehypePlugins: [rehypeNoZoomToData, rehypeClassToClassName, rehypeCodeMeta, createShikiRehypePlugin(highlighter, config), rehypeRestoreDataTitle, ...getLatexRehypePlugins(config), rehypeSlug],
|
|
505
|
+
recmaPlugins: [recmaCompoundComponents],
|
|
506
|
+
},
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
</StepSlugProvider>
|
|
510
|
+
</ImagePriorityProvider>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<PageNavigation currentSlug={slug.join('/')} config={config} />
|
|
514
|
+
<SocialFooter config={config} hidden={data.hideFooter} projectSlug={projectSlug ?? undefined} />
|
|
515
|
+
</article>
|
|
516
|
+
</ApiPageWrapper></>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const mdxContent = (
|
|
521
|
+
<StepSlugProvider entries={stepEntries}>
|
|
522
|
+
<MDXRemote
|
|
523
|
+
source={content}
|
|
524
|
+
components={AllComponentsWithInline}
|
|
525
|
+
options={{
|
|
526
|
+
...mdxSecurityOptions,
|
|
527
|
+
mdxOptions: {
|
|
528
|
+
remarkPlugins: [remarkGfm, [remarkVisibility, { audience: 'humans' }], ...getTypographyRemarkPlugins(config), ...getLatexRemarkPlugins(config)],
|
|
529
|
+
rehypePlugins: [rehypeNoZoomToData, rehypeClassToClassName, rehypeCodeMeta, createShikiRehypePlugin(highlighter, config), rehypeRestoreDataTitle, ...getLatexRehypePlugins(config), rehypeSlug],
|
|
530
|
+
recmaPlugins: [recmaCompoundComponents],
|
|
531
|
+
},
|
|
532
|
+
}}
|
|
533
|
+
/>
|
|
534
|
+
</StepSlugProvider>
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const articleContent = (
|
|
538
|
+
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
|
|
539
|
+
<Breadcrumb slug={slug} config={config} />
|
|
540
|
+
|
|
541
|
+
{data.title && (
|
|
542
|
+
<header className="mb-6 sm:mb-10">
|
|
543
|
+
<div className="flex items-center gap-3">
|
|
544
|
+
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-theme-text-primary tracking-tight">
|
|
545
|
+
{data.title}
|
|
546
|
+
</h1>
|
|
547
|
+
{rssIcon}
|
|
548
|
+
{hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
549
|
+
</div>
|
|
550
|
+
{data.description && (
|
|
551
|
+
<p className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3">
|
|
552
|
+
{data.description}
|
|
553
|
+
</p>
|
|
554
|
+
)}
|
|
555
|
+
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
556
|
+
</header>
|
|
557
|
+
)}
|
|
558
|
+
|
|
559
|
+
<div className={proseClasses}>
|
|
560
|
+
<ImagePriorityProvider>
|
|
561
|
+
{hasView ? <ViewWrapper>{mdxContent}</ViewWrapper> : mdxContent}
|
|
562
|
+
</ImagePriorityProvider>
|
|
563
|
+
|
|
564
|
+
<PageNavigation currentSlug={slug.join('/')} config={config} isWideMode={isWideMode || hasPanel} />
|
|
565
|
+
</div>
|
|
566
|
+
<SocialFooter config={config} hidden={data.hideFooter} projectSlug={projectSlug ?? undefined} />
|
|
567
|
+
</article>
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
if (hasPanel) {
|
|
571
|
+
return <>{jsonLdScript}<PanelWrapper>{articleContent}</PanelWrapper></>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<>
|
|
576
|
+
{jsonLdScript}
|
|
577
|
+
<PageColumns toc={<TableOfContents content={content} />} isWideMode={isWideMode}>
|
|
578
|
+
{articleContent}
|
|
579
|
+
</PageColumns>
|
|
580
|
+
</>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Inline JSON-LD for SEO. The `<` escape is the documented Next.js
|
|
585
|
+
// XSS-safe pattern (see app/layout.tsx and marketing/blog/[slug]/page.tsx).
|
|
586
|
+
// Trusted source: built from project config + page metadata, never user MDX.
|
|
587
|
+
function renderJsonLdScript(jsonLd: unknown): ReactElement {
|
|
588
|
+
const safe = JSON.stringify(jsonLd).replace(/</g, '\\u003c');
|
|
589
|
+
return (
|
|
590
|
+
<script
|
|
591
|
+
type="application/ld+json"
|
|
592
|
+
dangerouslySetInnerHTML={{ __html: safe }}
|
|
593
|
+
/>
|
|
594
|
+
);
|
|
595
|
+
}
|
package/vendored/lib/seo.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Metadata } from 'next';
|
|
11
11
|
import type { DocsConfig, Logo, LogoConfig, Favicon, FaviconConfig, LanguageConfig, LanguageCode } from './docs-types';
|
|
12
|
+
import { transformConfigImagePath } from './docs-types';
|
|
12
13
|
import { transformLanguagePath, extractLanguageFromPath, isValidLanguageCode } from './language-utils';
|
|
13
14
|
|
|
14
15
|
const HAS_DOCS_SUFFIX = /\b(?:Documentation|Docs)\s*$/i;
|
|
@@ -23,6 +24,26 @@ export function buildSiteTitle(configName: string): string {
|
|
|
23
24
|
: `${configName} Documentation`;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a Favicon config (string | { light, dark }) to a single asset
|
|
29
|
+
* path through the /_jd/ pipeline. Returns undefined when no favicon is
|
|
30
|
+
* configured — no default Jamdesk favicon is injected.
|
|
31
|
+
*/
|
|
32
|
+
export function getFaviconPath(
|
|
33
|
+
favicon: Favicon | undefined,
|
|
34
|
+
assetVersion?: string,
|
|
35
|
+
): string | undefined {
|
|
36
|
+
if (!favicon) return undefined;
|
|
37
|
+
const raw = typeof favicon === 'string' ? favicon : favicon.light;
|
|
38
|
+
return transformConfigImagePath(raw, assetVersion) || undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Metadata used when the project can't be resolved or its config is missing. */
|
|
42
|
+
export const FALLBACK_METADATA: Metadata = {
|
|
43
|
+
title: { template: '%s — Documentation', default: 'Documentation' },
|
|
44
|
+
description: 'Documentation',
|
|
45
|
+
};
|
|
46
|
+
|
|
26
47
|
/** Resolve a Logo or Favicon config to a path string, preferring light variant. */
|
|
27
48
|
function resolveImagePath(image?: Logo | Favicon): string {
|
|
28
49
|
if (!image) return '';
|
|
@@ -2301,12 +2301,12 @@
|
|
|
2301
2301
|
}
|
|
2302
2302
|
},
|
|
2303
2303
|
"node_modules/chevrotain-allstar": {
|
|
2304
|
-
"version": "0.4.
|
|
2305
|
-
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.
|
|
2306
|
-
"integrity": "sha512-
|
|
2304
|
+
"version": "0.4.3",
|
|
2305
|
+
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz",
|
|
2306
|
+
"integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==",
|
|
2307
2307
|
"license": "MIT",
|
|
2308
2308
|
"dependencies": {
|
|
2309
|
-
"lodash-es": "^4.
|
|
2309
|
+
"lodash-es": "^4.18.1"
|
|
2310
2310
|
},
|
|
2311
2311
|
"peerDependencies": {
|
|
2312
2312
|
"chevrotain": "^12.0.0"
|
|
@@ -2980,9 +2980,9 @@
|
|
|
2980
2980
|
"license": "MIT"
|
|
2981
2981
|
},
|
|
2982
2982
|
"node_modules/electron-to-chromium": {
|
|
2983
|
-
"version": "1.5.
|
|
2984
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2985
|
-
"integrity": "sha512-
|
|
2983
|
+
"version": "1.5.345",
|
|
2984
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.345.tgz",
|
|
2985
|
+
"integrity": "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==",
|
|
2986
2986
|
"license": "ISC"
|
|
2987
2987
|
},
|
|
2988
2988
|
"node_modules/enhanced-resolve": {
|