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.
Files changed (36) hide show
  1. package/dist/__tests__/unit/deps-sync.test.js +19 -12
  2. package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
  3. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
  4. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
  6. package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
  7. package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
  8. package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/language-filter.test.js +166 -0
  10. package/dist/__tests__/unit/language-filter.test.js.map +1 -0
  11. package/dist/__tests__/unit/verbose-hint.test.d.ts +2 -0
  12. package/dist/__tests__/unit/verbose-hint.test.d.ts.map +1 -0
  13. package/dist/__tests__/unit/verbose-hint.test.js +31 -0
  14. package/dist/__tests__/unit/verbose-hint.test.js.map +1 -0
  15. package/dist/index.js +7 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/language-filter.d.ts +31 -0
  18. package/dist/lib/language-filter.d.ts.map +1 -0
  19. package/dist/lib/language-filter.js +14 -0
  20. package/dist/lib/language-filter.js.map +1 -0
  21. package/dist/lib/verbose-hint.d.ts +25 -0
  22. package/dist/lib/verbose-hint.d.ts.map +1 -0
  23. package/dist/lib/verbose-hint.js +27 -0
  24. package/dist/lib/verbose-hint.js.map +1 -0
  25. package/package.json +1 -1
  26. package/vendored/components/navigation/PageNavigation.tsx +40 -43
  27. package/vendored/lib/docs-types.ts +4 -0
  28. package/vendored/lib/enhance-navigation.ts +10 -1
  29. package/vendored/lib/navigation-resolver.ts +28 -12
  30. package/vendored/lib/search.ts +28 -0
  31. package/vendored/lib/seo.ts +13 -6
  32. package/vendored/lib/static-artifacts.ts +92 -11
  33. package/vendored/lib/visibility.ts +197 -0
  34. package/vendored/schema/docs-schema.json +43 -0
  35. package/vendored/scripts/build-search-index.cjs +47 -11
  36. 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.124",
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
- for (const item of group.pages) {
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
- for (const item of anchor.pages) {
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
- for (const item of tab.pages) {
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
- for (const item of nav.pages) {
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.map((anchor: ExternalAnchorConfig) => ({
454
- name: anchor.name,
455
- href: anchor.href,
456
- icon: getIconName(anchor.icon),
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
- result.tabs = navigation.tabs.map(resolveTab);
489
- result.activeTab = findActiveTab(navigation.tabs, pathname);
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 = navigation.tabs.find(t => t.tab === result.activeTab);
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(...navigation.groups.map(resolveGroup));
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
@@ -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);
@@ -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
- // Page noindex can be set via frontmatter.noindex or frontmatter.seo.noindex
623
- const pageNoindex = frontmatter.noindex ?? frontmatter.seo?.noindex;
624
- if (pageNoindex === true) {
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 (pageNoindex !== false && metatags.robots) {
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 pageNoindex === false, it overrides any global noindex (no robots meta = index)
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 => !p.noindex && !p.hidden)
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.noindex || page.hidden) continue;
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
- !p.frontmatter.noindex && !p.frontmatter.hidden && !p.frontmatter.seo?.noindex
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