jamdesk 1.1.49 → 1.1.51
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/components/theme/ThemeProvider.tsx +0 -1
- package/vendored/lib/layout-helpers.tsx +470 -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
|
@@ -1,195 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
// Catch-all route for docs pages. Reads `headers()` for per-project
|
|
2
|
+
// resolution (`x-project-slug`/`x-host-at-docs` set by middleware), which
|
|
3
|
+
// forces force-dynamic. The `params.project` branch handles direct
|
|
4
|
+
// `/[project]/...` URLs without middleware involvement (used by tests and
|
|
5
|
+
// non-ISR local dev). The render body lives in lib/render-doc-page.tsx.
|
|
2
6
|
import { headers } from 'next/headers';
|
|
3
|
-
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Route Segment Configuration
|
|
7
|
-
*
|
|
8
|
-
* force-dynamic: Always render on each request. Required because:
|
|
9
|
-
* 1. In ISR mode, we read project slug from headers (set by middleware)
|
|
10
|
-
* 2. headers() is a dynamic API that opts out of static generation
|
|
11
|
-
*
|
|
12
|
-
* In static mode (local dev, customer builds), this is ignored because
|
|
13
|
-
* those builds don't deploy to Vercel - they use the static export.
|
|
14
|
-
*
|
|
15
|
-
* Caching strategy:
|
|
16
|
-
* - MDX content cached in R2 (fast fetch)
|
|
17
|
-
* - Vercel edge CDN caches responses
|
|
18
|
-
* - On-demand revalidation via /api/revalidate endpoint
|
|
19
|
-
*/
|
|
20
|
-
export const dynamic = 'force-dynamic';
|
|
21
|
-
export const dynamicParams = true; // Allow paths not in generateStaticParams
|
|
22
|
-
import { MDXComponents } from '@/components/mdx/MDXComponents';
|
|
23
|
-
import { Breadcrumb } from '@/components/navigation/Breadcrumb';
|
|
24
|
-
import { TableOfContents } from '@/components/navigation/TableOfContents';
|
|
25
|
-
import { PageColumns } from '@/components/layout/PageColumns';
|
|
26
|
-
import { PageNavigation } from '@/components/navigation/PageNavigation';
|
|
27
|
-
import { SocialFooter } from '@/components/navigation/SocialFooter';
|
|
28
|
-
import { ApiPageWrapper } from '@/components/mdx/ApiPage';
|
|
29
|
-
import { OpenApiEndpoint } from '@/components/mdx/OpenApiEndpoint';
|
|
30
|
-
import { OpenApiError } from '@/components/openapi/OpenApiError';
|
|
31
|
-
import { getHighlighter } from '@/lib/shiki-highlighter';
|
|
32
|
-
import { createShikiRehypePlugin } from '@/lib/shiki-config';
|
|
33
|
-
import rehypeSlug from 'rehype-slug';
|
|
34
|
-
import remarkGfm from 'remark-gfm';
|
|
35
|
-
import { rehypeCodeMeta, rehypeRestoreDataTitle } from '@/lib/rehype-code-meta';
|
|
36
|
-
import { rehypeClassToClassName } from '@/lib/rehype-class-to-classname';
|
|
37
|
-
import { rehypeNoZoomToData } from '@/lib/rehype-nozoom-to-data';
|
|
38
|
-
import { preprocessMdx, containsPanel, containsView, buildSnippetAliasMap } from '@/lib/preprocess-mdx';
|
|
39
|
-
import { loadSnippetsForIsr } from '@/lib/snippet-loader-isr';
|
|
40
|
-
import { PanelWrapper } from '@/components/mdx/PanelWrapper';
|
|
41
|
-
import { ViewWrapper } from '@/components/mdx/View';
|
|
42
|
-
import { getLatexRemarkPlugins, getLatexRehypePlugins } from '@/lib/latex-config';
|
|
43
|
-
import { getTypographyRemarkPlugins } from '@/lib/typography-config';
|
|
44
|
-
import { remarkVisibility } from '@/lib/remark-visibility';
|
|
45
|
-
import { recmaCompoundComponents } from '@/lib/recma-compound-components';
|
|
46
|
-
import { extractInlineComponents } from '@/lib/process-mdx-with-exports';
|
|
47
|
-
import { extractHeadings } from '@/lib/heading-extractor';
|
|
48
|
-
import { StepSlugProvider, type StepSlugEntry } from '@/components/mdx/StepSlugContext';
|
|
49
|
-
import { buildEndpointFromMdx } from '@/lib/build-endpoint-from-mdx';
|
|
50
|
-
import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
51
7
|
import fs from 'fs';
|
|
52
8
|
import path from 'path';
|
|
9
|
+
import type { Metadata } from 'next';
|
|
53
10
|
import { getContentDir } from '@/lib/docs';
|
|
54
|
-
import type { DocsConfig } from '@/lib/docs-types';
|
|
55
|
-
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
56
|
-
import { buildJsonLd } from '@/lib/json-ld';
|
|
57
11
|
import {
|
|
58
|
-
getContentLoader,
|
|
59
12
|
isIsrMode,
|
|
60
13
|
getProjectFromRequest,
|
|
61
14
|
getHostAtDocs,
|
|
62
|
-
normalizeSlugForContent,
|
|
63
|
-
parseFrontmatter,
|
|
64
|
-
projectExists,
|
|
65
15
|
} from '@/lib/content-loader';
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
68
|
-
parseOpenApiFrontmatter,
|
|
69
|
-
getCachedSpec,
|
|
70
|
-
parseEndpoint,
|
|
71
|
-
generateCodeExamples,
|
|
72
|
-
formatOpenApiWarning,
|
|
73
|
-
deriveAuthFromSecurity,
|
|
74
|
-
type OpenApiEndpointData,
|
|
75
|
-
type CodeExample,
|
|
76
|
-
type AuthMethod,
|
|
77
|
-
} from '@/lib/openapi';
|
|
78
|
-
import { classifyOpenApiLoadError } from '@/lib/openapi/classify-load-error';
|
|
79
|
-
import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
|
|
80
|
-
import { findFirstNavPage } from '@/lib/find-first-nav-page';
|
|
81
|
-
import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
82
|
-
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
83
|
-
import { ImagePriorityProvider } from '@/components/mdx/ImagePriorityProvider';
|
|
84
|
-
import { AIActionsMenu } from '@/components/AIActionsMenu';
|
|
85
|
-
import { getContextualOptions } from '@/lib/contextual-defaults';
|
|
86
|
-
|
|
87
|
-
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Parse MDX api frontmatter field (e.g., "POST /analytics/post")
|
|
91
|
-
* Returns the HTTP method and path
|
|
92
|
-
*/
|
|
93
|
-
function parseMdxApiField(apiField: string): { method: HttpMethod; path: string } | null {
|
|
94
|
-
if (!apiField || typeof apiField !== 'string') return null;
|
|
95
|
-
|
|
96
|
-
const trimmed = apiField.trim();
|
|
97
|
-
const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE'];
|
|
98
|
-
|
|
99
|
-
for (const method of methods) {
|
|
100
|
-
if (trimmed.toUpperCase().startsWith(method)) {
|
|
101
|
-
const path = trimmed.slice(method.length).trim();
|
|
102
|
-
return { method, path };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
16
|
+
import { renderDocPage, buildDocMetadata, type RenderInput } from '@/lib/render-doc-page';
|
|
107
17
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Note: ProjectSnippets.tsx is server-compatible (no 'use client'),
|
|
111
|
-
// individual components have 'use client' in their own files
|
|
112
|
-
import { SnippetComponents } from '@/components/snippets/ProjectSnippets';
|
|
18
|
+
export const dynamic = 'force-dynamic';
|
|
19
|
+
export const dynamicParams = true;
|
|
113
20
|
|
|
114
21
|
interface PageProps {
|
|
115
22
|
params: Promise<{
|
|
23
|
+
project?: string;
|
|
116
24
|
slug?: string[];
|
|
117
25
|
}>;
|
|
118
26
|
}
|
|
119
27
|
|
|
120
|
-
const DEFAULT_SITE_URL = 'https://docs.example.com';
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Resolve the base URL for SEO metadata and JSON-LD.
|
|
124
|
-
* ISR mode: derived from request headers (handles custom domains).
|
|
125
|
-
* Static mode: uses SITE_URL env var set during build.
|
|
126
|
-
*/
|
|
127
|
-
function resolveBaseUrl(
|
|
128
|
-
requestHeaders: Headers | null,
|
|
129
|
-
projectSlug: string | null,
|
|
130
|
-
hostAtDocs: boolean
|
|
131
|
-
): string {
|
|
132
|
-
if (requestHeaders && projectSlug) {
|
|
133
|
-
return getBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
134
|
-
}
|
|
135
|
-
return process.env.SITE_URL || DEFAULT_SITE_URL;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* docs.json override is treated as a UNIT — if method is set, both method and name
|
|
140
|
-
* come from docs.json, avoiding a stale customer-set `name` pairing with a
|
|
141
|
-
* spec-derived `method`. Falls back to deriving auth from the OpenAPI security schemes.
|
|
142
|
-
*/
|
|
143
|
-
function resolveAuth(
|
|
144
|
-
endpoint: OpenApiEndpointData | null | undefined,
|
|
145
|
-
config: DocsConfig,
|
|
146
|
-
): { method?: AuthMethod; headerName?: string } {
|
|
147
|
-
const override = config.api?.mdx?.auth;
|
|
148
|
-
if (override?.method) {
|
|
149
|
-
return { method: override.method, headerName: override.name };
|
|
150
|
-
}
|
|
151
|
-
return endpoint ? deriveAuthFromSecurity(endpoint.security) : {};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Frontmatter data from MDX files.
|
|
156
|
-
*/
|
|
157
|
-
interface FrontmatterData {
|
|
158
|
-
title?: string;
|
|
159
|
-
description?: string;
|
|
160
|
-
api?: string;
|
|
161
|
-
openapi?: string;
|
|
162
|
-
playground?: string;
|
|
163
|
-
mode?: string;
|
|
164
|
-
hideFooter?: boolean;
|
|
165
|
-
rss?: boolean;
|
|
166
|
-
[key: string]: unknown;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
28
|
function getAllDocPaths(): string[] {
|
|
170
29
|
const contentDir = getContentDir();
|
|
171
30
|
const paths: string[] = [];
|
|
172
31
|
|
|
173
32
|
function traverseDir(dir: string, basePath: string = '') {
|
|
174
33
|
if (!fs.existsSync(dir)) return;
|
|
175
|
-
|
|
176
34
|
const files = fs.readdirSync(dir);
|
|
177
|
-
|
|
178
35
|
for (const file of files) {
|
|
179
|
-
// Skip
|
|
36
|
+
// Skip dotfiles (.claude, .git, etc.)
|
|
180
37
|
if (file.startsWith('.')) continue;
|
|
181
|
-
|
|
182
38
|
const filePath = path.join(dir, file);
|
|
183
|
-
|
|
184
|
-
// Handle broken symlinks gracefully
|
|
185
39
|
let stat;
|
|
186
40
|
try {
|
|
187
41
|
stat = fs.statSync(filePath);
|
|
188
42
|
} catch {
|
|
189
|
-
// Broken symlink or inaccessible
|
|
43
|
+
// Broken symlink or inaccessible — skip
|
|
190
44
|
continue;
|
|
191
45
|
}
|
|
192
|
-
|
|
193
46
|
if (stat.isDirectory()) {
|
|
194
47
|
traverseDir(filePath, path.join(basePath, file));
|
|
195
48
|
} else if (file.endsWith('.mdx')) {
|
|
@@ -203,618 +56,70 @@ function getAllDocPaths(): string[] {
|
|
|
203
56
|
return paths;
|
|
204
57
|
}
|
|
205
58
|
|
|
206
|
-
function findFirstPage(config: DocsConfig, lang?: string): string {
|
|
207
|
-
const nav = config.navigation;
|
|
208
|
-
const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
|
|
209
|
-
const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
|
|
210
|
-
return result ? result.replace(/^\//, '') : 'introduction';
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function needsSlugRewrite(slug: string[]): boolean {
|
|
214
|
-
return slug.length === 0 || (slug.length === 1 && isValidLanguageCode(slug[0]));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
|
|
218
|
-
if (normalizedSlug.length === 0) return pathToSlug(findFirstPage(config));
|
|
219
|
-
if (normalizedSlug.length === 1 && isValidLanguageCode(normalizedSlug[0])) {
|
|
220
|
-
return pathToSlug(findFirstPage(config, normalizedSlug[0]));
|
|
221
|
-
}
|
|
222
|
-
return normalizedSlug;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
59
|
export async function generateStaticParams() {
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
if (isIsrMode()) {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
60
|
+
// ISR: pages generated on-demand, no build-time pre-render.
|
|
61
|
+
if (isIsrMode()) return [];
|
|
231
62
|
|
|
232
|
-
// Static mode: pre-render all pages from filesystem
|
|
233
63
|
const paths = getAllDocPaths();
|
|
64
|
+
// next-mdx-remote can't compile relative MDX imports — skip those tests.
|
|
65
|
+
const unsupportedPatterns = ['deep-relative-test', 'relative-snippets-test'];
|
|
234
66
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
const supportedPaths = paths.filter(path => {
|
|
242
|
-
const hasUnsupportedPattern = unsupportedPatterns.some(pattern =>
|
|
243
|
-
path.includes(pattern)
|
|
244
|
-
);
|
|
245
|
-
if (hasUnsupportedPattern) {
|
|
246
|
-
console.log(`[Build] Skipping page with unsupported relative imports: ${path}`);
|
|
247
|
-
}
|
|
248
|
-
return !hasUnsupportedPattern;
|
|
67
|
+
const supportedPaths = paths.filter(p => {
|
|
68
|
+
const skip = unsupportedPatterns.some(pattern => p.includes(pattern));
|
|
69
|
+
if (skip) console.log(`[Build] Skipping page with unsupported relative imports: ${p}`);
|
|
70
|
+
return !skip;
|
|
249
71
|
});
|
|
250
72
|
|
|
251
|
-
// Include empty slug for root route (resolves to first page in-place)
|
|
252
73
|
return [
|
|
253
74
|
{ slug: [] },
|
|
254
|
-
...supportedPaths.map((
|
|
255
|
-
slug: path.split('/'),
|
|
256
|
-
})),
|
|
75
|
+
...supportedPaths.map((p) => ({ slug: p.split('/') })),
|
|
257
76
|
];
|
|
258
77
|
}
|
|
259
78
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!projectSlug) {
|
|
274
|
-
return { title: 'Not Found' };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const exists = await projectExists(projectSlug);
|
|
278
|
-
if (!exists) {
|
|
279
|
-
return { title: 'Not Found' };
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Get content loader (ISR: R2, static: filesystem)
|
|
284
|
-
const loader = getContentLoader(projectSlug ?? undefined);
|
|
285
|
-
|
|
286
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
287
|
-
// Empty root → resolve to first page (see DocPage for the full rationale).
|
|
288
|
-
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
289
|
-
const configP = loader.getConfig();
|
|
290
|
-
const slug = needsSlugRewrite(normalizedSlug)
|
|
291
|
-
? resolveSlug(normalizedSlug, await configP)
|
|
292
|
-
: normalizedSlug;
|
|
293
|
-
const isRoot = normalizedSlug.length === 0;
|
|
294
|
-
const pagePath = slug.join('/');
|
|
295
|
-
|
|
296
|
-
const [fileContents, config] = await Promise.all([
|
|
297
|
-
loader.getContent(pagePath).catch(() => null),
|
|
298
|
-
configP,
|
|
299
|
-
]);
|
|
300
|
-
|
|
301
|
-
if (!fileContents) {
|
|
79
|
+
// Resolve project + hostAtDocs from URL params (direct `/[project]/...`
|
|
80
|
+
// hits in non-ISR local dev / tests) or from middleware-set headers (the
|
|
81
|
+
// production path). The header read is what forces this segment dynamic.
|
|
82
|
+
async function resolveInput(params: PageProps['params']): Promise<RenderInput> {
|
|
83
|
+
const resolved = await params;
|
|
84
|
+
const projectFromParams = resolved.project ?? null;
|
|
85
|
+
|
|
86
|
+
if (projectFromParams) {
|
|
87
|
+
// Direct /[project]/... URLs only hit this branch from tests and the
|
|
88
|
+
// non-ISR CLI dev server — production always goes through middleware
|
|
89
|
+
// and the header branch below. hostAtDocs derived from env so the
|
|
90
|
+
// segment stays statically analyzable when this branch is hit.
|
|
302
91
|
return {
|
|
303
|
-
|
|
92
|
+
slug: resolved.slug || [],
|
|
93
|
+
projectSlug: projectFromParams,
|
|
94
|
+
hostAtDocs: process.env.HOST_AT_DOCS === 'true',
|
|
95
|
+
requestHeaders: null,
|
|
304
96
|
};
|
|
305
97
|
}
|
|
306
98
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
99
|
+
if (!isIsrMode()) {
|
|
100
|
+
return {
|
|
101
|
+
slug: resolved.slug || [],
|
|
102
|
+
projectSlug: null,
|
|
103
|
+
hostAtDocs: process.env.HOST_AT_DOCS === 'true',
|
|
104
|
+
requestHeaders: null,
|
|
105
|
+
};
|
|
313
106
|
}
|
|
314
107
|
|
|
315
|
-
const
|
|
316
|
-
const languages = config.navigation?.languages;
|
|
317
|
-
|
|
318
|
-
const seoMetadata = buildSeoMetadata(config, data, pagePath, baseUrl, languages);
|
|
319
|
-
|
|
320
|
-
// If page title matches config.name, use absolute to prevent "X — X" double-wrap.
|
|
321
|
-
// If no title, use buildSiteTitle (which avoids "X Documentation Documentation").
|
|
322
|
-
const titleValue = data.title
|
|
323
|
-
? (data.title === config.name ? { absolute: data.title } : data.title)
|
|
324
|
-
: { absolute: buildSiteTitle(config.name) };
|
|
325
|
-
|
|
108
|
+
const requestHeaders = await headers();
|
|
326
109
|
return {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// noindex as a second dedup signal alongside the canonical tag.
|
|
332
|
-
...(isRoot && { robots: { index: false, follow: true } }),
|
|
333
|
-
...(data.rss ? {
|
|
334
|
-
alternates: {
|
|
335
|
-
...seoMetadata.alternates,
|
|
336
|
-
types: {
|
|
337
|
-
'application/rss+xml': hostAtDocs ? '/docs/feed.xml' : '/feed.xml',
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
} : {}),
|
|
110
|
+
slug: resolved.slug || [],
|
|
111
|
+
projectSlug: getProjectFromRequest(requestHeaders),
|
|
112
|
+
hostAtDocs: getHostAtDocs(requestHeaders),
|
|
113
|
+
requestHeaders,
|
|
341
114
|
};
|
|
342
115
|
}
|
|
343
116
|
|
|
344
|
-
export
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
let projectSlug: string | null = null;
|
|
349
|
-
let hostAtDocs = process.env.HOST_AT_DOCS === 'true'; // Local dev fallback
|
|
350
|
-
let requestHeaders: Headers | null = null;
|
|
351
|
-
if (isIsrMode()) {
|
|
352
|
-
requestHeaders = await headers();
|
|
353
|
-
projectSlug = getProjectFromRequest(requestHeaders);
|
|
354
|
-
hostAtDocs = getHostAtDocs(requestHeaders);
|
|
355
|
-
if (!projectSlug) {
|
|
356
|
-
notFound(); // Project not resolved by middleware
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Check if project has content in R2
|
|
360
|
-
// This returns 404 instead of 500 for projects that haven't been built yet
|
|
361
|
-
const exists = await projectExists(projectSlug);
|
|
362
|
-
if (!exists) {
|
|
363
|
-
notFound(); // Project not built to R2 yet
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Get content loader (ISR: R2, static: filesystem)
|
|
368
|
-
const loader = getContentLoader(projectSlug ?? undefined);
|
|
369
|
-
|
|
370
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
371
|
-
// Empty root renders the first page in place rather than 307'ing — Next's
|
|
372
|
-
// redirect() emits cache-control: private, blocking CDN caching. Canonical
|
|
373
|
-
// + noindex in generateMetadata prevent duplicate indexing.
|
|
374
|
-
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
375
|
-
const configP = loader.getConfig();
|
|
376
|
-
const slug = needsSlugRewrite(normalizedSlug)
|
|
377
|
-
? resolveSlug(normalizedSlug, await configP)
|
|
378
|
-
: normalizedSlug;
|
|
379
|
-
const pagePath = slug.join('/');
|
|
380
|
-
const currentLang = extractLanguageFromPath(`/${pagePath}`);
|
|
381
|
-
const [fileContents, config] = await Promise.all([
|
|
382
|
-
loader.getContent(pagePath).catch(() => null),
|
|
383
|
-
configP,
|
|
384
|
-
]);
|
|
385
|
-
|
|
386
|
-
// Check if content exists (getContent returns null via catch if not found)
|
|
387
|
-
if (!fileContents) {
|
|
388
|
-
notFound();
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const parsed = parseFrontmatter(fileContents);
|
|
392
|
-
const data = parsed.data as FrontmatterData;
|
|
393
|
-
const rawContent = parsed.content;
|
|
394
|
-
|
|
395
|
-
// JSON-LD structured data for rich search results.
|
|
396
|
-
// XSS-safe: replaces < with unicode escape per Next.js guide.
|
|
397
|
-
// Same pattern as marketing/app/layout.tsx and marketing/app/blog/[slug]/page.tsx.
|
|
398
|
-
const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
|
|
399
|
-
const jsonLd = buildJsonLd({
|
|
400
|
-
config,
|
|
401
|
-
pagePath,
|
|
402
|
-
pageTitle: data.title || pagePath,
|
|
403
|
-
baseUrl,
|
|
404
|
-
});
|
|
405
|
-
const jsonLdScript = (
|
|
406
|
-
<script
|
|
407
|
-
type="application/ld+json"
|
|
408
|
-
dangerouslySetInnerHTML={{
|
|
409
|
-
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
|
|
410
|
-
}}
|
|
411
|
-
/>
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
// Get cached Shiki highlighter for syntax highlighting
|
|
415
|
-
// This singleton is reused across all page renders for performance
|
|
416
|
-
const highlighter = await getHighlighter();
|
|
417
|
-
|
|
418
|
-
// Load snippets - different behavior for ISR vs static mode:
|
|
419
|
-
// - ISR mode: Fetch and compile snippets dynamically from R2
|
|
420
|
-
// - Static mode: Use pre-compiled SnippetComponents from build time
|
|
421
|
-
let snippetAliases: Record<string, React.ComponentType<unknown>> = {};
|
|
422
|
-
|
|
423
|
-
if (isIsrMode() && projectSlug) {
|
|
424
|
-
// ISR mode: Load snippets dynamically from R2
|
|
425
|
-
// This is required because ISR pages are rendered on-demand and can't use
|
|
426
|
-
// the static SnippetComponents which are empty in ISR builds
|
|
427
|
-
snippetAliases = await loadSnippetsForIsr(projectSlug, rawContent, MDXComponents);
|
|
428
|
-
} else {
|
|
429
|
-
// Static mode: Use pre-compiled snippets from build time
|
|
430
|
-
// This maps import aliases to compiled components:
|
|
431
|
-
// import Propagating from '/snippets/custom-subpath-propagating.mdx'
|
|
432
|
-
// maps 'Propagating' -> SnippetComponents['CustomSubpathPropagating']
|
|
433
|
-
snippetAliases = buildSnippetAliasMap(rawContent, SnippetComponents);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Preprocess MDX content to strip snippet imports
|
|
437
|
-
// (snippets are injected globally via AllComponents)
|
|
438
|
-
const content = preprocessMdx(rawContent, { assetVersion: config.assetVersion });
|
|
439
|
-
|
|
440
|
-
// Pre-resolve unique slugs for every <Step> on the page so duplicate titles
|
|
441
|
-
// across separate <Steps> blocks get -2, -3, ... suffixes that match the
|
|
442
|
-
// build-time TOC. Provider supplies the list to <Step> via context.
|
|
443
|
-
const stepEntries: StepSlugEntry[] = extractHeadings(content)
|
|
444
|
-
.filter(h => typeof h.stepNumber === 'number')
|
|
445
|
-
.map(h => ({ title: h.text, slug: h.id }));
|
|
446
|
-
|
|
447
|
-
// Extract and compile inline component exports from MDX
|
|
448
|
-
// Only pass MDXComponents (server-compatible) to inline extraction
|
|
449
|
-
const { inlineComponents, paramFields } = await extractInlineComponents(content, MDXComponents);
|
|
450
|
-
|
|
451
|
-
// Check for component name collisions and warn
|
|
452
|
-
const overriddenComponents = Object.keys(inlineComponents).filter(
|
|
453
|
-
(name) => name in MDXComponents
|
|
454
|
-
);
|
|
455
|
-
if (overriddenComponents.length > 0) {
|
|
456
|
-
console.warn(
|
|
457
|
-
`[MDX] Inline component(s) override built-in: ${overriddenComponents.join(', ')} in ${slug.join('/')}`
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Merge all components for MDXRemote:
|
|
462
|
-
// 1. Built-in MDX components (Card, Note, etc.)
|
|
463
|
-
// 2. Compiled snippet components (by filename-based names)
|
|
464
|
-
// 3. Snippet alias mappings (user's import aliases -> components)
|
|
465
|
-
// 4. Inline component exports from this MDX file
|
|
466
|
-
// 5. Auto-prefix `a` for hostAtDocs sites (so /path → /docs/path)
|
|
467
|
-
const AllComponentsWithInline = {
|
|
468
|
-
...MDXComponents,
|
|
469
|
-
...SnippetComponents,
|
|
470
|
-
...snippetAliases,
|
|
471
|
-
...inlineComponents,
|
|
472
|
-
// When hostAtDocs is true, override the base `a` from MDXComponents.tsx
|
|
473
|
-
// to auto-prefix absolute internal links with /docs.
|
|
474
|
-
// Styling must stay in sync with the base `a` in MDXComponents.tsx.
|
|
475
|
-
...(hostAtDocs ? {
|
|
476
|
-
a: ({ ariaLabel, href, ...props }: any) => {
|
|
477
|
-
const needsPrefix = href?.startsWith('/') && !href.startsWith('/docs/') && href !== '/docs';
|
|
478
|
-
return (
|
|
479
|
-
<a
|
|
480
|
-
className="text-theme-accent hover:text-theme-accent-hover transition-colors"
|
|
481
|
-
aria-label={ariaLabel}
|
|
482
|
-
href={needsPrefix ? `/docs${href}` : href}
|
|
483
|
-
{...props}
|
|
484
|
-
/>
|
|
485
|
-
);
|
|
486
|
-
},
|
|
487
|
-
} : {}),
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// Check if this is an API page (has api or openapi frontmatter)
|
|
491
|
-
const isApiPage = !!data.api || !!data.openapi;
|
|
492
|
-
|
|
493
|
-
// Check if wide mode is enabled (hides TOC, content expands to full width)
|
|
494
|
-
const isWideMode = data.mode === 'wide';
|
|
495
|
-
|
|
496
|
-
// Check if content contains a Panel component (replaces ToC with custom sidebar content)
|
|
497
|
-
const hasPanel = containsPanel(content);
|
|
498
|
-
|
|
499
|
-
// Check if content contains View components (multi-view content like language-specific docs)
|
|
500
|
-
const hasView = containsView(content);
|
|
501
|
-
|
|
502
|
-
// Parse OpenAPI endpoint data if openapi frontmatter is present
|
|
503
|
-
let openApiEndpointData: OpenApiEndpointData | null = null;
|
|
504
|
-
let openApiCodeExamples: CodeExample[] | null = null;
|
|
505
|
-
let openApiError: string | null = null;
|
|
506
|
-
|
|
507
|
-
// OpenAPI spec parsing - supports both static and ISR modes
|
|
508
|
-
let lastFailure: { err: unknown; specPath: string } | null = null;
|
|
509
|
-
if (data.openapi && typeof data.openapi === 'string') {
|
|
510
|
-
try {
|
|
511
|
-
// Normalize config to array (handles string, array, or undefined)
|
|
512
|
-
const openApiConfig = config.api?.openapi;
|
|
513
|
-
const allSpecPaths: string[] = typeof openApiConfig === 'string'
|
|
514
|
-
? [openApiConfig]
|
|
515
|
-
: Array.isArray(openApiConfig)
|
|
516
|
-
? openApiConfig
|
|
517
|
-
: [];
|
|
518
|
-
|
|
519
|
-
const parsed = parseOpenApiFrontmatter(
|
|
520
|
-
data.openapi,
|
|
521
|
-
allSpecPaths.length > 0 ? allSpecPaths : undefined
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
const baseSpecs = parsed.isShortFormat && allSpecPaths.length > 1
|
|
525
|
-
? allSpecPaths
|
|
526
|
-
: [parsed.specPath];
|
|
527
|
-
// For each base spec, expand into [<base>.<lang>.ext, <base>.ext] when
|
|
528
|
-
// the page is under a localized URL. Resolver tries each in order; the
|
|
529
|
-
// first that loads wins. Missing lang variant falls back to English.
|
|
530
|
-
const specsToTry = baseSpecs.flatMap(p => candidateSpecPaths(p, currentLang));
|
|
531
|
-
|
|
532
|
-
// Hoist mode-dependent values before the loop
|
|
533
|
-
const useIsr = isIsrMode() && !!projectSlug;
|
|
534
|
-
const resolveSpec = useIsr
|
|
535
|
-
? (await import('@/lib/openapi-isr')).resolveOpenApiSpec
|
|
536
|
-
: null;
|
|
537
|
-
const contentDir = useIsr ? null : getContentDir();
|
|
538
|
-
|
|
539
|
-
for (let i = 0; i < specsToTry.length; i++) {
|
|
540
|
-
const specPath = specsToTry[i];
|
|
541
|
-
try {
|
|
542
|
-
if (resolveSpec && projectSlug) {
|
|
543
|
-
const spec = await resolveSpec(projectSlug, specPath);
|
|
544
|
-
openApiEndpointData = parseEndpoint(
|
|
545
|
-
spec as Parameters<typeof parseEndpoint>[0],
|
|
546
|
-
parsed.method,
|
|
547
|
-
parsed.path,
|
|
548
|
-
specPath
|
|
549
|
-
);
|
|
550
|
-
} else {
|
|
551
|
-
const { api } = await getCachedSpec(specPath, contentDir!);
|
|
552
|
-
openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, specPath);
|
|
553
|
-
}
|
|
554
|
-
lastFailure = null;
|
|
555
|
-
break;
|
|
556
|
-
} catch (err) {
|
|
557
|
-
lastFailure = { err, specPath };
|
|
558
|
-
const isLast = i === specsToTry.length - 1;
|
|
559
|
-
if (!isLast) {
|
|
560
|
-
// Lang variant (or intermediate candidate) failed — log so we
|
|
561
|
-
// notice transient R2 errors that silently fall through to English.
|
|
562
|
-
console.warn(
|
|
563
|
-
`[openapi] spec candidate "${specPath}" failed; trying next: ${(err as Error).message}`
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (lastFailure) {
|
|
570
|
-
throw lastFailure.err;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Generate code examples
|
|
574
|
-
if (openApiEndpointData) {
|
|
575
|
-
const { method: authMethod, headerName: authHeaderName } = resolveAuth(openApiEndpointData, config);
|
|
576
|
-
const languages = config.api?.examples?.languages;
|
|
577
|
-
openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, authHeaderName, languages });
|
|
578
|
-
}
|
|
579
|
-
} catch (err) {
|
|
580
|
-
const typed = classifyOpenApiLoadError(err, lastFailure?.specPath ?? null);
|
|
581
|
-
console.warn(formatOpenApiWarning(typed));
|
|
582
|
-
openApiError = typed.suggestion
|
|
583
|
-
? `${typed.message} — ${typed.suggestion}`
|
|
584
|
-
: typed.message;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Parse MDX api field for pages with api: frontmatter (but not openapi:)
|
|
589
|
-
let mdxApiMethod: HttpMethod | null = null;
|
|
590
|
-
let mdxApiPath: string | null = null;
|
|
591
|
-
|
|
592
|
-
if (data.api && typeof data.api === 'string' && !data.openapi) {
|
|
593
|
-
const parsed = parseMdxApiField(data.api);
|
|
594
|
-
if (parsed) {
|
|
595
|
-
mdxApiMethod = parsed.method;
|
|
596
|
-
mdxApiPath = parsed.path;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// RSS feed icon for pages with rss: true
|
|
601
|
-
const rssIcon = data.rss ? (
|
|
602
|
-
<a
|
|
603
|
-
href={hostAtDocs ? '/docs/feed.xml' : '/feed.xml'}
|
|
604
|
-
target="_blank"
|
|
605
|
-
rel="noopener noreferrer"
|
|
606
|
-
aria-label="Subscribe to RSS feed"
|
|
607
|
-
title="RSS feed"
|
|
608
|
-
className="flex-shrink-0 text-[var(--color-text-muted)] hover:text-[var(--color-primary)] transition-colors cursor-pointer"
|
|
609
|
-
>
|
|
610
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
|
|
611
|
-
<circle cx="6.18" cy="17.82" r="2.18" />
|
|
612
|
-
<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" />
|
|
613
|
-
</svg>
|
|
614
|
-
</a>
|
|
615
|
-
) : null;
|
|
616
|
-
|
|
617
|
-
// Contextual AI Actions menu options
|
|
618
|
-
const contextualOptions = getContextualOptions(config);
|
|
619
|
-
const hasAiActions = contextualOptions.length > 0;
|
|
620
|
-
|
|
621
|
-
// Prose class for MDX content styling (defined in base.css)
|
|
622
|
-
const proseClasses = 'prose max-w-none';
|
|
623
|
-
|
|
624
|
-
// Playground configuration — covers both openapi: and api: pages
|
|
625
|
-
const hasApiEndpoint = openApiEndpointData || (mdxApiMethod && mdxApiPath);
|
|
626
|
-
const playgroundDisplay = hasApiEndpoint
|
|
627
|
-
? ((data.playground as 'interactive' | 'simple' | 'none' | undefined)
|
|
628
|
-
|| config.api?.playground?.display || 'interactive') as 'interactive' | 'simple' | 'none'
|
|
629
|
-
: 'none';
|
|
630
|
-
const mdxServerConfig = config.api?.mdx?.server;
|
|
631
|
-
const fallbackServerUrl = Array.isArray(mdxServerConfig) ? mdxServerConfig[0] : mdxServerConfig;
|
|
632
|
-
// Force proxy on hostAtDocs sites — direct mode is always cross-origin there
|
|
633
|
-
const proxyEnabled = hostAtDocs
|
|
634
|
-
|| config.api?.playground?.proxy
|
|
635
|
-
|| (config.api?.playground?.proxy == null && playgroundDisplay === 'interactive');
|
|
636
|
-
|
|
637
|
-
// Build endpoint data for api: pages (for playground)
|
|
638
|
-
let mdxEndpointData: OpenApiEndpointData | undefined;
|
|
639
|
-
if (!openApiEndpointData && mdxApiMethod && mdxApiPath && playgroundDisplay !== 'none') {
|
|
640
|
-
mdxEndpointData = buildEndpointFromMdx(mdxApiMethod, mdxApiPath, paramFields, fallbackServerUrl);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const resolvedMdxAuth = resolveAuth(mdxEndpointData, config);
|
|
644
|
-
const resolvedOpenApiAuth = resolveAuth(openApiEndpointData, config);
|
|
645
|
-
|
|
646
|
-
// For API pages, wrap the entire content area with ApiPageWrapper
|
|
647
|
-
// so code panels can be positioned as siblings at the page level
|
|
648
|
-
if (isApiPage) {
|
|
649
|
-
return (
|
|
650
|
-
<>{jsonLdScript}<ApiPageWrapper>
|
|
651
|
-
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10 flex-1 min-w-0">
|
|
652
|
-
{/* Breadcrumb */}
|
|
653
|
-
<Breadcrumb slug={slug} config={config} />
|
|
654
|
-
|
|
655
|
-
{/* Page Header */}
|
|
656
|
-
{data.title && (
|
|
657
|
-
<header className="mb-4 sm:mb-6">
|
|
658
|
-
<div className="flex items-center gap-3">
|
|
659
|
-
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-theme-text-primary tracking-tight">
|
|
660
|
-
{data.title}
|
|
661
|
-
</h1>
|
|
662
|
-
{rssIcon}
|
|
663
|
-
{hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
664
|
-
</div>
|
|
665
|
-
{data.description && (
|
|
666
|
-
<p className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3">
|
|
667
|
-
{data.description}
|
|
668
|
-
</p>
|
|
669
|
-
)}
|
|
670
|
-
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
671
|
-
</header>
|
|
672
|
-
)}
|
|
673
|
-
|
|
674
|
-
{/* Content */}
|
|
675
|
-
<div className={proseClasses}>
|
|
676
|
-
{/* MDX API endpoint badge (for pages with api: frontmatter) */}
|
|
677
|
-
{mdxApiMethod && mdxApiPath && (
|
|
678
|
-
mdxEndpointData && playgroundDisplay !== 'none' ? (
|
|
679
|
-
<OpenApiEndpoint
|
|
680
|
-
endpoint={mdxEndpointData}
|
|
681
|
-
playgroundOnly
|
|
682
|
-
playgroundDisplay={playgroundDisplay}
|
|
683
|
-
authMethod={resolvedMdxAuth.method}
|
|
684
|
-
authHeaderName={resolvedMdxAuth.headerName}
|
|
685
|
-
serverUrl={fallbackServerUrl}
|
|
686
|
-
proxyEnabled={proxyEnabled}
|
|
687
|
-
languages={config.api?.examples?.languages}
|
|
688
|
-
/>
|
|
689
|
-
) : (
|
|
690
|
-
<ApiEndpoint
|
|
691
|
-
method={mdxApiMethod}
|
|
692
|
-
path={mdxApiPath}
|
|
693
|
-
baseUrl={fallbackServerUrl}
|
|
694
|
-
/>
|
|
695
|
-
)
|
|
696
|
-
)}
|
|
697
|
-
|
|
698
|
-
{/* OpenAPI endpoint documentation (auto-generated from spec) */}
|
|
699
|
-
{openApiEndpointData && (
|
|
700
|
-
<OpenApiEndpoint
|
|
701
|
-
endpoint={openApiEndpointData}
|
|
702
|
-
codeExamples={openApiCodeExamples || undefined}
|
|
703
|
-
playgroundDisplay={playgroundDisplay}
|
|
704
|
-
authMethod={resolvedOpenApiAuth.method}
|
|
705
|
-
authHeaderName={resolvedOpenApiAuth.headerName}
|
|
706
|
-
serverUrl={fallbackServerUrl}
|
|
707
|
-
proxyEnabled={proxyEnabled}
|
|
708
|
-
languages={config.api?.examples?.languages}
|
|
709
|
-
/>
|
|
710
|
-
)}
|
|
711
|
-
|
|
712
|
-
{/* OpenAPI error — shown when spec parsing fails */}
|
|
713
|
-
{!openApiEndpointData && openApiError && (
|
|
714
|
-
<OpenApiError message={openApiError} slug={slug.join('/')} />
|
|
715
|
-
)}
|
|
716
|
-
|
|
717
|
-
{/* Additional MDX content — strip <ResponseExample> on OpenAPI pages
|
|
718
|
-
(auto-generated ResponseExamplePanel already handles responses) */}
|
|
719
|
-
<ImagePriorityProvider>
|
|
720
|
-
<StepSlugProvider entries={stepEntries}>
|
|
721
|
-
<MDXRemote
|
|
722
|
-
source={openApiEndpointData
|
|
723
|
-
? content.replace(/<ResponseExample>[\s\S]*?<\/ResponseExample>/g, '')
|
|
724
|
-
: content}
|
|
725
|
-
components={AllComponentsWithInline}
|
|
726
|
-
options={{
|
|
727
|
-
// Keep expression props (e.g. cols={2}) compatible under next-mdx-remote v6.
|
|
728
|
-
...mdxSecurityOptions,
|
|
729
|
-
mdxOptions: {
|
|
730
|
-
remarkPlugins: [remarkGfm, [remarkVisibility, { audience: 'humans' }], ...getTypographyRemarkPlugins(config), ...getLatexRemarkPlugins(config)],
|
|
731
|
-
rehypePlugins: [rehypeNoZoomToData, rehypeClassToClassName, rehypeCodeMeta, createShikiRehypePlugin(highlighter, config), rehypeRestoreDataTitle, ...getLatexRehypePlugins(config), rehypeSlug],
|
|
732
|
-
recmaPlugins: [recmaCompoundComponents],
|
|
733
|
-
},
|
|
734
|
-
}}
|
|
735
|
-
/>
|
|
736
|
-
</StepSlugProvider>
|
|
737
|
-
</ImagePriorityProvider>
|
|
738
|
-
</div>
|
|
739
|
-
|
|
740
|
-
{/* Previous/Next Navigation */}
|
|
741
|
-
<PageNavigation currentSlug={slug.join('/')} config={config} />
|
|
742
|
-
<SocialFooter config={config} hidden={data.hideFooter} projectSlug={projectSlug ?? undefined} />
|
|
743
|
-
</article>
|
|
744
|
-
</ApiPageWrapper></>
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// MDX content for non-API pages (extracted for conditional ViewWrapper wrapping)
|
|
749
|
-
const mdxContent = (
|
|
750
|
-
<StepSlugProvider entries={stepEntries}>
|
|
751
|
-
<MDXRemote
|
|
752
|
-
source={content}
|
|
753
|
-
components={AllComponentsWithInline}
|
|
754
|
-
options={{
|
|
755
|
-
// Keep expression props (e.g. cols={2}) compatible under next-mdx-remote v6.
|
|
756
|
-
...mdxSecurityOptions,
|
|
757
|
-
mdxOptions: {
|
|
758
|
-
remarkPlugins: [remarkGfm, [remarkVisibility, { audience: 'humans' }], ...getTypographyRemarkPlugins(config), ...getLatexRemarkPlugins(config)],
|
|
759
|
-
rehypePlugins: [rehypeNoZoomToData, rehypeClassToClassName, rehypeCodeMeta, createShikiRehypePlugin(highlighter, config), rehypeRestoreDataTitle, ...getLatexRehypePlugins(config), rehypeSlug],
|
|
760
|
-
recmaPlugins: [recmaCompoundComponents],
|
|
761
|
-
},
|
|
762
|
-
}}
|
|
763
|
-
/>
|
|
764
|
-
</StepSlugProvider>
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
// Shared article content for non-API pages
|
|
768
|
-
const articleContent = (
|
|
769
|
-
<article className="px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
|
|
770
|
-
{/* Breadcrumb */}
|
|
771
|
-
<Breadcrumb slug={slug} config={config} />
|
|
772
|
-
|
|
773
|
-
{/* Page Header */}
|
|
774
|
-
{data.title && (
|
|
775
|
-
<header className="mb-6 sm:mb-10">
|
|
776
|
-
<div className="flex items-center gap-3">
|
|
777
|
-
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-theme-text-primary tracking-tight">
|
|
778
|
-
{data.title}
|
|
779
|
-
</h1>
|
|
780
|
-
{rssIcon}
|
|
781
|
-
{hasAiActions && <div className="ml-auto flex-shrink-0 hidden sm:block"><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
782
|
-
</div>
|
|
783
|
-
{data.description && (
|
|
784
|
-
<p className="text-base sm:text-lg text-theme-text-secondary leading-relaxed mt-2 sm:mt-3">
|
|
785
|
-
{data.description}
|
|
786
|
-
</p>
|
|
787
|
-
)}
|
|
788
|
-
{hasAiActions && <div className="mt-3 sm:hidden" style={{ paddingLeft: 0 }}><AIActionsMenu options={contextualOptions} projectName={config.name} /></div>}
|
|
789
|
-
</header>
|
|
790
|
-
)}
|
|
791
|
-
|
|
792
|
-
{/* Content */}
|
|
793
|
-
<div className={proseClasses}>
|
|
794
|
-
<ImagePriorityProvider>
|
|
795
|
-
{hasView ? <ViewWrapper>{mdxContent}</ViewWrapper> : mdxContent}
|
|
796
|
-
</ImagePriorityProvider>
|
|
797
|
-
|
|
798
|
-
{/* Previous/Next Navigation - inside prose to match content width */}
|
|
799
|
-
<PageNavigation currentSlug={slug.join('/')} config={config} isWideMode={isWideMode || hasPanel} />
|
|
800
|
-
</div>
|
|
801
|
-
<SocialFooter config={config} hidden={data.hideFooter} projectSlug={projectSlug ?? undefined} />
|
|
802
|
-
</article>
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
// Pages with Panel component: use PanelWrapper to move Panel content to sidebar
|
|
806
|
-
if (hasPanel) {
|
|
807
|
-
return <>{jsonLdScript}<PanelWrapper>{articleContent}</PanelWrapper></>;
|
|
808
|
-
}
|
|
117
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
118
|
+
const input = await resolveInput(params);
|
|
119
|
+
return buildDocMetadata(input);
|
|
120
|
+
}
|
|
809
121
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
return (
|
|
813
|
-
<>
|
|
814
|
-
{jsonLdScript}
|
|
815
|
-
<PageColumns toc={<TableOfContents content={content} />} isWideMode={isWideMode}>
|
|
816
|
-
{articleContent}
|
|
817
|
-
</PageColumns>
|
|
818
|
-
</>
|
|
819
|
-
);
|
|
122
|
+
export default async function DocPage({ params }: PageProps) {
|
|
123
|
+
const input = await resolveInput(params);
|
|
124
|
+
return renderDocPage(input);
|
|
820
125
|
}
|