jamdesk 1.1.18 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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 +19 -6
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/openapi-check.d.ts +0 -1
- package/dist/commands/openapi-check.d.ts.map +1 -1
- package/dist/commands/openapi-check.js +0 -1
- package/dist/commands/openapi-check.js.map +1 -1
- package/dist/lib/deps.js +6 -6
- package/dist/lib/deps.js.map +1 -1
- package/package.json +3 -3
- package/vendored/app/[[...slug]]/page.tsx +23 -39
- package/vendored/app/layout.tsx +16 -7
- package/vendored/components/FontAwesomeLoader.tsx +2 -1
- package/vendored/components/errors/NotFoundContent.tsx +1 -1
- package/vendored/components/mdx/OpenApiEndpoint.tsx +8 -8
- package/vendored/components/mdx/YouTube.tsx +21 -3
- package/vendored/components/search/SearchModal.tsx +8 -6
- 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 +14 -13
- 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 +4 -3
- package/vendored/lib/isr-build-executor.ts +4 -1
- package/vendored/lib/navigation-resolver.ts +1 -1
- package/vendored/lib/page-isr-helpers.ts +9 -0
- package/vendored/lib/paths.ts +21 -9
- package/vendored/lib/project-slug-context.tsx +36 -0
- package/vendored/lib/r2-manifest.ts +1 -0
- package/vendored/lib/redirect-compiler.ts +1 -1
- package/vendored/lib/remark-extract-exports.ts +1 -2
- package/vendored/lib/remark-extract-param-fields.ts +1 -1
- package/vendored/next.config.js +2 -0
- package/vendored/schema/README.md +3 -25
- package/vendored/schema/docs-schema.json +17 -0
- package/vendored/scripts/copy-files.cjs +1 -1
- package/vendored/scripts/dev-project.cjs +4 -4
- 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
|
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
projectExists,
|
|
61
61
|
type ContentLoader,
|
|
62
62
|
} from '@/lib/content-loader';
|
|
63
|
-
import { getBaseUrl } from '@/lib/page-isr-helpers';
|
|
63
|
+
import { getBaseUrl, pathToSlug } from '@/lib/page-isr-helpers';
|
|
64
64
|
import {
|
|
65
65
|
parseOpenApiFrontmatter,
|
|
66
66
|
getCachedSpec,
|
|
@@ -180,7 +180,7 @@ function getAllDocPaths(): string[] {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Find the first page in navigation (
|
|
183
|
+
* Find the first page in navigation (used to resolve empty root slug in place).
|
|
184
184
|
*/
|
|
185
185
|
function findFirstPage(config: ReturnType<typeof getDocsConfig>): string {
|
|
186
186
|
const navigation = config.navigation;
|
|
@@ -256,7 +256,7 @@ function findFirstPage(config: ReturnType<typeof getDocsConfig>): string {
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
// Check pages array (
|
|
259
|
+
// Check pages array (flat format with groups inside pages)
|
|
260
260
|
if (navigation.pages && Array.isArray(navigation.pages)) {
|
|
261
261
|
for (const item of navigation.pages) {
|
|
262
262
|
// Could be a string page
|
|
@@ -286,7 +286,6 @@ export async function generateStaticParams() {
|
|
|
286
286
|
const paths = getAllDocPaths();
|
|
287
287
|
|
|
288
288
|
// Filter out pages with relative MDX imports (not supported by next-mdx-remote)
|
|
289
|
-
// These are test pages in the Mintlify docs that test a Mintlify-specific feature
|
|
290
289
|
const unsupportedPatterns = [
|
|
291
290
|
'deep-relative-test',
|
|
292
291
|
'relative-snippets-test',
|
|
@@ -302,7 +301,7 @@ export async function generateStaticParams() {
|
|
|
302
301
|
return !hasUnsupportedPattern;
|
|
303
302
|
});
|
|
304
303
|
|
|
305
|
-
// Include empty slug for root route (
|
|
304
|
+
// Include empty slug for root route (resolves to first page in-place)
|
|
306
305
|
return [
|
|
307
306
|
{ slug: [] },
|
|
308
307
|
...supportedPaths.map((path) => ({
|
|
@@ -337,19 +336,12 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
337
336
|
// Get content loader (ISR: R2, static: filesystem)
|
|
338
337
|
const loader = getContentLoader(projectSlug ?? undefined);
|
|
339
338
|
|
|
340
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true
|
|
339
|
+
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
340
|
+
// Empty root → resolve to first page (see DocPage for the full rationale).
|
|
341
341
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const config = await loader.getConfig();
|
|
346
|
-
return {
|
|
347
|
-
title: { absolute: `${config.name} - Redirecting...` },
|
|
348
|
-
robots: { index: false }, // Don't index redirect page
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const slug = normalizedSlug;
|
|
342
|
+
const slug = normalizedSlug.length > 0
|
|
343
|
+
? normalizedSlug
|
|
344
|
+
: pathToSlug(findFirstPage(await loader.getConfig()));
|
|
353
345
|
const pagePath = slug.join('/');
|
|
354
346
|
|
|
355
347
|
// Fetch content and config in parallel
|
|
@@ -387,6 +379,9 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
387
379
|
title: titleValue,
|
|
388
380
|
description: data.description || '',
|
|
389
381
|
...seoMetadata,
|
|
382
|
+
// Root serves first-page content but canonical points at /{firstPage};
|
|
383
|
+
// noindex as a second dedup signal alongside the canonical tag.
|
|
384
|
+
...(normalizedSlug.length === 0 && { robots: { index: false, follow: true } }),
|
|
390
385
|
...(data.rss ? {
|
|
391
386
|
alternates: {
|
|
392
387
|
...seoMetadata.alternates,
|
|
@@ -424,27 +419,16 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
424
419
|
// Get content loader (ISR: R2, static: filesystem)
|
|
425
420
|
const loader = getContentLoader(projectSlug ?? undefined);
|
|
426
421
|
|
|
427
|
-
// Normalize slug: strip /docs prefix when hostAtDocs=true
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// If first page is 'index', render it directly (don't redirect to avoid loop)
|
|
438
|
-
if (firstPage === 'index') {
|
|
439
|
-
slug = ['index'];
|
|
440
|
-
} else {
|
|
441
|
-
// When hostAtDocs=true, redirect to /docs/firstPage; otherwise /firstPage
|
|
442
|
-
const redirectPath = hostAtDocs ? `/docs/${firstPage}` : `/${firstPage}`;
|
|
443
|
-
redirect(redirectPath);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Load content and config in parallel
|
|
422
|
+
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
423
|
+
// Empty root request → render the first page in place instead of 307'ing
|
|
424
|
+
// to /{firstPage} (Lighthouse flagged ~1.5s mobile; Next's redirect() emits
|
|
425
|
+
// cache-control: private so CDNs can't absorb it). Canonical URL still
|
|
426
|
+
// points at /{firstPage} via buildSeoMetadata, and generateMetadata
|
|
427
|
+
// noindexes the root to prevent duplicate indexing.
|
|
428
|
+
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
429
|
+
const slug = normalizedSlug.length > 0
|
|
430
|
+
? normalizedSlug
|
|
431
|
+
: pathToSlug(findFirstPage(await loader.getConfig()));
|
|
448
432
|
const pagePath = slug.join('/');
|
|
449
433
|
const [fileContents, config] = await Promise.all([
|
|
450
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.
|
|
@@ -61,7 +61,7 @@ export function NotFoundContent({ config }: NotFoundContentProps) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Check pages array (
|
|
64
|
+
// Check pages array (flat format with groups inside pages)
|
|
65
65
|
if (navigation.pages && Array.isArray(navigation.pages)) {
|
|
66
66
|
for (const item of navigation.pages) {
|
|
67
67
|
// Could be a string page
|
|
@@ -76,7 +76,7 @@ function renderSchemaType(schema: JsonSchema): string {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Render nested schema properties
|
|
79
|
+
* Render nested schema properties as a list of field items
|
|
80
80
|
*/
|
|
81
81
|
function SchemaProperties({
|
|
82
82
|
schema,
|
|
@@ -216,7 +216,7 @@ function BodyFieldItem({
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* Authorization section - shows detailed auth requirements
|
|
219
|
+
* Authorization section - shows detailed auth requirements
|
|
220
220
|
*/
|
|
221
221
|
function AuthorizationSection({ security }: { security: SecurityRequirement[] }) {
|
|
222
222
|
if (security.length === 0) return null;
|
|
@@ -308,9 +308,9 @@ function ParameterSection({
|
|
|
308
308
|
key={`${param.in}-${param.name}`}
|
|
309
309
|
className={`py-5 ${index < parameters.length - 1 ? 'param-divider' : ''}`}
|
|
310
310
|
>
|
|
311
|
-
{/* Parameter name and type badges
|
|
311
|
+
{/* Parameter name and type badges */}
|
|
312
312
|
<div className="flex items-center gap-2 flex-wrap mb-2">
|
|
313
|
-
{/* Parameter name - plain bold text
|
|
313
|
+
{/* Parameter name - plain bold text */}
|
|
314
314
|
<span
|
|
315
315
|
className="text-sm font-bold text-[var(--color-text-primary)]"
|
|
316
316
|
style={{ fontFamily: 'var(--font-mono, ui-monospace, monospace)' }}
|
|
@@ -478,7 +478,7 @@ function getResponseExample(response: ParsedResponse): unknown | null {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
/**
|
|
481
|
-
* Response section
|
|
481
|
+
* Response section with field documentation
|
|
482
482
|
* Only shows response codes that have content (schema or example)
|
|
483
483
|
*/
|
|
484
484
|
function ResponseSection({
|
|
@@ -504,7 +504,7 @@ function ResponseSection({
|
|
|
504
504
|
|
|
505
505
|
return (
|
|
506
506
|
<div className="mt-8">
|
|
507
|
-
{/* Response Header -
|
|
507
|
+
{/* Response Header - status code and content type on same line */}
|
|
508
508
|
<div className="flex items-center justify-between border-b border-[var(--color-border)] pb-3 mb-4">
|
|
509
509
|
<h2 id="response" className="text-base font-semibold text-[var(--color-text-primary)] scroll-mt-20">
|
|
510
510
|
Response
|
|
@@ -550,7 +550,7 @@ function ResponseSection({
|
|
|
550
550
|
</p>
|
|
551
551
|
)}
|
|
552
552
|
|
|
553
|
-
{/* Response Fields -
|
|
553
|
+
{/* Response Fields - structured field documentation */}
|
|
554
554
|
{currentResponse?.content?.['application/json']?.schema && (
|
|
555
555
|
<ResponseFieldsList schema={currentResponse.content['application/json'].schema} />
|
|
556
556
|
)}
|
|
@@ -699,7 +699,7 @@ function FieldConstraints({ schema }: { schema: JsonSchema }) {
|
|
|
699
699
|
}
|
|
700
700
|
|
|
701
701
|
/**
|
|
702
|
-
*
|
|
702
|
+
* Response fields list with parent prefix for nested fields
|
|
703
703
|
* Handles typical API response schemas like { properties: { conference: { type: 'array', items: {...} } } }
|
|
704
704
|
*/
|
|
705
705
|
function ResponseFieldsList({
|
|
@@ -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,6 +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
|
+
import { useProjectSlug } from '@/lib/project-slug-context';
|
|
11
12
|
|
|
12
13
|
// Build-time env var for client-side configuration
|
|
13
14
|
const projectSlug = process.env.NEXT_PUBLIC_PROJECT_SLUG || 'default';
|
|
@@ -103,6 +104,7 @@ function NoResultsState({ query, onSuggestionClick }: { query: string; onSuggest
|
|
|
103
104
|
|
|
104
105
|
export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: SearchModalProps) {
|
|
105
106
|
const linkPrefix = useLinkPrefix();
|
|
107
|
+
const analyticsProjectSlug = useProjectSlug();
|
|
106
108
|
const [query, setQuery] = useState('');
|
|
107
109
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
108
110
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -142,7 +144,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
142
144
|
useEffect(() => {
|
|
143
145
|
if (!isOpen && lastSearchRef.current && !hasTrackedRef.current) {
|
|
144
146
|
// Modal closed with an untracked search - track it now
|
|
145
|
-
trackSearch({
|
|
147
|
+
trackSearch(analyticsProjectSlug, {
|
|
146
148
|
type: 'search_query',
|
|
147
149
|
query: lastSearchRef.current.query,
|
|
148
150
|
resultsCount: lastSearchRef.current.resultsCount,
|
|
@@ -153,7 +155,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
153
155
|
hasTrackedRef.current = false;
|
|
154
156
|
lastSearchRef.current = null;
|
|
155
157
|
}
|
|
156
|
-
}, [isOpen]);
|
|
158
|
+
}, [isOpen, analyticsProjectSlug]);
|
|
157
159
|
|
|
158
160
|
// Load search data and recent searches on mount
|
|
159
161
|
useEffect(() => {
|
|
@@ -263,7 +265,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
263
265
|
else if (query.trim() && results.length === 0 && !isSearching && e.key === 'Enter') {
|
|
264
266
|
e.preventDefault();
|
|
265
267
|
// Track this as a committed search with zero results
|
|
266
|
-
trackSearch({
|
|
268
|
+
trackSearch(analyticsProjectSlug, {
|
|
267
269
|
type: 'search_query',
|
|
268
270
|
query: query.trim(),
|
|
269
271
|
resultsCount: 0,
|
|
@@ -301,19 +303,19 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
301
303
|
|
|
302
304
|
document.addEventListener('keydown', handleKeyDown);
|
|
303
305
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
304
|
-
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate]);
|
|
306
|
+
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate, analyticsProjectSlug]);
|
|
305
307
|
|
|
306
308
|
const handleResultClick = (result: SearchResult, index: number) => {
|
|
307
309
|
if (query.trim()) {
|
|
308
310
|
// Track search_query event (the search itself)
|
|
309
|
-
trackSearch({
|
|
311
|
+
trackSearch(analyticsProjectSlug, {
|
|
310
312
|
type: 'search_query',
|
|
311
313
|
query: query.trim(),
|
|
312
314
|
resultsCount: results.length,
|
|
313
315
|
});
|
|
314
316
|
|
|
315
317
|
// Track search_click event (the result they clicked)
|
|
316
|
-
trackSearch({
|
|
318
|
+
trackSearch(analyticsProjectSlug, {
|
|
317
319
|
type: 'search_click',
|
|
318
320
|
query: query.trim(),
|
|
319
321
|
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,8 +1,11 @@
|
|
|
1
1
|
// Search Analytics Client
|
|
2
|
-
// Tracks search queries and clicks for analytics dashboard
|
|
2
|
+
// Tracks search queries and clicks for analytics dashboard.
|
|
3
|
+
//
|
|
4
|
+
// Endpoint is /_jd/search-ev (not /api/search-ev) so that sites fronted by
|
|
5
|
+
// the jamdesk.com Cloudflare Worker — which only proxies /_jd/* — route the
|
|
6
|
+
// request to the ISR app. Middleware rewrites /_jd/search-ev to /api/search-ev.
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
const ANALYTICS_URL = '/api/search-ev';
|
|
8
|
+
const ANALYTICS_URL = '/_jd/search-ev';
|
|
6
9
|
|
|
7
10
|
// Reuse the same session ID as the pageview tracking script (localStorage with 30-min inactivity timeout).
|
|
8
11
|
// Every call refreshes the timestamp, so active users keep the same session.
|
|
@@ -16,7 +19,6 @@ function getSessionId(): string {
|
|
|
16
19
|
const sessionTs = localStorage.getItem('jamdesk_session_ts');
|
|
17
20
|
|
|
18
21
|
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
22
|
const bytes = new Uint8Array(16);
|
|
21
23
|
crypto.getRandomValues(bytes);
|
|
22
24
|
sessionId = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') +
|
|
@@ -50,20 +52,21 @@ type SearchEvent = SearchQueryEvent | SearchClickEvent;
|
|
|
50
52
|
/**
|
|
51
53
|
* Track a search analytics event.
|
|
52
54
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
55
|
+
* The project slug MUST be supplied at runtime (typically via `useProjectSlug()`
|
|
56
|
+
* from `lib/project-slug-context`). Build-time env vars are not usable because
|
|
57
|
+
* the ISR deployment is multi-tenant and NEXT_PUBLIC_* vars are baked per-build.
|
|
58
|
+
*
|
|
59
|
+
* Fire-and-forget: returns immediately. The fetch runs in the background with
|
|
60
|
+
* `keepalive: true` so it survives page navigation. Errors are silently ignored.
|
|
56
61
|
*/
|
|
57
|
-
export function trackSearch(event: SearchEvent): void {
|
|
62
|
+
export function trackSearch(projectSlug: string, event: SearchEvent): void {
|
|
58
63
|
// Don't track in development
|
|
59
64
|
if (process.env.NODE_ENV === 'development') return;
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
// Empty slug means we couldn't resolve the project (e.g., non-ISR fallback) — no-op
|
|
62
67
|
if (!projectSlug) return;
|
|
63
68
|
|
|
64
69
|
try {
|
|
65
|
-
// Use fetch with keepalive for non-blocking analytics
|
|
66
|
-
// Note: sendBeacon with application/json triggers CORS preflight which can fail silently
|
|
67
70
|
const payload = JSON.stringify({
|
|
68
71
|
projectSlug,
|
|
69
72
|
sessionId: getSessionId(),
|
|
@@ -71,8 +74,6 @@ export function trackSearch(event: SearchEvent): void {
|
|
|
71
74
|
timestamp: Date.now(),
|
|
72
75
|
});
|
|
73
76
|
|
|
74
|
-
// fetch with keepalive allows request to complete even if page navigates
|
|
75
|
-
// This is more reliable than sendBeacon for JSON payloads
|
|
76
77
|
fetch(ANALYTICS_URL, {
|
|
77
78
|
method: 'POST',
|
|
78
79
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -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
|
}
|