jamdesk 1.1.124 → 1.1.126
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/deps-sync.test.js +19 -12
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
- package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
- package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
- package/dist/__tests__/unit/language-filter.test.js +166 -0
- package/dist/__tests__/unit/language-filter.test.js.map +1 -0
- package/dist/__tests__/unit/verbose-hint.test.d.ts +2 -0
- package/dist/__tests__/unit/verbose-hint.test.d.ts.map +1 -0
- package/dist/__tests__/unit/verbose-hint.test.js +31 -0
- package/dist/__tests__/unit/verbose-hint.test.js.map +1 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/language-filter.d.ts +31 -0
- package/dist/lib/language-filter.d.ts.map +1 -0
- package/dist/lib/language-filter.js +14 -0
- package/dist/lib/language-filter.js.map +1 -0
- package/dist/lib/verbose-hint.d.ts +25 -0
- package/dist/lib/verbose-hint.d.ts.map +1 -0
- package/dist/lib/verbose-hint.js +27 -0
- package/dist/lib/verbose-hint.js.map +1 -0
- package/package.json +1 -1
- package/vendored/components/navigation/PageNavigation.tsx +40 -43
- package/vendored/lib/docs-types.ts +4 -0
- package/vendored/lib/enhance-navigation.ts +10 -1
- package/vendored/lib/navigation-resolver.ts +28 -12
- package/vendored/lib/search.ts +28 -0
- package/vendored/lib/seo.ts +13 -6
- package/vendored/lib/static-artifacts.ts +92 -11
- package/vendored/lib/visibility.ts +197 -0
- package/vendored/schema/docs-schema.json +43 -0
- package/vendored/scripts/build-search-index.cjs +47 -11
- package/vendored/scripts/enhance-navigation.cjs +21 -7
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `-v`/`--verbose` is a global modifier flag consumed by subcommands
|
|
3
|
+
* (validate, dev, broken-links, …) via `program.opts().verbose`, not a
|
|
4
|
+
* standalone action. Invoked alone (`jamdesk -v`), Commander parses
|
|
5
|
+
* verbose=true, finds no command to run, and falls through to generic help —
|
|
6
|
+
* which reads to users as "the flag does nothing".
|
|
7
|
+
*
|
|
8
|
+
* The flag spelling lives here once, in VERBOSE_FLAGS: index.ts builds its
|
|
9
|
+
* Commander option from VERBOSE_OPTION_SPEC and this predicate matches the
|
|
10
|
+
* same tokens, so a rename can't silently desync the option declaration from
|
|
11
|
+
* the standalone-invocation guard.
|
|
12
|
+
*/
|
|
13
|
+
export declare const VERBOSE_FLAGS: string[];
|
|
14
|
+
/** Commander option spec for the verbose flag, passed to `.option()`. */
|
|
15
|
+
export declare const VERBOSE_OPTION_SPEC: string;
|
|
16
|
+
/**
|
|
17
|
+
* True only when there is at least one arg and every arg is a verbose flag —
|
|
18
|
+
* i.e. verbose with no subcommand. Bare `jamdesk` (no args) is false: the user
|
|
19
|
+
* didn't ask for verbose, so plain help is the right response and the hint
|
|
20
|
+
* would be noise.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isVerboseOnlyInvocation(args: string[]): boolean;
|
|
23
|
+
/** Hint shown for a standalone `-v`/`--verbose` (see isVerboseOnlyInvocation). */
|
|
24
|
+
export declare const VERBOSE_HINT = "-v/--verbose adds detail to a command \u2014 e.g. `jamdesk validate -v`. For the version number, use -V or --version.";
|
|
25
|
+
//# sourceMappingURL=verbose-hint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verbose-hint.d.ts","sourceRoot":"","sources":["../../src/lib/verbose-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,UAAsB,CAAC;AAEjD,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAE/D;AAED,kFAAkF;AAClF,eAAO,MAAM,YAAY,0HAC2F,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `-v`/`--verbose` is a global modifier flag consumed by subcommands
|
|
3
|
+
* (validate, dev, broken-links, …) via `program.opts().verbose`, not a
|
|
4
|
+
* standalone action. Invoked alone (`jamdesk -v`), Commander parses
|
|
5
|
+
* verbose=true, finds no command to run, and falls through to generic help —
|
|
6
|
+
* which reads to users as "the flag does nothing".
|
|
7
|
+
*
|
|
8
|
+
* The flag spelling lives here once, in VERBOSE_FLAGS: index.ts builds its
|
|
9
|
+
* Commander option from VERBOSE_OPTION_SPEC and this predicate matches the
|
|
10
|
+
* same tokens, so a rename can't silently desync the option declaration from
|
|
11
|
+
* the standalone-invocation guard.
|
|
12
|
+
*/
|
|
13
|
+
export const VERBOSE_FLAGS = ['-v', '--verbose'];
|
|
14
|
+
/** Commander option spec for the verbose flag, passed to `.option()`. */
|
|
15
|
+
export const VERBOSE_OPTION_SPEC = VERBOSE_FLAGS.join(', ');
|
|
16
|
+
/**
|
|
17
|
+
* True only when there is at least one arg and every arg is a verbose flag —
|
|
18
|
+
* i.e. verbose with no subcommand. Bare `jamdesk` (no args) is false: the user
|
|
19
|
+
* didn't ask for verbose, so plain help is the right response and the hint
|
|
20
|
+
* would be noise.
|
|
21
|
+
*/
|
|
22
|
+
export function isVerboseOnlyInvocation(args) {
|
|
23
|
+
return args.length > 0 && args.every((a) => VERBOSE_FLAGS.includes(a));
|
|
24
|
+
}
|
|
25
|
+
/** Hint shown for a standalone `-v`/`--verbose` (see isVerboseOnlyInvocation). */
|
|
26
|
+
export const VERBOSE_HINT = '-v/--verbose adds detail to a command — e.g. `jamdesk validate -v`. For the version number, use -V or --version.';
|
|
27
|
+
//# sourceMappingURL=verbose-hint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verbose-hint.js","sourceRoot":"","sources":["../../src/lib/verbose-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AAEjD,yEAAyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAc;IACpD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,YAAY,GACvB,kHAAkH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.126",
|
|
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",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import type { DocsConfig, NavigationPage, GroupConfig } from '@/lib/docs-types';
|
|
5
|
+
import type { DocsConfig, NavigationPage, NavigationPageObject, GroupConfig } from '@/lib/docs-types';
|
|
6
6
|
import { normalizeNavPage } from '@/lib/docs-types';
|
|
7
7
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
8
8
|
|
|
@@ -13,89 +13,86 @@ interface PageNavigationProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Extract all pages from navigation structure
|
|
16
|
+
* Extract all visible pages from the navigation structure, in sidebar order.
|
|
17
|
+
*
|
|
18
|
+
* Hidden pages (frontmatter `hidden: true` propagated onto the nav page object
|
|
19
|
+
* by enhance-navigation) and hidden tabs/anchors/groups are skipped so the
|
|
20
|
+
* prev/next pager mirrors the sidebar — it never links into hidden content,
|
|
21
|
+
* matching the sidebar filter in navigation-resolver. `searchable: true` only
|
|
22
|
+
* keeps a hidden node in artifacts; it does NOT bring it back into the pager.
|
|
17
23
|
*/
|
|
18
|
-
function extractAllPages(config: DocsConfig): { path: string; title: string }[] {
|
|
24
|
+
export function extractAllPages(config: DocsConfig): { path: string; title: string }[] {
|
|
19
25
|
const allPages: { path: string; title: string }[] = [];
|
|
20
26
|
const nav = config.navigation;
|
|
21
|
-
|
|
27
|
+
|
|
28
|
+
function pushPage(item: NavigationPage) {
|
|
29
|
+
if (typeof item !== 'string' && (item as NavigationPageObject & { hidden?: boolean }).hidden === true) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { path, title } = normalizeNavPage(item);
|
|
33
|
+
allPages.push({ path, title });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractFromPagesArray(pages: (NavigationPage | GroupConfig)[]) {
|
|
37
|
+
for (const item of pages) {
|
|
38
|
+
if (typeof item === 'string' || 'page' in item) {
|
|
39
|
+
pushPage(item as NavigationPage);
|
|
40
|
+
} else if ('group' in item) {
|
|
41
|
+
extractFromGroup(item as GroupConfig);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
// Helper to extract pages from a group (including nested groups)
|
|
23
47
|
function extractFromGroup(group: GroupConfig) {
|
|
48
|
+
if (group.hidden === true) return; // hidden groups are dropped from nav + pager
|
|
24
49
|
if (group.pages) {
|
|
25
|
-
|
|
26
|
-
if (typeof item === 'string' || 'page' in item) {
|
|
27
|
-
// It's a page
|
|
28
|
-
const { path, title } = normalizeNavPage(item as NavigationPage);
|
|
29
|
-
allPages.push({ path, title });
|
|
30
|
-
} else if ('group' in item) {
|
|
31
|
-
// It's a nested group
|
|
32
|
-
extractFromGroup(item);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
50
|
+
extractFromPagesArray(group.pages as (NavigationPage | GroupConfig)[]);
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
|
-
|
|
53
|
+
|
|
38
54
|
// Extract from anchors
|
|
39
55
|
if (nav.anchors) {
|
|
40
56
|
for (const anchor of nav.anchors) {
|
|
57
|
+
if ((anchor as { hidden?: boolean }).hidden === true) continue;
|
|
41
58
|
if (anchor.groups) {
|
|
42
59
|
for (const group of anchor.groups) {
|
|
43
60
|
extractFromGroup(group);
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
if (anchor.pages) {
|
|
47
|
-
|
|
48
|
-
if (typeof item === 'string' || 'page' in item) {
|
|
49
|
-
const { path, title } = normalizeNavPage(item as NavigationPage);
|
|
50
|
-
allPages.push({ path, title });
|
|
51
|
-
} else if ('group' in item) {
|
|
52
|
-
extractFromGroup(item);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
64
|
+
extractFromPagesArray(anchor.pages as (NavigationPage | GroupConfig)[]);
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
|
-
|
|
68
|
+
|
|
59
69
|
// Extract from tabs
|
|
60
70
|
if (nav.tabs) {
|
|
61
71
|
for (const tab of nav.tabs) {
|
|
72
|
+
if ((tab as { hidden?: boolean }).hidden === true) continue;
|
|
62
73
|
if (tab.groups) {
|
|
63
74
|
for (const group of tab.groups) {
|
|
64
75
|
extractFromGroup(group);
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
if (tab.pages) {
|
|
68
|
-
|
|
69
|
-
if (typeof item === 'string' || 'page' in item) {
|
|
70
|
-
const { path, title } = normalizeNavPage(item as NavigationPage);
|
|
71
|
-
allPages.push({ path, title });
|
|
72
|
-
} else if ('group' in item) {
|
|
73
|
-
extractFromGroup(item);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
79
|
+
extractFromPagesArray(tab.pages as (NavigationPage | GroupConfig)[]);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
|
|
83
|
+
|
|
80
84
|
// Extract from top-level groups
|
|
81
85
|
if (nav.groups) {
|
|
82
86
|
for (const group of nav.groups) {
|
|
83
87
|
extractFromGroup(group);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
// Extract from top-level pages
|
|
88
92
|
if (nav.pages) {
|
|
89
|
-
|
|
90
|
-
if (typeof item === 'string' || 'page' in item) {
|
|
91
|
-
const { path, title } = normalizeNavPage(item as NavigationPage);
|
|
92
|
-
allPages.push({ path, title });
|
|
93
|
-
} else if ('group' in item) {
|
|
94
|
-
extractFromGroup(item);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
93
|
+
extractFromPagesArray(nav.pages as (NavigationPage | GroupConfig)[]);
|
|
97
94
|
}
|
|
98
|
-
|
|
95
|
+
|
|
99
96
|
return allPages;
|
|
100
97
|
}
|
|
101
98
|
|
|
@@ -165,6 +165,8 @@ export interface GroupConfig {
|
|
|
165
165
|
group: string;
|
|
166
166
|
icon?: IconConfig;
|
|
167
167
|
hidden?: boolean;
|
|
168
|
+
/** When true on a hidden group, descendants stay in search/sitemap/AI context. */
|
|
169
|
+
searchable?: boolean;
|
|
168
170
|
root?: string;
|
|
169
171
|
tag?: string;
|
|
170
172
|
expanded?: boolean;
|
|
@@ -202,6 +204,8 @@ export interface TabConfig {
|
|
|
202
204
|
tab: string;
|
|
203
205
|
icon?: IconConfig;
|
|
204
206
|
hidden?: boolean;
|
|
207
|
+
/** When true on a hidden tab, descendants stay in search/sitemap/AI context. */
|
|
208
|
+
searchable?: boolean;
|
|
205
209
|
href?: string;
|
|
206
210
|
groups?: GroupConfig[];
|
|
207
211
|
pages?: (NavigationPage | GroupConfig)[];
|
|
@@ -88,14 +88,23 @@ function enhancePage(
|
|
|
88
88
|
existing.method || parseApiMethod(fm.api) || parseOpenApiMethod(fm.openapi);
|
|
89
89
|
const icon = existing.icon || (fm.icon as string | undefined);
|
|
90
90
|
const tag = existing.tag || (fm.tag as string | undefined);
|
|
91
|
+
// hidden: explicit nav-level setting wins, otherwise inherit from frontmatter
|
|
92
|
+
const existingHidden = (existing as { hidden?: boolean }).hidden;
|
|
93
|
+
let hidden: boolean | undefined;
|
|
94
|
+
if (typeof existingHidden === 'boolean') {
|
|
95
|
+
hidden = existingHidden;
|
|
96
|
+
} else if (fm.hidden === true) {
|
|
97
|
+
hidden = true;
|
|
98
|
+
}
|
|
91
99
|
|
|
92
|
-
if (title || method || icon || tag) {
|
|
100
|
+
if (title || method || icon || tag || hidden !== undefined) {
|
|
93
101
|
return {
|
|
94
102
|
page: pagePath,
|
|
95
103
|
...(title && { title }),
|
|
96
104
|
...(method && { method }),
|
|
97
105
|
...(icon && { icon }),
|
|
98
106
|
...(tag && { tag }),
|
|
107
|
+
...(hidden !== undefined && { hidden }),
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
|
|
@@ -194,11 +194,19 @@ function resolvePages(pages: (NavigationPage | GroupConfig)[]): {
|
|
|
194
194
|
|
|
195
195
|
for (const item of pages) {
|
|
196
196
|
if (typeof item === 'string' || 'page' in item) {
|
|
197
|
+
// Skip page objects explicitly marked hidden
|
|
198
|
+
if (typeof item !== 'string' && (item as NavigationPageObject & { hidden?: boolean }).hidden === true) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
197
201
|
// It's a page
|
|
198
202
|
const resolved = resolvePage(item as NavigationPage);
|
|
199
203
|
resolvedPages.push(resolved);
|
|
200
204
|
items.push({ type: 'page', page: resolved });
|
|
201
205
|
} else if ('group' in item) {
|
|
206
|
+
// Skip hidden nested groups
|
|
207
|
+
if (item.hidden === true) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
202
210
|
// It's a nested group
|
|
203
211
|
const resolved = resolveGroup(item);
|
|
204
212
|
nestedGroups.push(resolved);
|
|
@@ -234,6 +242,7 @@ function resolveTabGroups(tab: TabConfig): ResolvedGroup[] {
|
|
|
234
242
|
|
|
235
243
|
if (tab.groups) {
|
|
236
244
|
for (const group of tab.groups) {
|
|
245
|
+
if (group.hidden === true) continue;
|
|
237
246
|
groups.push(resolveGroup(group));
|
|
238
247
|
}
|
|
239
248
|
}
|
|
@@ -424,8 +433,9 @@ export function resolveNavigation(
|
|
|
424
433
|
// If no language detected in path, use default
|
|
425
434
|
currentLanguage = detectedLang || defaultLang;
|
|
426
435
|
|
|
427
|
-
// Build resolved languages array
|
|
436
|
+
// Build resolved languages array (skip hidden languages from the selector)
|
|
428
437
|
for (const langConfig of config.navigation.languages) {
|
|
438
|
+
if (langConfig.hidden === true) continue;
|
|
429
439
|
const displayInfo = getLanguageDisplayInfo(langConfig.language);
|
|
430
440
|
resolvedLanguages.push({
|
|
431
441
|
code: langConfig.language,
|
|
@@ -450,16 +460,19 @@ export function resolveNavigation(
|
|
|
450
460
|
|
|
451
461
|
// Resolve top-level external anchors (from config.anchors)
|
|
452
462
|
if (config.anchors && Array.isArray(config.anchors)) {
|
|
453
|
-
result.externalAnchors = config.anchors
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
463
|
+
result.externalAnchors = config.anchors
|
|
464
|
+
.filter((anchor: ExternalAnchorConfig & { hidden?: boolean }) => anchor.hidden !== true)
|
|
465
|
+
.map((anchor: ExternalAnchorConfig) => ({
|
|
466
|
+
name: anchor.name,
|
|
467
|
+
href: anchor.href,
|
|
468
|
+
icon: getIconName(anchor.icon),
|
|
469
|
+
}));
|
|
458
470
|
}
|
|
459
471
|
|
|
460
472
|
// Check for global.anchors (external links)
|
|
461
473
|
if (navigation.global?.anchors) {
|
|
462
474
|
for (const anchor of navigation.global.anchors) {
|
|
475
|
+
if (anchor.hidden === true) continue;
|
|
463
476
|
result.externalAnchors.push({
|
|
464
477
|
name: anchor.anchor,
|
|
465
478
|
href: anchor.href,
|
|
@@ -483,21 +496,24 @@ export function resolveNavigation(
|
|
|
483
496
|
return true;
|
|
484
497
|
});
|
|
485
498
|
|
|
486
|
-
// Resolve top-level tabs
|
|
499
|
+
// Resolve top-level tabs — hidden tabs are excluded from the sidebar and cannot be active
|
|
487
500
|
if (navigation.tabs) {
|
|
488
|
-
|
|
489
|
-
result.
|
|
501
|
+
const visibleTabs = navigation.tabs.filter(t => t.hidden !== true);
|
|
502
|
+
result.tabs = visibleTabs.map(resolveTab);
|
|
503
|
+
result.activeTab = findActiveTab(visibleTabs, pathname);
|
|
490
504
|
|
|
491
505
|
// Only get groups from the active tab
|
|
492
|
-
const activeTabConfig =
|
|
506
|
+
const activeTabConfig = visibleTabs.find(t => t.tab === result.activeTab);
|
|
493
507
|
if (activeTabConfig) {
|
|
494
508
|
result.groups.push(...resolveTabGroups(activeTabConfig));
|
|
495
509
|
}
|
|
496
510
|
}
|
|
497
511
|
|
|
498
|
-
// Resolve top-level groups
|
|
512
|
+
// Resolve top-level groups (hidden groups are excluded from the sidebar)
|
|
499
513
|
if (navigation.groups) {
|
|
500
|
-
result.groups.push(
|
|
514
|
+
result.groups.push(
|
|
515
|
+
...navigation.groups.filter(g => g.hidden !== true).map(resolveGroup),
|
|
516
|
+
);
|
|
501
517
|
}
|
|
502
518
|
|
|
503
519
|
// Resolve top-level pages
|
package/vendored/lib/search.ts
CHANGED
|
@@ -67,6 +67,24 @@ function extractSections(content: string): { heading: string; content: string }[
|
|
|
67
67
|
return sections;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Build an in-memory search index by scanning the local `content/` directory.
|
|
72
|
+
*
|
|
73
|
+
* This is the runtime fallback for when the prebuilt R2 search-data.json
|
|
74
|
+
* artifact is unavailable (e.g. first request before build completes or in
|
|
75
|
+
* local dev). It intentionally mirrors the filtering logic in
|
|
76
|
+
* lib/static-artifacts.ts:generateSearchData — keep both in sync per the
|
|
77
|
+
* search-index sync chain documented in builder/CLAUDE.md.
|
|
78
|
+
*
|
|
79
|
+
* Frontmatter-level visibility (hidden / noindex / seo.noindex) IS honored
|
|
80
|
+
* here, matching build-search-index.cjs and generateSearchData.
|
|
81
|
+
*
|
|
82
|
+
* NOTE: Unlike generateSearchData, this fallback does NOT have access to
|
|
83
|
+
* VisibilityInputs (no config or nav tree available at request time). Pages
|
|
84
|
+
* hidden only via nav-tree hidden:true or seo.indexHiddenPages will therefore
|
|
85
|
+
* still appear in this fallback's results but NOT in the built artifact. This
|
|
86
|
+
* is acceptable — local dev search is documented as "production only".
|
|
87
|
+
*/
|
|
70
88
|
export function buildSearchIndex(): SearchResult[] {
|
|
71
89
|
const documents: SearchResult[] = [];
|
|
72
90
|
const contentDir = path.join(process.cwd(), 'content');
|
|
@@ -87,6 +105,16 @@ export function buildSearchIndex(): SearchResult[] {
|
|
|
87
105
|
const fileContents = fs.readFileSync(filePath, 'utf8');
|
|
88
106
|
const { data, content } = parseFrontmatterLenient(fileContents);
|
|
89
107
|
|
|
108
|
+
// Frontmatter-level visibility: skip author-hidden / noindexed pages,
|
|
109
|
+
// mirroring build-search-index.cjs and generateSearchData.
|
|
110
|
+
if (
|
|
111
|
+
data.hidden === true ||
|
|
112
|
+
data.noindex === true ||
|
|
113
|
+
(data.seo as { noindex?: boolean } | undefined)?.noindex === true
|
|
114
|
+
) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
// Filter for="agents" content out of the search index.
|
|
91
119
|
const visibleContent = filterVisibility(content, 'humans');
|
|
92
120
|
const sections = extractSections(visibleContent);
|
package/vendored/lib/seo.ts
CHANGED
|
@@ -618,17 +618,24 @@ export function buildSeoMetadata(
|
|
|
618
618
|
// 1. Generator - always add
|
|
619
619
|
metadata.generator = 'Jamdesk';
|
|
620
620
|
|
|
621
|
-
// 2. Robots (priority: page > global)
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
621
|
+
// 2. Robots (priority: explicit page > hidden auto-noindex > global)
|
|
622
|
+
// Explicit page noindex via frontmatter.noindex or frontmatter.seo.noindex always wins.
|
|
623
|
+
// hidden: true implies noindex,follow UNLESS the project opts hidden pages into
|
|
624
|
+
// indexing via seo.indexHiddenPages: true or seo.indexing: 'all'.
|
|
625
|
+
const explicitPageNoindex = frontmatter.noindex ?? frontmatter.seo?.noindex;
|
|
626
|
+
const projectIndexesHidden =
|
|
627
|
+
config.seo?.indexHiddenPages === true || config.seo?.indexing === 'all';
|
|
628
|
+
const hiddenImpliesNoindex = frontmatter.hidden === true && !projectIndexesHidden;
|
|
629
|
+
const effectiveNoindex = explicitPageNoindex ?? (hiddenImpliesNoindex ? true : undefined);
|
|
630
|
+
|
|
631
|
+
if (effectiveNoindex === true) {
|
|
625
632
|
// noindex does NOT imply nofollow - use follow: true
|
|
626
633
|
metadata.robots = { index: false, follow: true };
|
|
627
|
-
} else if (
|
|
634
|
+
} else if (effectiveNoindex !== false && metatags.robots) {
|
|
628
635
|
// Page didn't explicitly set noindex: false, so use global robots
|
|
629
636
|
metadata.robots = metatags.robots;
|
|
630
637
|
}
|
|
631
|
-
// If
|
|
638
|
+
// If effectiveNoindex === false, it overrides any global noindex (no robots meta = index)
|
|
632
639
|
|
|
633
640
|
// 3. Googlebot (separate from robots)
|
|
634
641
|
if (metatags.googlebot) {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveLocaleWithLoweredSet,
|
|
15
15
|
} from './language-utils.js';
|
|
16
16
|
import { buildHreflangAlternates } from './seo.js';
|
|
17
|
+
import { computePageVisibility, type VisibilityInputs } from './visibility.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Page metadata for artifact generation.
|
|
@@ -52,16 +53,45 @@ export interface SitemapOptions {
|
|
|
52
53
|
* localization signal (in addition to in-page hreflang link tags).
|
|
53
54
|
*/
|
|
54
55
|
languages?: LanguageConfig[];
|
|
56
|
+
/**
|
|
57
|
+
* Visibility inputs from lib/visibility.ts. When provided, filtering
|
|
58
|
+
* delegates to computePageVisibility for consistent hidden/orphan/searchable
|
|
59
|
+
* handling across all artifacts. Falls back to simple noindex+hidden check
|
|
60
|
+
* when omitted (legacy behavior for call sites not yet wired to build.ts).
|
|
61
|
+
*/
|
|
62
|
+
visibility?: VisibilityInputs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether a PageMetadata entry belongs in an artifact.
|
|
67
|
+
*
|
|
68
|
+
* When visibility inputs are present, delegates to computePageVisibility for
|
|
69
|
+
* consistent hidden/orphan/searchable handling. Otherwise falls back to the
|
|
70
|
+
* legacy noindex+hidden check. Shared by sitemap and llms.txt so both apply the
|
|
71
|
+
* same rule (PageMetadata has no per-page seo.noindex, so it's passed undefined).
|
|
72
|
+
*/
|
|
73
|
+
function isPageMetadataIncluded(page: PageMetadata, visibility?: VisibilityInputs): boolean {
|
|
74
|
+
if (!visibility) {
|
|
75
|
+
return !page.noindex && !page.hidden;
|
|
76
|
+
}
|
|
77
|
+
return computePageVisibility(
|
|
78
|
+
page.path,
|
|
79
|
+
{ hidden: page.hidden, noindex: page.noindex, seo: { noindex: undefined } },
|
|
80
|
+
visibility,
|
|
81
|
+
).inArtifacts;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
/**
|
|
58
85
|
* Generate sitemap.xml from page metadata.
|
|
59
86
|
*
|
|
87
|
+
* Filtering delegates to lib/visibility.ts when visibility inputs are provided,
|
|
88
|
+
* giving consistent hidden/orphan/searchable handling across all artifacts.
|
|
89
|
+
*
|
|
60
90
|
* @param options - Sitemap options
|
|
61
91
|
* @returns XML string
|
|
62
92
|
*/
|
|
63
93
|
export function generateSitemap(options: SitemapOptions): string {
|
|
64
|
-
const { baseUrl, pages, hostAtDocs = false, noindex = false, languages } = options;
|
|
94
|
+
const { baseUrl, pages, hostAtDocs = false, noindex = false, languages, visibility } = options;
|
|
65
95
|
|
|
66
96
|
if (noindex) {
|
|
67
97
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -77,7 +107,7 @@ export function generateSitemap(options: SitemapOptions): string {
|
|
|
77
107
|
: 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
78
108
|
|
|
79
109
|
const entries = pages
|
|
80
|
-
.filter(p =>
|
|
110
|
+
.filter(p => isPageMetadataIncluded(p, visibility))
|
|
81
111
|
.map(p => {
|
|
82
112
|
const url = `${baseUrl}${urlPrefix}/${p.path}`;
|
|
83
113
|
const lastmod = p.lastModified || new Date().toISOString().split('T')[0];
|
|
@@ -129,16 +159,25 @@ export interface LlmsTxtOptions {
|
|
|
129
159
|
hostAtDocs?: boolean;
|
|
130
160
|
/** Block all crawlers - generates empty llms.txt */
|
|
131
161
|
noindex?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Visibility inputs from lib/visibility.ts. When provided, filtering
|
|
164
|
+
* delegates to computePageVisibility for consistent hidden/orphan/searchable
|
|
165
|
+
* handling across all artifacts.
|
|
166
|
+
*/
|
|
167
|
+
visibility?: VisibilityInputs;
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
/**
|
|
135
171
|
* Generate llms.txt following https://llmstxt.org/ spec.
|
|
136
172
|
*
|
|
173
|
+
* Filtering delegates to lib/visibility.ts when visibility inputs are provided,
|
|
174
|
+
* giving consistent hidden/orphan/searchable handling across all artifacts.
|
|
175
|
+
*
|
|
137
176
|
* @param options - LLMs.txt options
|
|
138
177
|
* @returns Plain text string
|
|
139
178
|
*/
|
|
140
179
|
export function generateLlmsTxt(options: LlmsTxtOptions): string {
|
|
141
|
-
const { name, description, baseUrl, pages, hostAtDocs = false, noindex = false } = options;
|
|
180
|
+
const { name, description, baseUrl, pages, hostAtDocs = false, noindex = false, visibility } = options;
|
|
142
181
|
|
|
143
182
|
if (noindex) {
|
|
144
183
|
return '';
|
|
@@ -153,7 +192,7 @@ export function generateLlmsTxt(options: LlmsTxtOptions): string {
|
|
|
153
192
|
}
|
|
154
193
|
|
|
155
194
|
for (const page of pages) {
|
|
156
|
-
if (page
|
|
195
|
+
if (!isPageMetadataIncluded(page, visibility)) continue;
|
|
157
196
|
|
|
158
197
|
const url = `${baseUrl}${urlPrefix}/${page.path}.md`;
|
|
159
198
|
const desc = page.description ? `: ${page.description}` : '';
|
|
@@ -317,22 +356,34 @@ export interface LlmsFullTxtOptions {
|
|
|
317
356
|
pages: LlmsFullPageInfo[];
|
|
318
357
|
/** Block all crawlers - generates empty file */
|
|
319
358
|
noindex?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Visibility inputs from lib/visibility.ts. When provided, filtering
|
|
361
|
+
* delegates to computePageVisibility for consistent hidden/orphan/searchable
|
|
362
|
+
* handling across all artifacts.
|
|
363
|
+
*/
|
|
364
|
+
visibility?: VisibilityInputs;
|
|
320
365
|
}
|
|
321
366
|
|
|
322
367
|
/**
|
|
323
368
|
* Generate llms-full.txt with complete documentation content for LLM context windows.
|
|
324
369
|
* Follows https://llmstxt.org/ spec.
|
|
370
|
+
*
|
|
371
|
+
* Filtering delegates to lib/visibility.ts when visibility inputs are provided,
|
|
372
|
+
* giving consistent hidden/orphan/searchable handling across all artifacts.
|
|
325
373
|
*/
|
|
326
374
|
export function generateLlmsFullTxt(options: LlmsFullTxtOptions): string {
|
|
327
|
-
const { name, pages, noindex = false } = options;
|
|
375
|
+
const { name, pages, noindex = false, visibility } = options;
|
|
328
376
|
|
|
329
377
|
if (noindex) {
|
|
330
378
|
return '';
|
|
331
379
|
}
|
|
332
380
|
|
|
333
|
-
const visiblePages = pages.filter(p =>
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
const visiblePages = pages.filter(p => {
|
|
382
|
+
const path = p.path.replace(/\.mdx?$/, '');
|
|
383
|
+
return visibility
|
|
384
|
+
? computePageVisibility(path, p.frontmatter as never, visibility).inArtifacts
|
|
385
|
+
: !p.frontmatter.noindex && !p.frontmatter.hidden && !p.frontmatter.seo?.noindex;
|
|
386
|
+
});
|
|
336
387
|
|
|
337
388
|
const parts: string[] = [
|
|
338
389
|
`# ${name} - Complete Documentation\n\n`,
|
|
@@ -384,6 +435,14 @@ export interface GenerateAllOptions {
|
|
|
384
435
|
llmsFullPages?: LlmsFullPageInfo[];
|
|
385
436
|
/** Language configurations (forwarded to sitemap for hreflang siblings) */
|
|
386
437
|
languages?: LanguageConfig[];
|
|
438
|
+
/**
|
|
439
|
+
* Visibility inputs from lib/visibility.ts. When provided, all artifact
|
|
440
|
+
* generators use computePageVisibility for consistent hidden/orphan/searchable
|
|
441
|
+
* filtering. The production caller (build.ts) always passes this; the legacy
|
|
442
|
+
* fallback (simple noindex+hidden check) exists for test call sites that
|
|
443
|
+
* construct minimal options objects.
|
|
444
|
+
*/
|
|
445
|
+
visibility?: VisibilityInputs;
|
|
387
446
|
}
|
|
388
447
|
|
|
389
448
|
/**
|
|
@@ -400,20 +459,26 @@ export interface GeneratedArtifacts {
|
|
|
400
459
|
/**
|
|
401
460
|
* Generate all static artifacts for a project.
|
|
402
461
|
*
|
|
462
|
+
* When visibility inputs are provided (always in production via build.ts), all
|
|
463
|
+
* generators use computePageVisibility from lib/visibility.ts for consistent
|
|
464
|
+
* hidden/orphan/searchable filtering across sitemap, llms.txt, llms-full.txt,
|
|
465
|
+
* and search index.
|
|
466
|
+
*
|
|
403
467
|
* @param options - Generation options
|
|
404
468
|
* @returns Object with all generated artifacts
|
|
405
469
|
*/
|
|
406
470
|
export function generateAllArtifacts(options: GenerateAllOptions): GeneratedArtifacts {
|
|
407
471
|
const {
|
|
408
472
|
baseUrl, name, description, pages, hostAtDocs, noindex, rssPages, llmsFullPages, languages,
|
|
473
|
+
visibility,
|
|
409
474
|
} = options;
|
|
410
475
|
|
|
411
|
-
const sitemap = generateSitemap({ baseUrl, pages, hostAtDocs, noindex, languages });
|
|
476
|
+
const sitemap = generateSitemap({ baseUrl, pages, hostAtDocs, noindex, languages, visibility });
|
|
412
477
|
const llmsTxt = generateLlmsTxt({
|
|
413
|
-
name, description, baseUrl, pages, hostAtDocs, noindex,
|
|
478
|
+
name, description, baseUrl, pages, hostAtDocs, noindex, visibility,
|
|
414
479
|
});
|
|
415
480
|
const llmsFullTxt = llmsFullPages
|
|
416
|
-
? generateLlmsFullTxt({ name, pages: llmsFullPages, noindex })
|
|
481
|
+
? generateLlmsFullTxt({ name, pages: llmsFullPages, noindex, visibility })
|
|
417
482
|
: '';
|
|
418
483
|
const robotsTxt = generateRobotsTxt({ baseUrl, hostAtDocs, noindex });
|
|
419
484
|
|
|
@@ -724,15 +789,21 @@ export function extractSections(content: string): Array<{ heading: string; conte
|
|
|
724
789
|
/**
|
|
725
790
|
* Generate search data from page content.
|
|
726
791
|
*
|
|
792
|
+
* Filtering delegates to lib/visibility.ts when visibility inputs are provided,
|
|
793
|
+
* giving consistent hidden/orphan/searchable handling across all artifacts.
|
|
794
|
+
* Keep this filter in sync with lib/search.ts:buildSearchIndex (runtime fallback).
|
|
795
|
+
*
|
|
727
796
|
* @param pages - Array of page info with content
|
|
728
797
|
* @param projectLanguages - Language codes declared in docs.json.navigation.languages.
|
|
729
798
|
* Used as the locale whitelist; slugs whose prefix is not in this list are
|
|
730
799
|
* tagged as default-language (locale='').
|
|
800
|
+
* @param visibility - Optional visibility inputs; when omitted all pages are included.
|
|
731
801
|
* @returns JSON string of search documents
|
|
732
802
|
*/
|
|
733
803
|
export function generateSearchData(
|
|
734
804
|
pages: SearchPageInfo[],
|
|
735
805
|
projectLanguages: readonly string[],
|
|
806
|
+
visibility?: VisibilityInputs,
|
|
736
807
|
): string {
|
|
737
808
|
const documents: SearchDocument[] = [];
|
|
738
809
|
const loweredLanguages = buildLoweredLocaleSet(projectLanguages);
|
|
@@ -740,6 +811,16 @@ export function generateSearchData(
|
|
|
740
811
|
for (const page of pages) {
|
|
741
812
|
const pathWithoutExt = page.path.replace(/\.mdx?$/, '');
|
|
742
813
|
const slug = pathWithoutExt.replace(/\\/g, '/');
|
|
814
|
+
|
|
815
|
+
// Skip pages excluded by the visibility module when inputs are available.
|
|
816
|
+
if (visibility && !computePageVisibility(
|
|
817
|
+
slug,
|
|
818
|
+
page.frontmatter as never,
|
|
819
|
+
visibility,
|
|
820
|
+
).inArtifacts) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
|
|
743
824
|
const pageType = inferPageType(slug);
|
|
744
825
|
const locale = resolveLocaleWithLoweredSet(slug, loweredLanguages);
|
|
745
826
|
// Filter for="agents" content out of the search index — the site
|