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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/unit/openapi.test.js +20 -5
  3. package/dist/__tests__/unit/openapi.test.js.map +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +3 -1
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/dev.d.ts.map +1 -1
  8. package/dist/commands/dev.js +19 -6
  9. package/dist/commands/dev.js.map +1 -1
  10. package/dist/commands/openapi-check.d.ts +0 -1
  11. package/dist/commands/openapi-check.d.ts.map +1 -1
  12. package/dist/commands/openapi-check.js +0 -1
  13. package/dist/commands/openapi-check.js.map +1 -1
  14. package/dist/lib/deps.js +6 -6
  15. package/dist/lib/deps.js.map +1 -1
  16. package/package.json +3 -3
  17. package/vendored/app/[[...slug]]/page.tsx +23 -39
  18. package/vendored/app/layout.tsx +16 -7
  19. package/vendored/components/FontAwesomeLoader.tsx +2 -1
  20. package/vendored/components/errors/NotFoundContent.tsx +1 -1
  21. package/vendored/components/mdx/OpenApiEndpoint.tsx +8 -8
  22. package/vendored/components/mdx/YouTube.tsx +21 -3
  23. package/vendored/components/search/SearchModal.tsx +8 -6
  24. package/vendored/components/snippets/generated/CodeLink.tsx +25 -0
  25. package/vendored/components/snippets/generated/HeaderAPI.tsx +44 -0
  26. package/vendored/components/snippets/generated/PlansAvailable.tsx +53 -0
  27. package/vendored/lib/analytics-client.ts +14 -13
  28. package/vendored/lib/docs-types.ts +16 -8
  29. package/vendored/lib/email-templates/build-failure.tsx +3 -0
  30. package/vendored/lib/extract-highlights.ts +2 -0
  31. package/vendored/lib/font-awesome.ts +2 -0
  32. package/vendored/lib/indexnow.ts +4 -3
  33. package/vendored/lib/isr-build-executor.ts +4 -1
  34. package/vendored/lib/navigation-resolver.ts +1 -1
  35. package/vendored/lib/page-isr-helpers.ts +9 -0
  36. package/vendored/lib/paths.ts +21 -9
  37. package/vendored/lib/project-slug-context.tsx +36 -0
  38. package/vendored/lib/r2-manifest.ts +1 -0
  39. package/vendored/lib/redirect-compiler.ts +1 -1
  40. package/vendored/lib/remark-extract-exports.ts +1 -2
  41. package/vendored/lib/remark-extract-param-fields.ts +1 -1
  42. package/vendored/next.config.js +2 -0
  43. package/vendored/schema/README.md +3 -25
  44. package/vendored/schema/docs-schema.json +17 -0
  45. package/vendored/scripts/copy-files.cjs +1 -1
  46. package/vendored/scripts/dev-project.cjs +4 -4
  47. package/vendored/components/snippets/generated/ar__SnippetIntro.tsx +0 -43
  48. package/vendored/components/snippets/generated/es__SnippetIntro.tsx +0 -43
  49. package/vendored/components/snippets/generated/ja__SnippetIntro.tsx +0 -43
  50. package/vendored/components/snippets/generated/ko__SnippetIntro.tsx +0 -43
  51. package/vendored/postcss.config.js +0 -6
@@ -1,4 +1,4 @@
1
- import { notFound, redirect } from 'next/navigation';
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 (for root redirect)
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 (Mintlify format with groups inside pages)
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 (will redirect to first page)
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
- // Handle root route - redirect case, no specific metadata needed
344
- if (normalizedSlug.length === 0) {
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
- // This ensures content is looked up correctly regardless of URL structure
429
- let slug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
430
-
431
- // Handle root route - redirect to first page (or render index directly)
432
- // Next.js treats /index as root, so we must handle 'index' specially to avoid infinite redirect
433
- if (slug.length === 0) {
434
- const config = await loader.getConfig();
435
- const firstPage = findFirstPage(config);
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),
@@ -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, FA_CSS_HREF } from '@/components/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, applying asset prefix for /images/ paths.
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
- new MutationObserver(removeDevIndicators).observe(document.body, { childList: true, subtree: true });
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
- <LayoutWrapper config={config}>
520
- {children}
521
- </LayoutWrapper>
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 const FA_CSS_HREF = '/_jd/fonts/fontawesome/css/all.min.css';
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 (Mintlify format with groups inside pages)
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 - Mintlify style
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 like Mintlify
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 - Mintlify style */}
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 like Mintlify */}
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 - Mintlify style with field documentation
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 - Mintlify style with status code and content type on same line */}
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 - Mintlify style field documentation */}
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
- * Mintlify-style response fields list with parent prefix for nested fields
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
- * Lazy-loaded YouTube video embed using lite-youtube-embed.
15
- * Video iframe only loads when user clicks play.
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
- // Firebase Function URL for search analytics
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
- * 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.
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
- const projectSlug = process.env.NEXT_PUBLIC_PROJECT_SLUG;
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 an image path from /images/... to /_jd/images/...
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
- // Skip external URLs
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
- // Ensure absolute path — relative paths break on nested routes
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('/images/')) {
894
- return appendAssetVersion(ASSET_PREFIX + normalized, assetVersion);
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 /images/... paths to /_jd/images/...
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
  }
@@ -0,0 +1,2 @@
1
+ /** Path to Font Awesome CSS — shared between server layout and client loader. */
2
+ export const FA_CSS_HREF = '/_jd/fonts/fontawesome/css/all.min.css';