jamdesk 1.1.19 → 1.1.21
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/openapi-server-variable-description.test.d.ts +2 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.d.ts.map +1 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.js +55 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.js.map +1 -0
- package/dist/__tests__/unit/openapi.test.js +20 -5
- package/dist/__tests__/unit/openapi.test.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +15 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/lib/deps.js +6 -6
- package/dist/lib/deps.js.map +1 -1
- package/package.json +6 -4
- package/scripts/patch-openapi-schemas.js +91 -0
- package/vendored/app/[[...slug]]/page.tsx +24 -39
- package/vendored/app/layout.tsx +16 -7
- package/vendored/components/FontAwesomeLoader.tsx +2 -1
- package/vendored/components/mdx/YouTube.tsx +21 -3
- package/vendored/components/search/SearchModal.tsx +15 -12
- package/vendored/components/snippets/generated/CodeLink.tsx +25 -0
- package/vendored/components/snippets/generated/HeaderAPI.tsx +44 -0
- package/vendored/components/snippets/generated/PlansAvailable.tsx +53 -0
- package/vendored/lib/analytics-client.ts +9 -27
- package/vendored/lib/docs-types.ts +16 -8
- package/vendored/lib/email-templates/build-failure.tsx +3 -0
- package/vendored/lib/extract-highlights.ts +2 -0
- package/vendored/lib/font-awesome.ts +2 -0
- package/vendored/lib/indexnow.ts +3 -1
- package/vendored/lib/isr-build-executor.ts +4 -1
- package/vendored/lib/page-isr-helpers.ts +9 -0
- package/vendored/lib/paths.ts +21 -9
- package/vendored/lib/project-slug-context.tsx +27 -0
- package/vendored/lib/r2-manifest.ts +1 -0
- package/vendored/next.config.js +2 -0
- package/vendored/schema/docs-schema.json +17 -0
- package/vendored/components/snippets/generated/ar__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/es__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/ja__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/ko__SnippetIntro.tsx +0 -43
- package/vendored/postcss.config.js +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { notFound
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
2
|
import { headers } from 'next/headers';
|
|
3
3
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
4
4
|
|
|
@@ -47,7 +47,8 @@ import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
|
47
47
|
import fs from 'fs';
|
|
48
48
|
import path from 'path';
|
|
49
49
|
import matter from 'gray-matter';
|
|
50
|
-
import {
|
|
50
|
+
import { getContentDir } from '@/lib/docs';
|
|
51
|
+
import type { DocsConfig } from '@/lib/docs-types';
|
|
51
52
|
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
52
53
|
import { buildJsonLd } from '@/lib/json-ld';
|
|
53
54
|
import {
|
|
@@ -60,7 +61,7 @@ import {
|
|
|
60
61
|
projectExists,
|
|
61
62
|
type ContentLoader,
|
|
62
63
|
} from '@/lib/content-loader';
|
|
63
|
-
import { getBaseUrl } from '@/lib/page-isr-helpers';
|
|
64
|
+
import { getBaseUrl, pathToSlug } from '@/lib/page-isr-helpers';
|
|
64
65
|
import {
|
|
65
66
|
parseOpenApiFrontmatter,
|
|
66
67
|
getCachedSpec,
|
|
@@ -180,9 +181,9 @@ function getAllDocPaths(): string[] {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
/**
|
|
183
|
-
* Find the first page in navigation (
|
|
184
|
+
* Find the first page in navigation (used to resolve empty root slug in place).
|
|
184
185
|
*/
|
|
185
|
-
function findFirstPage(config:
|
|
186
|
+
function findFirstPage(config: DocsConfig): string {
|
|
186
187
|
const navigation = config.navigation;
|
|
187
188
|
|
|
188
189
|
// Helper to extract page path from a page entry
|
|
@@ -301,7 +302,7 @@ export async function generateStaticParams() {
|
|
|
301
302
|
return !hasUnsupportedPattern;
|
|
302
303
|
});
|
|
303
304
|
|
|
304
|
-
// Include empty slug for root route (
|
|
305
|
+
// Include empty slug for root route (resolves to first page in-place)
|
|
305
306
|
return [
|
|
306
307
|
{ slug: [] },
|
|
307
308
|
...supportedPaths.map((path) => ({
|
|
@@ -336,19 +337,13 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
336
337
|
// Get content loader (ISR: R2, static: filesystem)
|
|
337
338
|
const loader = getContentLoader(projectSlug ?? undefined);
|
|
338
339
|
|
|
339
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true
|
|
340
|
+
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
341
|
+
// Empty root → resolve to first page (see DocPage for the full rationale).
|
|
340
342
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
title: { absolute: `${config.name} - Redirecting...` },
|
|
347
|
-
robots: { index: false }, // Don't index redirect page
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const slug = normalizedSlug;
|
|
343
|
+
const isRoot = normalizedSlug.length === 0;
|
|
344
|
+
const slug = isRoot
|
|
345
|
+
? pathToSlug(findFirstPage(await loader.getConfig()))
|
|
346
|
+
: normalizedSlug;
|
|
352
347
|
const pagePath = slug.join('/');
|
|
353
348
|
|
|
354
349
|
// Fetch content and config in parallel
|
|
@@ -386,6 +381,9 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
386
381
|
title: titleValue,
|
|
387
382
|
description: data.description || '',
|
|
388
383
|
...seoMetadata,
|
|
384
|
+
// Root serves first-page content but canonical points at /{firstPage};
|
|
385
|
+
// noindex as a second dedup signal alongside the canonical tag.
|
|
386
|
+
...(isRoot && { robots: { index: false, follow: true } }),
|
|
389
387
|
...(data.rss ? {
|
|
390
388
|
alternates: {
|
|
391
389
|
...seoMetadata.alternates,
|
|
@@ -423,27 +421,14 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
423
421
|
// Get content loader (ISR: R2, static: filesystem)
|
|
424
422
|
const loader = getContentLoader(projectSlug ?? undefined);
|
|
425
423
|
|
|
426
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const firstPage = findFirstPage(config);
|
|
435
|
-
|
|
436
|
-
// If first page is 'index', render it directly (don't redirect to avoid loop)
|
|
437
|
-
if (firstPage === 'index') {
|
|
438
|
-
slug = ['index'];
|
|
439
|
-
} else {
|
|
440
|
-
// When hostAtDocs=true, redirect to /docs/firstPage; otherwise /firstPage
|
|
441
|
-
const redirectPath = hostAtDocs ? `/docs/${firstPage}` : `/${firstPage}`;
|
|
442
|
-
redirect(redirectPath);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Load content and config in parallel
|
|
424
|
+
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
425
|
+
// Empty root renders the first page in place rather than 307'ing — Next's
|
|
426
|
+
// redirect() emits cache-control: private, blocking CDN caching. Canonical
|
|
427
|
+
// + noindex in generateMetadata prevent duplicate indexing.
|
|
428
|
+
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
429
|
+
const slug = normalizedSlug.length === 0
|
|
430
|
+
? pathToSlug(findFirstPage(await loader.getConfig()))
|
|
431
|
+
: normalizedSlug;
|
|
447
432
|
const pagePath = slug.join('/');
|
|
448
433
|
const [fileContents, config] = await Promise.all([
|
|
449
434
|
loader.getContent(pagePath).catch(() => null),
|
package/vendored/app/layout.tsx
CHANGED
|
@@ -6,7 +6,8 @@ import { ThemeProvider } from '@/components/theme/ThemeProvider';
|
|
|
6
6
|
import { LayoutWrapper } from '@/components/layout/LayoutWrapper';
|
|
7
7
|
import { CodeBlockCopyButton } from '@/components/CodeBlockCopyButton';
|
|
8
8
|
import { HeaderLinkCopy } from '@/components/HeaderLinkCopy';
|
|
9
|
-
import { FontAwesomeLoader
|
|
9
|
+
import { FontAwesomeLoader } from '@/components/FontAwesomeLoader';
|
|
10
|
+
import { FA_CSS_HREF } from '@/lib/font-awesome';
|
|
10
11
|
import { getDocsConfig } from '@/lib/docs';
|
|
11
12
|
import { getDocsConfig as getIsrDocsConfig } from '@/lib/docs-isr';
|
|
12
13
|
import { isIsrMode, getProjectFromRequest, getHostAtDocs } from '@/lib/page-isr-helpers';
|
|
@@ -17,6 +18,7 @@ import path from 'path';
|
|
|
17
18
|
import type { DocsConfig, Favicon, FontConfig } from '@/lib/docs-types';
|
|
18
19
|
import { transformConfigImagePath } from '@/lib/docs-types';
|
|
19
20
|
import { LinkPrefixProvider } from '@/lib/link-prefix-context';
|
|
21
|
+
import { ProjectSlugProvider } from '@/lib/project-slug-context';
|
|
20
22
|
import { getAnalyticsScript } from '@/lib/analytics-script';
|
|
21
23
|
import { buildSiteTitle } from '@/lib/seo';
|
|
22
24
|
import { fetchCustomCss, fetchCustomJs } from '@/lib/r2-content';
|
|
@@ -35,7 +37,7 @@ const jetbrainsMono = JetBrains_Mono({
|
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* Get favicon path from config,
|
|
40
|
+
* Get favicon path from config, routing it through the /_jd/ asset pipeline.
|
|
39
41
|
* Returns undefined when no favicon is configured — no default Jamdesk
|
|
40
42
|
* favicon is injected, so customers without a favicon get no icon.
|
|
41
43
|
*/
|
|
@@ -470,9 +472,14 @@ export default async function RootLayout({
|
|
|
470
472
|
el.remove();
|
|
471
473
|
});
|
|
472
474
|
};
|
|
473
|
-
// Run immediately and observe for new elements
|
|
475
|
+
// Run immediately and observe for new elements once body exists
|
|
474
476
|
removeDevIndicators();
|
|
475
|
-
|
|
477
|
+
var observeBody = function() {
|
|
478
|
+
if (document.body) {
|
|
479
|
+
new MutationObserver(removeDevIndicators).observe(document.body, { childList: true, subtree: true });
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
if (document.body) { observeBody(); } else { document.addEventListener('DOMContentLoaded', observeBody, { once: true }); }
|
|
476
483
|
})();
|
|
477
484
|
`,
|
|
478
485
|
}}
|
|
@@ -516,9 +523,11 @@ export default async function RootLayout({
|
|
|
516
523
|
forcedTheme={appearanceStrict ? (appearanceDefault === 'system' ? undefined : appearanceDefault as 'light' | 'dark') : undefined}
|
|
517
524
|
>
|
|
518
525
|
<LinkPrefixProvider prefix={linkPrefix}>
|
|
519
|
-
<
|
|
520
|
-
{
|
|
521
|
-
|
|
526
|
+
<ProjectSlugProvider slug={resolvedProjectSlug || ''}>
|
|
527
|
+
<LayoutWrapper config={config}>
|
|
528
|
+
{children}
|
|
529
|
+
</LayoutWrapper>
|
|
530
|
+
</ProjectSlugProvider>
|
|
522
531
|
</LinkPrefixProvider>
|
|
523
532
|
{/* Client components for copy buttons after hydration */}
|
|
524
533
|
<CodeBlockCopyButton />
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
+
import { FA_CSS_HREF } from '@/lib/font-awesome';
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export { FA_CSS_HREF };
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Ensures Font Awesome CSS is loaded in the document head.
|
|
@@ -8,13 +8,15 @@ interface YouTubeProps {
|
|
|
8
8
|
title?: string;
|
|
9
9
|
/** Optional: Start time in seconds */
|
|
10
10
|
start?: number;
|
|
11
|
+
/** Optional: Render as a vertical YouTube Short (9:16 aspect ratio) */
|
|
12
|
+
short?: boolean;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* YouTube video embed. Standard videos use lite-youtube-embed (lazy-loaded,
|
|
17
|
+
* iframe only loads on click). Shorts use a direct iframe with 9:16 aspect ratio.
|
|
16
18
|
*/
|
|
17
|
-
export function YouTube({ id, title, start }: YouTubeProps): React.ReactElement | null {
|
|
19
|
+
export function YouTube({ id, title, start, short }: YouTubeProps): React.ReactElement | null {
|
|
18
20
|
if (!id) {
|
|
19
21
|
console.warn('YouTube component requires an "id" prop');
|
|
20
22
|
return null;
|
|
@@ -23,6 +25,22 @@ export function YouTube({ id, title, start }: YouTubeProps): React.ReactElement
|
|
|
23
25
|
const startSeconds = start && start > 0 ? Math.floor(start) : undefined;
|
|
24
26
|
const params = startSeconds ? `rel=0&start=${startSeconds}` : 'rel=0';
|
|
25
27
|
|
|
28
|
+
if (short) {
|
|
29
|
+
const src = `https://www.youtube.com/embed/${id}?${params}`;
|
|
30
|
+
return (
|
|
31
|
+
<div className="my-6 mx-auto max-w-[360px]">
|
|
32
|
+
<iframe
|
|
33
|
+
className="w-full aspect-[9/16] rounded-xl"
|
|
34
|
+
src={src}
|
|
35
|
+
title={title || 'YouTube Short'}
|
|
36
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
37
|
+
allowFullScreen
|
|
38
|
+
loading="lazy"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
return (
|
|
27
45
|
<div className="my-6 [&_lite-youtube]:rounded-xl [&_lite-youtube]:overflow-hidden">
|
|
28
46
|
<YouTubeEmbed
|
|
@@ -8,9 +8,7 @@ import { useBodyScrollLock } from '@/hooks/useBodyScrollLock';
|
|
|
8
8
|
import { getSuggestions } from '@/lib/search-suggestions';
|
|
9
9
|
import { trackSearch } from '@/lib/analytics-client';
|
|
10
10
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
11
|
-
|
|
12
|
-
// Build-time env var for client-side configuration
|
|
13
|
-
const projectSlug = process.env.NEXT_PUBLIC_PROJECT_SLUG || 'default';
|
|
11
|
+
import { useProjectSlug } from '@/lib/project-slug-context';
|
|
14
12
|
|
|
15
13
|
interface SearchResult {
|
|
16
14
|
id: string;
|
|
@@ -103,6 +101,7 @@ function NoResultsState({ query, onSuggestionClick }: { query: string; onSuggest
|
|
|
103
101
|
|
|
104
102
|
export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: SearchModalProps) {
|
|
105
103
|
const linkPrefix = useLinkPrefix();
|
|
104
|
+
const projectSlug = useProjectSlug();
|
|
106
105
|
const [query, setQuery] = useState('');
|
|
107
106
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
108
107
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -142,7 +141,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
142
141
|
useEffect(() => {
|
|
143
142
|
if (!isOpen && lastSearchRef.current && !hasTrackedRef.current) {
|
|
144
143
|
// Modal closed with an untracked search - track it now
|
|
145
|
-
trackSearch({
|
|
144
|
+
trackSearch(projectSlug, {
|
|
146
145
|
type: 'search_query',
|
|
147
146
|
query: lastSearchRef.current.query,
|
|
148
147
|
resultsCount: lastSearchRef.current.resultsCount,
|
|
@@ -153,15 +152,19 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
153
152
|
hasTrackedRef.current = false;
|
|
154
153
|
lastSearchRef.current = null;
|
|
155
154
|
}
|
|
156
|
-
}, [isOpen]);
|
|
155
|
+
}, [isOpen, projectSlug]);
|
|
157
156
|
|
|
158
|
-
// Load
|
|
157
|
+
// Load recent searches (depends on projectSlug from context, kept separate
|
|
158
|
+
// so search-data init doesn't re-fetch when the slug would hypothetically change)
|
|
159
159
|
useEffect(() => {
|
|
160
160
|
if (isOpen) {
|
|
161
|
-
// Load recent searches
|
|
162
161
|
setRecentSearches(getRecentSearches(projectSlug));
|
|
162
|
+
}
|
|
163
|
+
}, [isOpen, projectSlug]);
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
// Load search data on mount
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (isOpen) {
|
|
165
168
|
setIsInitializing(true);
|
|
166
169
|
setInitError(null);
|
|
167
170
|
|
|
@@ -263,7 +266,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
263
266
|
else if (query.trim() && results.length === 0 && !isSearching && e.key === 'Enter') {
|
|
264
267
|
e.preventDefault();
|
|
265
268
|
// Track this as a committed search with zero results
|
|
266
|
-
trackSearch({
|
|
269
|
+
trackSearch(projectSlug, {
|
|
267
270
|
type: 'search_query',
|
|
268
271
|
query: query.trim(),
|
|
269
272
|
resultsCount: 0,
|
|
@@ -301,19 +304,19 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
301
304
|
|
|
302
305
|
document.addEventListener('keydown', handleKeyDown);
|
|
303
306
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
304
|
-
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate]);
|
|
307
|
+
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate, projectSlug]);
|
|
305
308
|
|
|
306
309
|
const handleResultClick = (result: SearchResult, index: number) => {
|
|
307
310
|
if (query.trim()) {
|
|
308
311
|
// Track search_query event (the search itself)
|
|
309
|
-
trackSearch({
|
|
312
|
+
trackSearch(projectSlug, {
|
|
310
313
|
type: 'search_query',
|
|
311
314
|
query: query.trim(),
|
|
312
315
|
resultsCount: results.length,
|
|
313
316
|
});
|
|
314
317
|
|
|
315
318
|
// Track search_click event (the result they clicked)
|
|
316
|
-
trackSearch({
|
|
319
|
+
trackSearch(projectSlug, {
|
|
317
320
|
type: 'search_click',
|
|
318
321
|
query: query.trim(),
|
|
319
322
|
resultsCount: results.length,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const CodeLink = ({ title, href }: any) => {
|
|
18
|
+
return (
|
|
19
|
+
<Card title={`${title}`} href={`${href}`} horizontal icon="code">
|
|
20
|
+
{" "}
|
|
21
|
+
</Card>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default CodeLink;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const HeaderAPI = ({ noProfileKey, profileKeyRequired }: any) => (
|
|
18
|
+
<>
|
|
19
|
+
<ParamField header="Authorization" type="string" required>
|
|
20
|
+
<a href="/apis/overview#authorization">API Key</a> of the Primary Profile.
|
|
21
|
+
<br />
|
|
22
|
+
<br />
|
|
23
|
+
Format: <code>Authorization: Bearer API_KEY</code>
|
|
24
|
+
</ParamField>
|
|
25
|
+
{!noProfileKey &&
|
|
26
|
+
(profileKeyRequired ? (
|
|
27
|
+
<ParamField header="Profile-Key" type="string" required>
|
|
28
|
+
<a href="/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
|
|
29
|
+
<br />
|
|
30
|
+
<br />
|
|
31
|
+
Format: <code>Profile-Key: PROFILE_KEY</code>
|
|
32
|
+
</ParamField>
|
|
33
|
+
) : (
|
|
34
|
+
<ParamField header="Profile-Key" type="string">
|
|
35
|
+
<a href="/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
|
|
36
|
+
<br />
|
|
37
|
+
<br />
|
|
38
|
+
Format: <code>Profile-Key: PROFILE_KEY</code>
|
|
39
|
+
</ParamField>
|
|
40
|
+
))}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export default HeaderAPI;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const PlansAvailable = ({ plans, maxPackRequired }: any) => {
|
|
18
|
+
let displayPlans = plans;
|
|
19
|
+
|
|
20
|
+
if (plans.length === 1) {
|
|
21
|
+
const lowerCasePlan = plans[0].toLowerCase();
|
|
22
|
+
if (lowerCasePlan === "basic") {
|
|
23
|
+
displayPlans = ["Basic", "Premium", "Business", "Enterprise"];
|
|
24
|
+
} else if (lowerCasePlan === "business") {
|
|
25
|
+
displayPlans = ["Business", "Enterprise"];
|
|
26
|
+
} else if (lowerCasePlan === "premium") {
|
|
27
|
+
displayPlans = ["Premium", "Business", "Enterprise"];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
|
|
33
|
+
<Note>
|
|
34
|
+
Available on {displayPlans.length === 1 ? "the " : ""}
|
|
35
|
+
{displayPlans.join(", ").replace(/\b\w/g, (l) => l.toUpperCase())}{" "}
|
|
36
|
+
{displayPlans.length > 1 ? "plans" : "plan"}.
|
|
37
|
+
|
|
38
|
+
{maxPackRequired && (
|
|
39
|
+
|
|
40
|
+
<a href="https://www.acme.com/docs/additional/maxpack"
|
|
41
|
+
className="flex items-center mt-2 cursor-pointer"
|
|
42
|
+
>
|
|
43
|
+
<span className="px-1.5 py-0.5 rounded text-sm" style={{backgroundColor: '#C264B6', color: 'white', fontSize: '12px'}}>
|
|
44
|
+
Max Pack required
|
|
45
|
+
</span>
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
</Note>
|
|
49
|
+
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default PlansAvailable;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Endpoint is /_jd/search-ev (not /api/search-ev) so sites fronted by the
|
|
2
|
+
// jamdesk.com Cloudflare Worker — which only proxies /_jd/* — reach the ISR
|
|
3
|
+
// app. Middleware rewrites /_jd/search-ev to /api/search-ev.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
const ANALYTICS_URL = '/api/search-ev';
|
|
5
|
+
const ANALYTICS_URL = '/_jd/search-ev';
|
|
6
6
|
|
|
7
|
-
// Reuse the same session ID as the pageview tracking script (localStorage with 30-min inactivity timeout).
|
|
8
|
-
// Every call refreshes the timestamp, so active users keep the same session.
|
|
9
7
|
const SESSION_TIMEOUT_MS = 1800000; // 30 minutes
|
|
10
8
|
|
|
11
9
|
function getSessionId(): string {
|
|
@@ -16,7 +14,6 @@ function getSessionId(): string {
|
|
|
16
14
|
const sessionTs = localStorage.getItem('jamdesk_session_ts');
|
|
17
15
|
|
|
18
16
|
if (!sessionId || !sessionTs || (now - parseInt(sessionTs)) >= SESSION_TIMEOUT_MS) {
|
|
19
|
-
// Match the tracking script's format: 16 random bytes as hex + timestamp in base36
|
|
20
17
|
const bytes = new Uint8Array(16);
|
|
21
18
|
crypto.getRandomValues(bytes);
|
|
22
19
|
sessionId = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') +
|
|
@@ -27,7 +24,6 @@ function getSessionId(): string {
|
|
|
27
24
|
return sessionId;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
// Use separate event types to avoid double-counting
|
|
31
27
|
interface SearchQueryEvent {
|
|
32
28
|
type: 'search_query';
|
|
33
29
|
query: string;
|
|
@@ -48,22 +44,14 @@ interface SearchClickEvent {
|
|
|
48
44
|
type SearchEvent = SearchQueryEvent | SearchClickEvent;
|
|
49
45
|
|
|
50
46
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Note: This function returns immediately without waiting for the request to complete.
|
|
54
|
-
* The fetch request runs in the background with `keepalive: true` to survive page navigation.
|
|
55
|
-
* Errors are silently ignored since analytics should never break the user experience.
|
|
47
|
+
* Fire-and-forget. The slug must be supplied at runtime (via `useProjectSlug()`)
|
|
48
|
+
* because `NEXT_PUBLIC_*` vars are baked per-build and the ISR app is multi-tenant.
|
|
56
49
|
*/
|
|
57
|
-
export function trackSearch(event: SearchEvent): void {
|
|
58
|
-
// Don't track in development
|
|
50
|
+
export function trackSearch(projectSlug: string, event: SearchEvent): void {
|
|
59
51
|
if (process.env.NODE_ENV === 'development') return;
|
|
60
|
-
|
|
61
|
-
const projectSlug = process.env.NEXT_PUBLIC_PROJECT_SLUG;
|
|
62
52
|
if (!projectSlug) return;
|
|
63
53
|
|
|
64
54
|
try {
|
|
65
|
-
// Use fetch with keepalive for non-blocking analytics
|
|
66
|
-
// Note: sendBeacon with application/json triggers CORS preflight which can fail silently
|
|
67
55
|
const payload = JSON.stringify({
|
|
68
56
|
projectSlug,
|
|
69
57
|
sessionId: getSessionId(),
|
|
@@ -71,17 +59,11 @@ export function trackSearch(event: SearchEvent): void {
|
|
|
71
59
|
timestamp: Date.now(),
|
|
72
60
|
});
|
|
73
61
|
|
|
74
|
-
// fetch with keepalive allows request to complete even if page navigates
|
|
75
|
-
// This is more reliable than sendBeacon for JSON payloads
|
|
76
62
|
fetch(ANALYTICS_URL, {
|
|
77
63
|
method: 'POST',
|
|
78
64
|
headers: { 'Content-Type': 'application/json' },
|
|
79
65
|
body: payload,
|
|
80
66
|
keepalive: true,
|
|
81
|
-
}).catch(() => {
|
|
82
|
-
|
|
83
|
-
});
|
|
84
|
-
} catch {
|
|
85
|
-
// Silent fail - analytics should never break the app
|
|
86
|
-
}
|
|
67
|
+
}).catch(() => {});
|
|
68
|
+
} catch {}
|
|
87
69
|
}
|
|
@@ -741,6 +741,14 @@ export interface SpellcheckConfig {
|
|
|
741
741
|
ignore?: string[];
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Image optimization configuration
|
|
746
|
+
*/
|
|
747
|
+
export interface ImagesConfig {
|
|
748
|
+
/** Automatically convert PNG/JPG images to WebP format during build (default: false) */
|
|
749
|
+
convertToWebp?: boolean;
|
|
750
|
+
}
|
|
751
|
+
|
|
744
752
|
// =============================================================================
|
|
745
753
|
// MAIN DOCS CONFIG
|
|
746
754
|
// =============================================================================
|
|
@@ -807,6 +815,7 @@ export interface DocsConfig {
|
|
|
807
815
|
analytics?: AnalyticsConfig;
|
|
808
816
|
chat?: ChatConfig;
|
|
809
817
|
spellcheck?: SpellcheckConfig;
|
|
818
|
+
images?: ImagesConfig;
|
|
810
819
|
|
|
811
820
|
// Mintlify compatibility fields (normalized at load time)
|
|
812
821
|
modeToggle?: ModeToggleConfig;
|
|
@@ -872,7 +881,7 @@ export function appendAssetVersion(url: string, assetVersion?: string): string {
|
|
|
872
881
|
}
|
|
873
882
|
|
|
874
883
|
/**
|
|
875
|
-
* Transform
|
|
884
|
+
* Transform a local asset path to /_jd/... for routing through the asset pipeline.
|
|
876
885
|
*
|
|
877
886
|
* Used for config-defined images (favicon, logo) that need the asset prefix.
|
|
878
887
|
* Also normalizes relative paths and strips /public/ prefix (Next.js convention).
|
|
@@ -882,23 +891,22 @@ export function transformConfigImagePath(
|
|
|
882
891
|
assetVersion?: string,
|
|
883
892
|
): string | undefined {
|
|
884
893
|
if (!path) return path;
|
|
885
|
-
//
|
|
886
|
-
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) {
|
|
894
|
+
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//') || path.startsWith('data:')) {
|
|
887
895
|
return path;
|
|
888
896
|
}
|
|
889
|
-
//
|
|
897
|
+
// Relative paths break on nested routes
|
|
890
898
|
let normalized = path.startsWith('/') ? path : `/${path}`;
|
|
891
899
|
// Strip /public/ prefix (Next.js convention: public/logo.png served at /logo.png)
|
|
892
900
|
normalized = normalized.replace(/^\/public\//, '/');
|
|
893
|
-
if (normalized.startsWith('/
|
|
894
|
-
return
|
|
901
|
+
if (normalized.startsWith(ASSET_PREFIX + '/')) {
|
|
902
|
+
return normalized;
|
|
895
903
|
}
|
|
896
|
-
return normalized;
|
|
904
|
+
return appendAssetVersion(ASSET_PREFIX + normalized, assetVersion);
|
|
897
905
|
}
|
|
898
906
|
|
|
899
907
|
/**
|
|
900
908
|
* Normalize logo config to consistent object format.
|
|
901
|
-
* Also transforms
|
|
909
|
+
* Also transforms local paths to /_jd/... via transformConfigImagePath.
|
|
902
910
|
*/
|
|
903
911
|
export function normalizeLogo(logo: Logo | undefined, assetVersion?: string): LogoConfig | null {
|
|
904
912
|
if (!logo) return null;
|
|
@@ -100,8 +100,11 @@ function formatPhase(phase?: string): string {
|
|
|
100
100
|
validate: 'Configuration Validation',
|
|
101
101
|
copy_content: 'Content Preparation',
|
|
102
102
|
nextjs_build: 'Next.js Build',
|
|
103
|
+
optimize_images: 'Image Optimization',
|
|
103
104
|
r2_upload: 'CDN Upload',
|
|
105
|
+
embeddings: 'AI Search Indexing',
|
|
104
106
|
vercel_purge: 'Cache Refresh',
|
|
107
|
+
cleanup: 'Cleanup',
|
|
105
108
|
};
|
|
106
109
|
return phaseLabels[phase] || phase;
|
|
107
110
|
}
|
|
@@ -16,6 +16,7 @@ export interface ExtractedHighlights {
|
|
|
16
16
|
redirectCount: number;
|
|
17
17
|
pageCount: number;
|
|
18
18
|
hasOpenApi: boolean;
|
|
19
|
+
imageOptimization: boolean;
|
|
19
20
|
lastUpdatedAt: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -121,6 +122,7 @@ export function extractConfigHighlights(config: DocsConfig, pageCount: number =
|
|
|
121
122
|
redirectCount: config.redirects?.length ?? 0,
|
|
122
123
|
pageCount,
|
|
123
124
|
hasOpenApi: hasOpenApiConfigured(config),
|
|
125
|
+
imageOptimization: config.images?.convertToWebp === true,
|
|
124
126
|
lastUpdatedAt: Date.now(),
|
|
125
127
|
};
|
|
126
128
|
}
|
package/vendored/lib/indexnow.ts
CHANGED
|
@@ -60,6 +60,9 @@ export async function submitToIndexNow(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const submitted = Math.min(urls.length, MAX_BATCH_SIZE);
|
|
63
|
+
// keyLocation omitted — IndexNow requires it at the root level of the host,
|
|
64
|
+
// and our key verification route is at /api/indexnow/{key} (a subdirectory).
|
|
65
|
+
// When omitted, IndexNow defaults to /{key}.txt which it verifies asynchronously.
|
|
63
66
|
const res = await fetch(INDEXNOW_ENDPOINT, {
|
|
64
67
|
method: 'POST',
|
|
65
68
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
@@ -67,7 +70,6 @@ export async function submitToIndexNow(
|
|
|
67
70
|
body: JSON.stringify({
|
|
68
71
|
host,
|
|
69
72
|
key,
|
|
70
|
-
keyLocation: `https://${host}/api/indexnow/${key}`,
|
|
71
73
|
urlList: urls.slice(0, MAX_BATCH_SIZE),
|
|
72
74
|
}),
|
|
73
75
|
});
|
|
@@ -205,7 +205,10 @@ export const ISR_PHASES = {
|
|
|
205
205
|
copy_content: { label: 'Preparing content...', weight: 10 },
|
|
206
206
|
// ISR mode: nextjs_build is instant (pages compiled on-demand by Vercel)
|
|
207
207
|
nextjs_build: { label: 'Building documentation...', weight: 5 },
|
|
208
|
-
|
|
208
|
+
// Only runs when docs.json `images.convertToWebp` is enabled; the dashboard
|
|
209
|
+
// auto-marks this row skipped when no timing entry is recorded.
|
|
210
|
+
optimize_images: { label: 'Optimizing images...', weight: 5 },
|
|
211
|
+
r2_upload: { label: 'Uploading to CDN...', weight: 35 },
|
|
209
212
|
embeddings: { label: 'Indexing AI search + chat...', weight: 5 },
|
|
210
213
|
vercel_purge: { label: 'Refreshing cache...', weight: 20 },
|
|
211
214
|
cleanup: { label: 'Cleaning up...', weight: 5 },
|
|
@@ -61,6 +61,15 @@ export function normalizeSlugForContent(slug: string[], hostAtDocs: boolean): st
|
|
|
61
61
|
return slug;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Split a page path (e.g. `"get-started/introduction"`) into the slug array
|
|
66
|
+
* Next.js expects. Collapses leading/trailing/repeated slashes so it accepts
|
|
67
|
+
* every shape `findFirstPage()` may return.
|
|
68
|
+
*/
|
|
69
|
+
export function pathToSlug(path: string): string[] {
|
|
70
|
+
return path.split('/').filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
/**
|
|
65
74
|
* Get generateStaticParams result for ISR mode.
|
|
66
75
|
*
|