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.
- package/dist/__tests__/unit/openapi.test.js +20 -5
- package/dist/__tests__/unit/openapi.test.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +15 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/lib/deps.js +6 -6
- package/dist/lib/deps.js.map +1 -1
- package/package.json +3 -3
- package/vendored/app/[[...slug]]/page.tsx +22 -37
- package/vendored/app/layout.tsx +16 -7
- package/vendored/components/FontAwesomeLoader.tsx +2 -1
- package/vendored/components/mdx/YouTube.tsx +21 -3
- package/vendored/components/search/SearchModal.tsx +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 +3 -1
- package/vendored/lib/isr-build-executor.ts +4 -1
- package/vendored/lib/page-isr-helpers.ts +9 -0
- package/vendored/lib/paths.ts +21 -9
- package/vendored/lib/project-slug-context.tsx +36 -0
- package/vendored/lib/r2-manifest.ts +1 -0
- package/vendored/next.config.js +2 -0
- package/vendored/schema/docs-schema.json +17 -0
- package/vendored/components/snippets/generated/ar__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/es__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/ja__SnippetIntro.tsx +0 -43
- package/vendored/components/snippets/generated/ko__SnippetIntro.tsx +0 -43
- package/vendored/postcss.config.js +0 -6
|
@@ -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
|
}
|
package/vendored/lib/indexnow.ts
CHANGED
|
@@ -60,6 +60,9 @@ export async function submitToIndexNow(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const submitted = Math.min(urls.length, MAX_BATCH_SIZE);
|
|
63
|
+
// keyLocation omitted — IndexNow requires it at the root level of the host,
|
|
64
|
+
// and our key verification route is at /api/indexnow/{key} (a subdirectory).
|
|
65
|
+
// When omitted, IndexNow defaults to /{key}.txt which it verifies asynchronously.
|
|
63
66
|
const res = await fetch(INDEXNOW_ENDPOINT, {
|
|
64
67
|
method: 'POST',
|
|
65
68
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
@@ -67,7 +70,6 @@ export async function submitToIndexNow(
|
|
|
67
70
|
body: JSON.stringify({
|
|
68
71
|
host,
|
|
69
72
|
key,
|
|
70
|
-
keyLocation: `https://${host}/api/indexnow/${key}`,
|
|
71
73
|
urlList: urls.slice(0, MAX_BATCH_SIZE),
|
|
72
74
|
}),
|
|
73
75
|
});
|
|
@@ -205,7 +205,10 @@ export const ISR_PHASES = {
|
|
|
205
205
|
copy_content: { label: 'Preparing content...', weight: 10 },
|
|
206
206
|
// ISR mode: nextjs_build is instant (pages compiled on-demand by Vercel)
|
|
207
207
|
nextjs_build: { label: 'Building documentation...', weight: 5 },
|
|
208
|
-
|
|
208
|
+
// Only runs when docs.json `images.convertToWebp` is enabled; the dashboard
|
|
209
|
+
// auto-marks this row skipped when no timing entry is recorded.
|
|
210
|
+
optimize_images: { label: 'Optimizing images...', weight: 5 },
|
|
211
|
+
r2_upload: { label: 'Uploading to CDN...', weight: 35 },
|
|
209
212
|
embeddings: { label: 'Indexing AI search + chat...', weight: 5 },
|
|
210
213
|
vercel_purge: { label: 'Refreshing cache...', weight: 20 },
|
|
211
214
|
cleanup: { label: 'Cleaning up...', weight: 5 },
|
|
@@ -61,6 +61,15 @@ export function normalizeSlugForContent(slug: string[], hostAtDocs: boolean): st
|
|
|
61
61
|
return slug;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Split a page path (e.g. `"get-started/introduction"`) into the slug array
|
|
66
|
+
* Next.js expects. Collapses leading/trailing/repeated slashes so it accepts
|
|
67
|
+
* every shape `findFirstPage()` may return.
|
|
68
|
+
*/
|
|
69
|
+
export function pathToSlug(path: string): string[] {
|
|
70
|
+
return path.split('/').filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
/**
|
|
65
74
|
* Get generateStaticParams result for ISR mode.
|
|
66
75
|
*
|
package/vendored/lib/paths.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
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
|
+
}
|
package/vendored/next.config.js
CHANGED
|
@@ -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;
|