jamdesk 1.1.19 → 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 (37) hide show
  1. package/dist/__tests__/unit/openapi.test.js +20 -5
  2. package/dist/__tests__/unit/openapi.test.js.map +1 -1
  3. package/dist/commands/deploy.d.ts.map +1 -1
  4. package/dist/commands/deploy.js +3 -1
  5. package/dist/commands/deploy.js.map +1 -1
  6. package/dist/commands/dev.d.ts.map +1 -1
  7. package/dist/commands/dev.js +15 -2
  8. package/dist/commands/dev.js.map +1 -1
  9. package/dist/lib/deps.js +6 -6
  10. package/dist/lib/deps.js.map +1 -1
  11. package/package.json +3 -3
  12. package/vendored/app/[[...slug]]/page.tsx +22 -37
  13. package/vendored/app/layout.tsx +16 -7
  14. package/vendored/components/FontAwesomeLoader.tsx +2 -1
  15. package/vendored/components/mdx/YouTube.tsx +21 -3
  16. package/vendored/components/search/SearchModal.tsx +8 -6
  17. package/vendored/components/snippets/generated/CodeLink.tsx +25 -0
  18. package/vendored/components/snippets/generated/HeaderAPI.tsx +44 -0
  19. package/vendored/components/snippets/generated/PlansAvailable.tsx +53 -0
  20. package/vendored/lib/analytics-client.ts +14 -13
  21. package/vendored/lib/docs-types.ts +16 -8
  22. package/vendored/lib/email-templates/build-failure.tsx +3 -0
  23. package/vendored/lib/extract-highlights.ts +2 -0
  24. package/vendored/lib/font-awesome.ts +2 -0
  25. package/vendored/lib/indexnow.ts +3 -1
  26. package/vendored/lib/isr-build-executor.ts +4 -1
  27. package/vendored/lib/page-isr-helpers.ts +9 -0
  28. package/vendored/lib/paths.ts +21 -9
  29. package/vendored/lib/project-slug-context.tsx +36 -0
  30. package/vendored/lib/r2-manifest.ts +1 -0
  31. package/vendored/next.config.js +2 -0
  32. package/vendored/schema/docs-schema.json +17 -0
  33. package/vendored/components/snippets/generated/ar__SnippetIntro.tsx +0 -43
  34. package/vendored/components/snippets/generated/es__SnippetIntro.tsx +0 -43
  35. package/vendored/components/snippets/generated/ja__SnippetIntro.tsx +0 -43
  36. package/vendored/components/snippets/generated/ko__SnippetIntro.tsx +0 -43
  37. package/vendored/postcss.config.js +0 -6
@@ -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';
@@ -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
- r2_upload: { label: 'Uploading to CDN...', weight: 40 },
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
  *
@@ -1,20 +1,32 @@
1
1
  /**
2
- * ESM Path Helpers
3
- *
4
- * Since ESM doesn't have __dirname, we create helpers for common paths.
5
- * Import these instead of using __dirname directly.
2
+ * Path Helpers
3
+ *
4
+ * Centralized build-service paths derived from the CJS `__dirname` of this file.
6
5
  */
7
6
 
8
- import { fileURLToPath } from 'url';
9
7
  import { dirname, join } from 'path';
8
+ import { existsSync } from 'fs';
10
9
 
11
- // Get the directory of the current module
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
10
+ /**
11
+ * Resolve the build-service root directory.
12
+ *
13
+ * In source (tsx / vitest): __dirname = build-service/lib → parent is root.
14
+ * In compiled CJS: __dirname = build-service/dist/lib → grandparent is root.
15
+ *
16
+ * We walk up from __dirname until we find the directory containing package.json.
17
+ */
18
+ function findProjectRoot(): string {
19
+ let dir = dirname(__dirname); // one level up from lib/
20
+ if (existsSync(join(dir, 'package.json'))) return dir;
21
+ dir = dirname(dir); // two levels up (from dist/lib/)
22
+ if (existsSync(join(dir, 'package.json'))) return dir;
23
+ // Fallback — should never happen
24
+ return dirname(__dirname);
25
+ }
14
26
 
15
27
  // Export common paths
16
28
  export const LIB_DIR = __dirname;
17
- export const BUILD_SERVICE_DIR = dirname(__dirname);
29
+ export const BUILD_SERVICE_DIR = findProjectRoot();
18
30
  export const PROJECTS_DIR = join(BUILD_SERVICE_DIR, 'projects');
19
31
  export const SCRIPTS_DIR = join(BUILD_SERVICE_DIR, 'scripts');
20
32
  export const PUBLIC_DIR = join(BUILD_SERVICE_DIR, 'public');
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react';
4
+
5
+ /**
6
+ * Context for exposing the resolved project slug to client components.
7
+ *
8
+ * The value is populated in `app/layout.tsx` from the `x-project-slug`
9
+ * request header (set by middleware). Empty string means "unknown" — clients
10
+ * should treat this as a signal to no-op (e.g. skip analytics).
11
+ *
12
+ * Why this exists: `NEXT_PUBLIC_PROJECT_SLUG` is inlined at build time and is
13
+ * unset in the multi-tenant ISR deployment, so client code cannot read it.
14
+ */
15
+ const ProjectSlugContext = createContext<string>('');
16
+
17
+ export function ProjectSlugProvider({
18
+ children,
19
+ slug,
20
+ }: {
21
+ children: ReactNode;
22
+ slug: string;
23
+ }): React.ReactElement {
24
+ return (
25
+ <ProjectSlugContext.Provider value={slug}>
26
+ {children}
27
+ </ProjectSlugContext.Provider>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Hook returning the resolved project slug, or '' when unavailable.
33
+ */
34
+ export function useProjectSlug(): string {
35
+ return useContext(ProjectSlugContext);
36
+ }
@@ -52,6 +52,7 @@ export interface Manifest {
52
52
  projectSlug?: string;
53
53
  files: Record<string, { hash: string; size?: number }>;
54
54
  snippets?: Record<string, { hash: string }>;
55
+ assets?: Record<string, { hash: string }>;
55
56
  configHash?: string;
56
57
  }
57
58
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  /** @type {import('next').NextConfig} */
14
14
  const nextConfig = {
15
+ poweredByHeader: false,
15
16
  // Hide dev indicator (floating icon in bottom-left)
16
17
  devIndicators: false,
17
18
  // Rewrite /_jd/ asset paths to public/ in dev (ISR middleware handles this in production)
@@ -20,6 +21,7 @@ const nextConfig = {
20
21
  { source: '/_jd/images/:path*', destination: '/images/:path*' },
21
22
  { source: '/_jd/videos/:path*', destination: '/videos/:path*' },
22
23
  { source: '/_jd/playground/:path*', destination: '/api/playground/:path*' },
24
+ { source: '/_jd/:path*', destination: '/:path*' },
23
25
  ];
24
26
  },
25
27
  // Allow /_jd/ image paths with ?v= cache-busting query strings in <Image>
@@ -1544,6 +1544,17 @@
1544
1544
  },
1545
1545
  "additionalProperties": false,
1546
1546
  "description": "Configuration for the jamdesk spellcheck command"
1547
+ },
1548
+ "images": {
1549
+ "type": "object",
1550
+ "description": "Image optimization settings for documentation builds",
1551
+ "properties": {
1552
+ "convertToWebp": {
1553
+ "type": "boolean",
1554
+ "description": "Automatically convert PNG and JPG images to WebP format during builds. Reduces image sizes by 60-80% for faster page loads. SVG and GIF files are not converted. Default: false"
1555
+ }
1556
+ },
1557
+ "additionalProperties": false
1547
1558
  }
1548
1559
  },
1549
1560
  "required": [
@@ -1660,6 +1671,9 @@
1660
1671
  },
1661
1672
  "spellcheck": {
1662
1673
  "$ref": "#/anyOf/0/properties/spellcheck"
1674
+ },
1675
+ "images": {
1676
+ "$ref": "#/anyOf/0/properties/images"
1663
1677
  }
1664
1678
  },
1665
1679
  "required": [
@@ -1776,6 +1790,9 @@
1776
1790
  },
1777
1791
  "spellcheck": {
1778
1792
  "$ref": "#/anyOf/0/properties/spellcheck"
1793
+ },
1794
+ "images": {
1795
+ "$ref": "#/anyOf/0/properties/images"
1779
1796
  }
1780
1797
  },
1781
1798
  "required": [
@@ -1,43 +0,0 @@
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
- // Helper component for rendering plain MDX snippets
18
- // Content is from project snippets (controlled source), not user input
19
- const PlainMdxSnippet = ({ content }: { content: string }) => {
20
- const formattedContent = content
21
- .split('\n\n')
22
- .map((paragraph) => {
23
- let html = paragraph.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
24
- html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
25
- html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
26
- return html;
27
- })
28
- .filter(p => p.trim());
29
-
30
- return (
31
- <div className="snippet-content">
32
- {formattedContent.map((p, i) => (
33
- <p key={i} dangerouslySetInnerHTML={{ __html: p }} />
34
- ))}
35
- </div>
36
- );
37
- };
38
-
39
- const SnippetIntro = () => {
40
- return <PlainMdxSnippet content={"أحد المبادئ الأساسية في تطوير البرمجيات هو مبدأ DRY (Don't Repeat\nYourself). وهذا مبدأ ينطبق على التوثيق أيضًا.\nإذا وجدت نفسك تكرر المحتوى نفسه في عدة أماكن، ففكّر في إنشاء مقتطف\nمخصص (snippet) للحفاظ على content الخاص بك متزامنًا."} />;
41
- };
42
-
43
- export default SnippetIntro;