jamdesk 1.1.20 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/unit/openapi-server-variable-description.test.d.ts +2 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.d.ts.map +1 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.js +55 -0
- package/dist/__tests__/unit/openapi-server-variable-description.test.js.map +1 -0
- package/package.json +4 -2
- package/scripts/patch-openapi-schemas.js +91 -0
- package/vendored/app/[[...slug]]/page.tsx +14 -14
- package/vendored/components/search/SearchModal.tsx +14 -13
- package/vendored/lib/analytics-client.ts +7 -26
- package/vendored/lib/project-slug-context.tsx +4 -13
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-server-variable-description.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/openapi-server-variable-description.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression guard for the @apidevtools/openapi-schemas@2.1.0 typo in
|
|
3
|
+
* the OpenAPI 3.1 server-variable schema (`descriptions` instead of
|
|
4
|
+
* `description`). The patcher at scripts/patch-openapi-schemas.js must
|
|
5
|
+
* have fixed the installed schema by the time this test runs.
|
|
6
|
+
*
|
|
7
|
+
* This test uses the REAL SwaggerParser (not a mock) so it actually
|
|
8
|
+
* exercises the bundled schema. If the patcher didn't run, or a future
|
|
9
|
+
* upstream release reintroduces the typo, this test fails.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from 'vitest';
|
|
12
|
+
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
13
|
+
describe('OpenAPI 3.1 server variable description (regression)', () => {
|
|
14
|
+
it('accepts `description` on a server variable (spec §4.7.10.1)', async () => {
|
|
15
|
+
const spec = {
|
|
16
|
+
openapi: '3.1.0',
|
|
17
|
+
info: { title: 'Regression Test', version: '1.0.0' },
|
|
18
|
+
servers: [
|
|
19
|
+
{
|
|
20
|
+
url: 'https://{project}.jamdesk.app',
|
|
21
|
+
description: 'Production (subdomain)',
|
|
22
|
+
variables: {
|
|
23
|
+
project: {
|
|
24
|
+
default: 'your-project',
|
|
25
|
+
description: 'Your Jamdesk project slug',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
paths: {},
|
|
31
|
+
};
|
|
32
|
+
// Should not throw. If the schema still has `descriptions` (typo), SwaggerParser
|
|
33
|
+
// rejects `description` as an unevaluated property.
|
|
34
|
+
await SwaggerParser.validate(spec);
|
|
35
|
+
});
|
|
36
|
+
it('accepts a server variable without description (sanity check)', async () => {
|
|
37
|
+
const spec = {
|
|
38
|
+
openapi: '3.1.0',
|
|
39
|
+
info: { title: 'Regression Test', version: '1.0.0' },
|
|
40
|
+
servers: [
|
|
41
|
+
{
|
|
42
|
+
url: 'https://{project}.jamdesk.app',
|
|
43
|
+
variables: {
|
|
44
|
+
project: {
|
|
45
|
+
default: 'your-project',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
paths: {},
|
|
51
|
+
};
|
|
52
|
+
await SwaggerParser.validate(spec);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=openapi-server-variable-description.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-server-variable-description.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/openapi-server-variable-description.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,aAAa,MAAM,6BAA6B,CAAC;AAExD,QAAQ,CAAC,sDAAsD,EAAE,GAAG,EAAE;IACpE,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,IAAI,GAAG;YACX,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE;YACpD,OAAO,EAAE;gBACP;oBACE,GAAG,EAAE,+BAA+B;oBACpC,WAAW,EAAE,wBAAwB;oBACrC,SAAS,EAAE;wBACT,OAAO,EAAE;4BACP,OAAO,EAAE,cAAc;4BACvB,WAAW,EAAE,2BAA2B;yBACzC;qBACF;iBACF;aACF;YACD,KAAK,EAAE,EAAE;SACV,CAAC;QACF,iFAAiF;QACjF,oDAAoD;QACpD,MAAM,aAAa,CAAC,QAAQ,CAAC,IAAa,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,IAAI,GAAG;YACX,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE;YACpD,OAAO,EAAE;gBACP;oBACE,GAAG,EAAE,+BAA+B;oBACpC,SAAS,EAAE;wBACT,OAAO,EAAE;4BACP,OAAO,EAAE,cAAc;yBACxB;qBACF;iBACF;aACF;YACD,KAAK,EAAE,EAAE;SACV,CAAC;QACF,MAAM,aAAa,CAAC,QAAQ,CAAC,IAAa,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"files": [
|
|
60
60
|
"bin/",
|
|
61
61
|
"dist/",
|
|
62
|
+
"scripts/patch-openapi-schemas.js",
|
|
62
63
|
"templates/",
|
|
63
64
|
"vendored/components/",
|
|
64
65
|
"vendored/contexts/",
|
|
@@ -91,7 +92,8 @@
|
|
|
91
92
|
"test": "vitest",
|
|
92
93
|
"test:local": "node scripts/test-local.js",
|
|
93
94
|
"lint": "eslint src/",
|
|
94
|
-
"dev": "tsc --watch"
|
|
95
|
+
"dev": "tsc --watch",
|
|
96
|
+
"postinstall": "node scripts/patch-openapi-schemas.js"
|
|
95
97
|
},
|
|
96
98
|
"dependencies": {
|
|
97
99
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Patch a typo in @apidevtools/openapi-schemas@2.1.0.
|
|
4
|
+
*
|
|
5
|
+
* The bundled OpenAPI 3.1 meta-schema defines `server-variable.properties`
|
|
6
|
+
* as `{ enum, default, descriptions }` — but the OpenAPI 3.1 spec says the
|
|
7
|
+
* third property is `description` (singular). Combined with
|
|
8
|
+
* `unevaluatedProperties: false` on the same definition, any spec-valid
|
|
9
|
+
* `description` field on a server variable is rejected with:
|
|
10
|
+
*
|
|
11
|
+
* #/servers/0/variables/<name> must NOT have unevaluated properties
|
|
12
|
+
*
|
|
13
|
+
* We swap the typo at install time so swagger-parser accepts the spec.
|
|
14
|
+
* Idempotent — safe to run multiple times. Deletes itself cleanly once
|
|
15
|
+
* upstream ships a fix:
|
|
16
|
+
*
|
|
17
|
+
* https://github.com/APIDevTools/openapi-schemas
|
|
18
|
+
*
|
|
19
|
+
* This file runs as an npm `postinstall` script in the package root, so
|
|
20
|
+
* `__dirname` is always `<package>/scripts/` and `node_modules` resolves
|
|
21
|
+
* one level up.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
const schemaPath = path.join(
|
|
31
|
+
__dirname,
|
|
32
|
+
'..',
|
|
33
|
+
'node_modules',
|
|
34
|
+
'@apidevtools',
|
|
35
|
+
'openapi-schemas',
|
|
36
|
+
'schemas',
|
|
37
|
+
'v3.1',
|
|
38
|
+
'schema.json'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// If the dep isn't installed (e.g., production install with --omit=dev and
|
|
42
|
+
// swagger-parser moved to devDependencies), silently no-op.
|
|
43
|
+
if (!fs.existsSync(schemaPath)) {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let schema;
|
|
48
|
+
try {
|
|
49
|
+
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(
|
|
52
|
+
'[jamdesk] patch-openapi-schemas: failed to parse schema, skipping:',
|
|
53
|
+
err.message
|
|
54
|
+
);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defs = schema && schema.$defs;
|
|
59
|
+
const serverVariable = defs && defs['server-variable'];
|
|
60
|
+
const props = serverVariable && serverVariable.properties;
|
|
61
|
+
|
|
62
|
+
if (!props) {
|
|
63
|
+
// Schema shape changed upstream — bail out quietly.
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (props.description) {
|
|
68
|
+
// Already patched (or upstream fixed it). Either way, no work to do.
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!props.descriptions) {
|
|
73
|
+
// No typo to fix. Quietly no-op.
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
props.description = props.descriptions;
|
|
78
|
+
delete props.descriptions;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.warn(
|
|
84
|
+
'[jamdesk] patch-openapi-schemas: failed to write patch, skipping:',
|
|
85
|
+
err.message
|
|
86
|
+
);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
console.log(
|
|
90
|
+
'[jamdesk] patched openapi-schemas 3.1 server-variable typo (descriptions → description)'
|
|
91
|
+
);
|
|
@@ -47,7 +47,8 @@ import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
|
47
47
|
import fs from 'fs';
|
|
48
48
|
import path from 'path';
|
|
49
49
|
import matter from 'gray-matter';
|
|
50
|
-
import {
|
|
50
|
+
import { getContentDir } from '@/lib/docs';
|
|
51
|
+
import type { DocsConfig } from '@/lib/docs-types';
|
|
51
52
|
import { buildSeoMetadata, generateAutoDescription, buildSiteTitle } from '@/lib/seo';
|
|
52
53
|
import { buildJsonLd } from '@/lib/json-ld';
|
|
53
54
|
import {
|
|
@@ -182,7 +183,7 @@ function getAllDocPaths(): string[] {
|
|
|
182
183
|
/**
|
|
183
184
|
* Find the first page in navigation (used to resolve empty root slug in place).
|
|
184
185
|
*/
|
|
185
|
-
function findFirstPage(config:
|
|
186
|
+
function findFirstPage(config: DocsConfig): string {
|
|
186
187
|
const navigation = config.navigation;
|
|
187
188
|
|
|
188
189
|
// Helper to extract page path from a page entry
|
|
@@ -339,9 +340,10 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
339
340
|
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
340
341
|
// Empty root → resolve to first page (see DocPage for the full rationale).
|
|
341
342
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
const isRoot = normalizedSlug.length === 0;
|
|
344
|
+
const slug = isRoot
|
|
345
|
+
? pathToSlug(findFirstPage(await loader.getConfig()))
|
|
346
|
+
: normalizedSlug;
|
|
345
347
|
const pagePath = slug.join('/');
|
|
346
348
|
|
|
347
349
|
// Fetch content and config in parallel
|
|
@@ -381,7 +383,7 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
381
383
|
...seoMetadata,
|
|
382
384
|
// Root serves first-page content but canonical points at /{firstPage};
|
|
383
385
|
// noindex as a second dedup signal alongside the canonical tag.
|
|
384
|
-
...(
|
|
386
|
+
...(isRoot && { robots: { index: false, follow: true } }),
|
|
385
387
|
...(data.rss ? {
|
|
386
388
|
alternates: {
|
|
387
389
|
...seoMetadata.alternates,
|
|
@@ -420,15 +422,13 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
420
422
|
const loader = getContentLoader(projectSlug ?? undefined);
|
|
421
423
|
|
|
422
424
|
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
423
|
-
// Empty root
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
// points at /{firstPage} via buildSeoMetadata, and generateMetadata
|
|
427
|
-
// noindexes the root to prevent duplicate indexing.
|
|
425
|
+
// Empty root renders the first page in place rather than 307'ing — Next's
|
|
426
|
+
// redirect() emits cache-control: private, blocking CDN caching. Canonical
|
|
427
|
+
// + noindex in generateMetadata prevent duplicate indexing.
|
|
428
428
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
429
|
-
const slug = normalizedSlug.length
|
|
430
|
-
?
|
|
431
|
-
:
|
|
429
|
+
const slug = normalizedSlug.length === 0
|
|
430
|
+
? pathToSlug(findFirstPage(await loader.getConfig()))
|
|
431
|
+
: normalizedSlug;
|
|
432
432
|
const pagePath = slug.join('/');
|
|
433
433
|
const [fileContents, config] = await Promise.all([
|
|
434
434
|
loader.getContent(pagePath).catch(() => null),
|
|
@@ -10,9 +10,6 @@ import { trackSearch } from '@/lib/analytics-client';
|
|
|
10
10
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
11
11
|
import { useProjectSlug } from '@/lib/project-slug-context';
|
|
12
12
|
|
|
13
|
-
// Build-time env var for client-side configuration
|
|
14
|
-
const projectSlug = process.env.NEXT_PUBLIC_PROJECT_SLUG || 'default';
|
|
15
|
-
|
|
16
13
|
interface SearchResult {
|
|
17
14
|
id: string;
|
|
18
15
|
title: string;
|
|
@@ -104,7 +101,7 @@ function NoResultsState({ query, onSuggestionClick }: { query: string; onSuggest
|
|
|
104
101
|
|
|
105
102
|
export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: SearchModalProps) {
|
|
106
103
|
const linkPrefix = useLinkPrefix();
|
|
107
|
-
const
|
|
104
|
+
const projectSlug = useProjectSlug();
|
|
108
105
|
const [query, setQuery] = useState('');
|
|
109
106
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
110
107
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -144,7 +141,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
144
141
|
useEffect(() => {
|
|
145
142
|
if (!isOpen && lastSearchRef.current && !hasTrackedRef.current) {
|
|
146
143
|
// Modal closed with an untracked search - track it now
|
|
147
|
-
trackSearch(
|
|
144
|
+
trackSearch(projectSlug, {
|
|
148
145
|
type: 'search_query',
|
|
149
146
|
query: lastSearchRef.current.query,
|
|
150
147
|
resultsCount: lastSearchRef.current.resultsCount,
|
|
@@ -155,15 +152,19 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
155
152
|
hasTrackedRef.current = false;
|
|
156
153
|
lastSearchRef.current = null;
|
|
157
154
|
}
|
|
158
|
-
}, [isOpen,
|
|
155
|
+
}, [isOpen, projectSlug]);
|
|
159
156
|
|
|
160
|
-
// Load
|
|
157
|
+
// Load recent searches (depends on projectSlug from context, kept separate
|
|
158
|
+
// so search-data init doesn't re-fetch when the slug would hypothetically change)
|
|
161
159
|
useEffect(() => {
|
|
162
160
|
if (isOpen) {
|
|
163
|
-
// Load recent searches
|
|
164
161
|
setRecentSearches(getRecentSearches(projectSlug));
|
|
162
|
+
}
|
|
163
|
+
}, [isOpen, projectSlug]);
|
|
165
164
|
|
|
166
|
-
|
|
165
|
+
// Load search data on mount
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (isOpen) {
|
|
167
168
|
setIsInitializing(true);
|
|
168
169
|
setInitError(null);
|
|
169
170
|
|
|
@@ -265,7 +266,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
265
266
|
else if (query.trim() && results.length === 0 && !isSearching && e.key === 'Enter') {
|
|
266
267
|
e.preventDefault();
|
|
267
268
|
// Track this as a committed search with zero results
|
|
268
|
-
trackSearch(
|
|
269
|
+
trackSearch(projectSlug, {
|
|
269
270
|
type: 'search_query',
|
|
270
271
|
query: query.trim(),
|
|
271
272
|
resultsCount: 0,
|
|
@@ -303,19 +304,19 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
|
|
|
303
304
|
|
|
304
305
|
document.addEventListener('keydown', handleKeyDown);
|
|
305
306
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
306
|
-
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate,
|
|
307
|
+
}, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate, projectSlug]);
|
|
307
308
|
|
|
308
309
|
const handleResultClick = (result: SearchResult, index: number) => {
|
|
309
310
|
if (query.trim()) {
|
|
310
311
|
// Track search_query event (the search itself)
|
|
311
|
-
trackSearch(
|
|
312
|
+
trackSearch(projectSlug, {
|
|
312
313
|
type: 'search_query',
|
|
313
314
|
query: query.trim(),
|
|
314
315
|
resultsCount: results.length,
|
|
315
316
|
});
|
|
316
317
|
|
|
317
318
|
// Track search_click event (the result they clicked)
|
|
318
|
-
trackSearch(
|
|
319
|
+
trackSearch(projectSlug, {
|
|
319
320
|
type: 'search_click',
|
|
320
321
|
query: query.trim(),
|
|
321
322
|
resultsCount: results.length,
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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.
|
|
1
|
+
// Endpoint is /_jd/search-ev (not /api/search-ev) so sites fronted by the
|
|
2
|
+
// jamdesk.com Cloudflare Worker — which only proxies /_jd/* — reach the ISR
|
|
3
|
+
// app. Middleware rewrites /_jd/search-ev to /api/search-ev.
|
|
7
4
|
|
|
8
5
|
const ANALYTICS_URL = '/_jd/search-ev';
|
|
9
6
|
|
|
10
|
-
// Reuse the same session ID as the pageview tracking script (localStorage with 30-min inactivity timeout).
|
|
11
|
-
// Every call refreshes the timestamp, so active users keep the same session.
|
|
12
7
|
const SESSION_TIMEOUT_MS = 1800000; // 30 minutes
|
|
13
8
|
|
|
14
9
|
function getSessionId(): string {
|
|
@@ -29,7 +24,6 @@ function getSessionId(): string {
|
|
|
29
24
|
return sessionId;
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
// Use separate event types to avoid double-counting
|
|
33
27
|
interface SearchQueryEvent {
|
|
34
28
|
type: 'search_query';
|
|
35
29
|
query: string;
|
|
@@ -50,20 +44,11 @@ interface SearchClickEvent {
|
|
|
50
44
|
type SearchEvent = SearchQueryEvent | SearchClickEvent;
|
|
51
45
|
|
|
52
46
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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.
|
|
47
|
+
* Fire-and-forget. The slug must be supplied at runtime (via `useProjectSlug()`)
|
|
48
|
+
* because `NEXT_PUBLIC_*` vars are baked per-build and the ISR app is multi-tenant.
|
|
61
49
|
*/
|
|
62
50
|
export function trackSearch(projectSlug: string, event: SearchEvent): void {
|
|
63
|
-
// Don't track in development
|
|
64
51
|
if (process.env.NODE_ENV === 'development') return;
|
|
65
|
-
|
|
66
|
-
// Empty slug means we couldn't resolve the project (e.g., non-ISR fallback) — no-op
|
|
67
52
|
if (!projectSlug) return;
|
|
68
53
|
|
|
69
54
|
try {
|
|
@@ -79,10 +64,6 @@ export function trackSearch(projectSlug: string, event: SearchEvent): void {
|
|
|
79
64
|
headers: { 'Content-Type': 'application/json' },
|
|
80
65
|
body: payload,
|
|
81
66
|
keepalive: true,
|
|
82
|
-
}).catch(() => {
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
} catch {
|
|
86
|
-
// Silent fail - analytics should never break the app
|
|
87
|
-
}
|
|
67
|
+
}).catch(() => {});
|
|
68
|
+
} catch {}
|
|
88
69
|
}
|
|
@@ -2,16 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, type ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
*/
|
|
5
|
+
// `NEXT_PUBLIC_PROJECT_SLUG` is inlined at build time and unset in the
|
|
6
|
+
// multi-tenant ISR deployment, so the slug has to flow through context.
|
|
7
|
+
// `layout.tsx` seeds the value from the `x-project-slug` header set by
|
|
8
|
+
// middleware. Empty string means "unknown" — consumers should no-op.
|
|
15
9
|
const ProjectSlugContext = createContext<string>('');
|
|
16
10
|
|
|
17
11
|
export function ProjectSlugProvider({
|
|
@@ -28,9 +22,6 @@ export function ProjectSlugProvider({
|
|
|
28
22
|
);
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
* Hook returning the resolved project slug, or '' when unavailable.
|
|
33
|
-
*/
|
|
34
25
|
export function useProjectSlug(): string {
|
|
35
26
|
return useContext(ProjectSlugContext);
|
|
36
27
|
}
|