@mui/internal-docs-infra 0.4.1-canary.9 → 0.5.1-canary.0
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/cli/index.mjs +1 -1
- package/createSitemap/createSitemap.mjs +1 -1
- package/createSitemap/types.d.mts +36 -1
- package/package.json +4 -4
- package/pipeline/syncPageIndex/mergeMetadataMarkdown.d.mts +2 -2
- package/pipeline/syncPageIndex/mergeMetadataMarkdown.mjs +16 -8
- package/pipeline/syncPageIndex/metadataToMarkdown.d.mts +17 -0
- package/pipeline/syncPageIndex/metadataToMarkdown.mjs +314 -87
- package/pipeline/syncPageIndex/syncPageIndex.mjs +18 -7
- package/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.mjs +8 -3
- package/pipeline/transformMarkdownMetadata/types.d.mts +8 -0
- package/useSearch/types.d.mts +13 -0
- package/useSearch/useSearch.mjs +7 -2
- package/withDocsInfra/withDeploymentConfig.mjs +3 -0
package/cli/index.mjs
CHANGED
|
@@ -7,4 +7,4 @@ function getVersion() {
|
|
|
7
7
|
}
|
|
8
8
|
yargs().scriptName('docs-infra').usage('$0 <command> [args]').command(runValidate).demandCommand(1, 'You need at least one command before moving on').strict().help()
|
|
9
9
|
// MUI_VERSION is set through the code-infra build command.
|
|
10
|
-
.version("0.
|
|
10
|
+
.version("0.5.0" || getVersion()).parse(hideBin(process.argv));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
1
2
|
/**
|
|
2
3
|
* Section data structure from sitemap
|
|
3
4
|
*/
|
|
@@ -35,6 +36,8 @@ export interface SitemapPage {
|
|
|
35
36
|
exports?: Record<string, SitemapExport>;
|
|
36
37
|
tags?: string[];
|
|
37
38
|
skipDetailSection?: boolean;
|
|
39
|
+
audience?: Audience;
|
|
40
|
+
index?: boolean;
|
|
38
41
|
image?: {
|
|
39
42
|
url: string;
|
|
40
43
|
alt?: string;
|
|
@@ -59,4 +62,36 @@ export type OramaSchemaType = 'string' | 'number' | 'boolean' | 'string[]' | 'nu
|
|
|
59
62
|
export interface Sitemap {
|
|
60
63
|
schema: Record<string, OramaSchemaType>;
|
|
61
64
|
data: Record<string, SitemapSectionData>;
|
|
62
|
-
}
|
|
65
|
+
}
|
|
66
|
+
export type Audience = 'private' | 'introductory' | 'intermediate' | 'advanced' | 'business';
|
|
67
|
+
/**
|
|
68
|
+
* Page metadata type extending Next.js `Metadata`.
|
|
69
|
+
*
|
|
70
|
+
* Adds the `audience` field under `other` using the WHATWG MetaExtensions `audience` meta name.
|
|
71
|
+
* All standard Next.js metadata fields (title, description, openGraph, etc.) remain available.
|
|
72
|
+
*
|
|
73
|
+
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadata-fields
|
|
74
|
+
*/
|
|
75
|
+
export type NextMetadata = Metadata & {
|
|
76
|
+
other?: {
|
|
77
|
+
/**
|
|
78
|
+
* Categorize the principal intended audience for the page.
|
|
79
|
+
* Uses the WHATWG MetaExtensions `audience` meta name.
|
|
80
|
+
*
|
|
81
|
+
* When omitted, the page is public and intended for all audiences.
|
|
82
|
+
*
|
|
83
|
+
* - `'private'`: Internal page, not intended for public consumption.
|
|
84
|
+
* Should be paired with `robots: { index: false }` to exclude from public indexing.
|
|
85
|
+
* - `'introductory'`: Content aimed at beginners.
|
|
86
|
+
* - `'intermediate'`: Content aimed at intermediate users.
|
|
87
|
+
* - `'advanced'`: Content aimed at advanced users.
|
|
88
|
+
* - `'business'`: Content aimed at prospective customers and decision-makers
|
|
89
|
+
* (e.g. marketing pages, pricing, product overviews).
|
|
90
|
+
*
|
|
91
|
+
* @see https://wiki.whatwg.org/wiki/MetaExtensions
|
|
92
|
+
* @see https://brittlebit.org/specifications/html-meta-audience/specification-for-html-meta-element-with-name-value-audience.html
|
|
93
|
+
*/
|
|
94
|
+
audience?: Audience;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
};
|
|
97
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1-canary.0",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
6
|
"keywords": [
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"homepage": "https://github.com/mui/mui-public/tree/master/packages/docs-infra",
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@babel/runtime": "^7.28.6",
|
|
25
|
-
"@babel/standalone": "^7.
|
|
25
|
+
"@babel/standalone": "^7.29.1",
|
|
26
26
|
"@orama/orama": "^3.1.18",
|
|
27
27
|
"@orama/plugin-qps": "^3.1.18",
|
|
28
28
|
"@orama/stemmers": "^3.1.18",
|
|
29
29
|
"@orama/stopwords": "^3.1.18",
|
|
30
|
-
"@wooorm/starry-night": "^3.
|
|
30
|
+
"@wooorm/starry-night": "^3.9.0",
|
|
31
31
|
"chalk": "^5.6.2",
|
|
32
32
|
"clipboard-copy": "^4.0.1",
|
|
33
33
|
"fflate": "^0.8.2",
|
|
@@ -506,5 +506,5 @@
|
|
|
506
506
|
"bin": {
|
|
507
507
|
"docs-infra": "./cli/index.mjs"
|
|
508
508
|
},
|
|
509
|
-
"gitSha": "
|
|
509
|
+
"gitSha": "df142b55588e09b911db8be848c133b0ebad1cc0"
|
|
510
510
|
}
|
|
@@ -38,8 +38,8 @@ export interface MergeMetadataMarkdownOptions extends Omit<MetadataToMarkdownOpt
|
|
|
38
38
|
* @example
|
|
39
39
|
* ```ts
|
|
40
40
|
* const existingMarkdown = `# Components
|
|
41
|
-
* - [
|
|
42
|
-
* - [
|
|
41
|
+
* - Button - ([Outline](#button), [Contents](./button/page.mdx)) - A button
|
|
42
|
+
* - Checkbox - ([Outline](#checkbox), [Contents](./checkbox/page.mdx)) - A checkbox
|
|
43
43
|
* `;
|
|
44
44
|
*
|
|
45
45
|
* const newMetadata = {
|
|
@@ -24,8 +24,8 @@ import { markdownToMetadata, metadataToMarkdown } from "./metadataToMarkdown.mjs
|
|
|
24
24
|
* @example
|
|
25
25
|
* ```ts
|
|
26
26
|
* const existingMarkdown = `# Components
|
|
27
|
-
* - [
|
|
28
|
-
* - [
|
|
27
|
+
* - Button - ([Outline](#button), [Contents](./button/page.mdx)) - A button
|
|
28
|
+
* - Checkbox - ([Outline](#checkbox), [Contents](./checkbox/page.mdx)) - A checkbox
|
|
29
29
|
* `;
|
|
30
30
|
*
|
|
31
31
|
* const newMetadata = {
|
|
@@ -108,7 +108,11 @@ export async function mergeMetadataMarkdown(existingMarkdown, newMetadata, optio
|
|
|
108
108
|
// Preserve skipDetailSection from existing (user-managed for external links)
|
|
109
109
|
skipDetailSection: existingPage.skipDetailSection,
|
|
110
110
|
// Preserve sections from existing if new doesn't have them
|
|
111
|
-
sections: newPage.sections || existingPage.sections
|
|
111
|
+
sections: newPage.sections || existingPage.sections,
|
|
112
|
+
// Preserve displayTitle (user-managed title override) only if it still
|
|
113
|
+
// differs from the new title. If the override now matches the actual title,
|
|
114
|
+
// clear it so the titles stay in sync going forward.
|
|
115
|
+
displayTitle: existingPage.displayTitle && existingPage.displayTitle !== newPage.title ? existingPage.displayTitle : undefined
|
|
112
116
|
};
|
|
113
117
|
pages.push(merged);
|
|
114
118
|
addedPaths.add(newPage.path);
|
|
@@ -130,12 +134,14 @@ export async function mergeMetadataMarkdown(existingMarkdown, newMetadata, optio
|
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
// If alphabetical sorting is requested, sort pages alphabetically by title
|
|
133
|
-
const alphabeticalSortMarker = "[//]: # 'This
|
|
134
|
-
|
|
137
|
+
const alphabeticalSortMarker = "[//]: # 'This section is autogenerated, but the following list order, title, and [Tag]s can be modified, but nothing within the parentheses. Automatically sorted alphabetically.'";
|
|
138
|
+
// TODO: Remove the old marker check once all index files have been migrated to the new format.
|
|
139
|
+
const oldAlphabeticalSortMarker = "[//]: # 'This file is autogenerated, but the following list can be modified. Automatically sorted alphabetically.'";
|
|
140
|
+
const requestsAlphabeticalSort = existingMarkdown.includes(alphabeticalSortMarker) || existingMarkdown.includes(oldAlphabeticalSortMarker);
|
|
135
141
|
if (requestsAlphabeticalSort) {
|
|
136
142
|
pages = pages.sort((a, b) => {
|
|
137
|
-
const titleA = a.title
|
|
138
|
-
const titleB = b.title
|
|
143
|
+
const titleA = a.displayTitle ?? a.title ?? a.slug;
|
|
144
|
+
const titleB = b.displayTitle ?? b.title ?? b.slug;
|
|
139
145
|
return titleA.localeCompare(titleB);
|
|
140
146
|
});
|
|
141
147
|
}
|
|
@@ -144,7 +150,9 @@ export async function mergeMetadataMarkdown(existingMarkdown, newMetadata, optio
|
|
|
144
150
|
const mergedMetadata = {
|
|
145
151
|
title: newMetadata.title,
|
|
146
152
|
// Always use the new title
|
|
147
|
-
pages
|
|
153
|
+
pages,
|
|
154
|
+
// Preserve the existing pageMetadata (e.g., robots config) from the current file
|
|
155
|
+
pageMetadata: existingMetadata.pageMetadata
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
// Preserve the alphabetical sorting marker if it was present
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import type { Root } from 'mdast';
|
|
2
2
|
import type { ExtractedMetadata } from "../transformMarkdownMetadata/types.mjs";
|
|
3
|
+
import { Audience } from "../../createSitemap/types.mjs";
|
|
3
4
|
export interface PageMetadata extends ExtractedMetadata {
|
|
4
5
|
/** The slug/path for this page (e.g., 'button', 'checkbox') */
|
|
5
6
|
slug: string;
|
|
6
7
|
/** The relative path to the page's MDX file */
|
|
7
8
|
path: string;
|
|
9
|
+
/**
|
|
10
|
+
* User-customized display title for the page.
|
|
11
|
+
* When set, this overrides `title` in the navigation list and sitemap.
|
|
12
|
+
* The `title` field continues to be used for the heading section.
|
|
13
|
+
*
|
|
14
|
+
* This value is detected automatically: if a user edits the list item text
|
|
15
|
+
* so it no longer matches the heading, the edited text is preserved here.
|
|
16
|
+
*/
|
|
17
|
+
displayTitle?: string;
|
|
8
18
|
/** Tags for this entry (e.g., 'New', 'Hot', 'Beta') */
|
|
9
19
|
tags?: string[];
|
|
10
20
|
/** Skip generating detail section for this entry (for external links) */
|
|
11
21
|
skipDetailSection?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The intended audience for this page.
|
|
24
|
+
* When omitted, the page is public and intended for all audiences.
|
|
25
|
+
*/
|
|
26
|
+
audience?: Audience;
|
|
27
|
+
/** Whether this page is an index page (has the autogenerated comment marker) */
|
|
28
|
+
index?: boolean;
|
|
12
29
|
/** Component parts with their API metadata (for multi-part components) */
|
|
13
30
|
parts?: Record<string, {
|
|
14
31
|
props?: string[];
|
|
@@ -32,6 +32,135 @@ function astNodesToMarkdown(nodes) {
|
|
|
32
32
|
* Options for metadataToMarkdown and metadataToMarkdownAst functions
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Serializes a JS value to a JavaScript-style literal string (unquoted keys, trailing commas).
|
|
37
|
+
* This produces output like `{ robots: { index: false } }` instead of JSON's `{ "robots": { "index": false } }`.
|
|
38
|
+
*/
|
|
39
|
+
function serializeJsValue(value, indent) {
|
|
40
|
+
if (value === null || value === undefined) {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
// Use single quotes for strings, escaping any internal single quotes
|
|
45
|
+
return `'${String(value).replace(/'/g, "\\'")}'`;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== 'object') {
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
if (value.length === 0) {
|
|
52
|
+
return '[]';
|
|
53
|
+
}
|
|
54
|
+
const items = value.map(item => `${' '.repeat(indent + 1)}${serializeJsValue(item, indent + 1)},`);
|
|
55
|
+
return `[\n${items.join('\n')}\n${' '.repeat(indent)}]`;
|
|
56
|
+
}
|
|
57
|
+
const entries = Object.entries(value);
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
return '{}';
|
|
60
|
+
}
|
|
61
|
+
const lines = entries.map(([key, val]) => `${' '.repeat(indent + 1)}${key}: ${serializeJsValue(val, indent + 1)},`);
|
|
62
|
+
return `{\n${lines.join('\n')}\n${' '.repeat(indent)}}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets the status label for a page based on its audience and index flags.
|
|
67
|
+
*
|
|
68
|
+
* Status labels in the editable list:
|
|
69
|
+
* - \"Private\": audience='private', index=false
|
|
70
|
+
* - \"Index\": audience='private', index=true
|
|
71
|
+
* - \"Public Index\": audience!='private', index=true
|
|
72
|
+
* - \"Introductory\": audience='introductory'
|
|
73
|
+
* - \"Intermediate\": audience='intermediate'
|
|
74
|
+
* - \"Advanced\": audience='advanced'
|
|
75
|
+
* - \"Business\": audience='business'
|
|
76
|
+
*/
|
|
77
|
+
function getStatusLabel(page) {
|
|
78
|
+
const audience = page.audience;
|
|
79
|
+
const isIndex = page.index ?? false;
|
|
80
|
+
if (audience === 'private' && isIndex) {
|
|
81
|
+
return 'Index';
|
|
82
|
+
}
|
|
83
|
+
if (audience === 'private' && !isIndex) {
|
|
84
|
+
return 'Private';
|
|
85
|
+
}
|
|
86
|
+
if (audience && audience !== 'private' && isIndex) {
|
|
87
|
+
const audienceLabel = audience.charAt(0).toUpperCase() + audience.slice(1);
|
|
88
|
+
return `${audienceLabel} Index`;
|
|
89
|
+
}
|
|
90
|
+
if (audience && audience !== 'private') {
|
|
91
|
+
return audience.charAt(0).toUpperCase() + audience.slice(1);
|
|
92
|
+
}
|
|
93
|
+
if (isIndex) {
|
|
94
|
+
return 'Public Index';
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parses a status label string back into audience/index flags.
|
|
101
|
+
*/
|
|
102
|
+
function parseStatusLabel(label) {
|
|
103
|
+
const trimmed = label.trim();
|
|
104
|
+
switch (trimmed) {
|
|
105
|
+
case 'Private':
|
|
106
|
+
return {
|
|
107
|
+
audience: 'private',
|
|
108
|
+
index: false
|
|
109
|
+
};
|
|
110
|
+
case 'Index':
|
|
111
|
+
return {
|
|
112
|
+
audience: 'private',
|
|
113
|
+
index: true
|
|
114
|
+
};
|
|
115
|
+
case 'Public Index':
|
|
116
|
+
return {
|
|
117
|
+
index: true
|
|
118
|
+
};
|
|
119
|
+
case 'Introductory':
|
|
120
|
+
return {
|
|
121
|
+
audience: 'introductory',
|
|
122
|
+
index: false
|
|
123
|
+
};
|
|
124
|
+
case 'Introductory Index':
|
|
125
|
+
return {
|
|
126
|
+
audience: 'introductory',
|
|
127
|
+
index: true
|
|
128
|
+
};
|
|
129
|
+
case 'Intermediate':
|
|
130
|
+
return {
|
|
131
|
+
audience: 'intermediate',
|
|
132
|
+
index: false
|
|
133
|
+
};
|
|
134
|
+
case 'Intermediate Index':
|
|
135
|
+
return {
|
|
136
|
+
audience: 'intermediate',
|
|
137
|
+
index: true
|
|
138
|
+
};
|
|
139
|
+
case 'Advanced':
|
|
140
|
+
return {
|
|
141
|
+
audience: 'advanced',
|
|
142
|
+
index: false
|
|
143
|
+
};
|
|
144
|
+
case 'Advanced Index':
|
|
145
|
+
return {
|
|
146
|
+
audience: 'advanced',
|
|
147
|
+
index: true
|
|
148
|
+
};
|
|
149
|
+
case 'Business':
|
|
150
|
+
return {
|
|
151
|
+
audience: 'business',
|
|
152
|
+
index: false
|
|
153
|
+
};
|
|
154
|
+
case 'Business Index':
|
|
155
|
+
return {
|
|
156
|
+
audience: 'business',
|
|
157
|
+
index: true
|
|
158
|
+
};
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`Unknown status label "${trimmed}". ` + 'Valid labels are: Private, Index, Public Index, Introductory, Introductory Index, ' + 'Intermediate, Intermediate Index, Advanced, Advanced Index, Business, Business Index.');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
35
164
|
/**
|
|
36
165
|
* Converts a HeadingHierarchy into markdown list format
|
|
37
166
|
*/
|
|
@@ -339,7 +468,7 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
339
468
|
|
|
340
469
|
// Add editable section marker
|
|
341
470
|
// Extract just the comment text from the marker (strip [//]: # 'text' wrapper)
|
|
342
|
-
const defaultMarkerText = 'This
|
|
471
|
+
const defaultMarkerText = 'This section is autogenerated, but the following list order, title, and [Tag]s can be modified, but nothing within the parentheses.';
|
|
343
472
|
let markerText = defaultMarkerText;
|
|
344
473
|
if (editableMarker) {
|
|
345
474
|
// Extract text between single quotes: [//]: # 'text'
|
|
@@ -359,14 +488,15 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
359
488
|
// Add page list (editable section) as proper list items
|
|
360
489
|
const listItems = [];
|
|
361
490
|
for (const page of pages) {
|
|
362
|
-
|
|
491
|
+
// List items use displayTitle when the user has overridden the title
|
|
492
|
+
const listTitle = page.displayTitle ?? page.title ?? page.slug;
|
|
363
493
|
|
|
364
494
|
// Check if this is a single-link entry (external link or no detail section)
|
|
365
495
|
const isSingleLink = page.skipDetailSection || false;
|
|
366
496
|
let paragraphChildren;
|
|
367
497
|
if (isSingleLink) {
|
|
368
498
|
// Format: - [Title](./path) [Tag1] [Tag2]
|
|
369
|
-
paragraphChildren = [link(page.path,
|
|
499
|
+
paragraphChildren = [link(page.path, listTitle)];
|
|
370
500
|
|
|
371
501
|
// Add tags if present (directly after link)
|
|
372
502
|
if (page.tags && page.tags.length > 0) {
|
|
@@ -375,8 +505,9 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
375
505
|
}
|
|
376
506
|
}
|
|
377
507
|
} else {
|
|
378
|
-
// Format: -
|
|
379
|
-
|
|
508
|
+
// Format: - Title [Tag1] [Tag2] - (StatusLabel, [Outline](#slug), [Contents](./path))
|
|
509
|
+
const statusLabel = getStatusLabel(page);
|
|
510
|
+
paragraphChildren = [text(listTitle)];
|
|
380
511
|
|
|
381
512
|
// Add tags if present (directly after component name)
|
|
382
513
|
if (page.tags && page.tags.length > 0) {
|
|
@@ -385,9 +516,12 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
385
516
|
}
|
|
386
517
|
}
|
|
387
518
|
|
|
388
|
-
// Add separator and
|
|
389
|
-
paragraphChildren.push(text(' - '));
|
|
390
|
-
paragraphChildren.push(link(page.
|
|
519
|
+
// Add separator and parenthetical links (status label first if present)
|
|
520
|
+
paragraphChildren.push(text(statusLabel ? ` - (${statusLabel}, ` : ' - ('));
|
|
521
|
+
paragraphChildren.push(link(`#${page.slug}`, 'Outline'));
|
|
522
|
+
paragraphChildren.push(text(', '));
|
|
523
|
+
paragraphChildren.push(link(page.path, 'Contents'));
|
|
524
|
+
paragraphChildren.push(text(')'));
|
|
391
525
|
}
|
|
392
526
|
listItems.push({
|
|
393
527
|
type: 'listItem',
|
|
@@ -409,7 +543,7 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
409
543
|
const normalizedPath = typeof path === 'string' ? path.replace(/\\/g, '/') : undefined;
|
|
410
544
|
const trimmedPath = normalizedPath?.replace(/^(src\/app\/|app\/)/, '').replace(/\/page\.mdx$/, '');
|
|
411
545
|
const quotedPath = trimmedPath && /[()]/.test(trimmedPath) ? `"${trimmedPath}"` : trimmedPath;
|
|
412
|
-
const doNotEditComment = quotedPath ? `This
|
|
546
|
+
const doNotEditComment = quotedPath ? `This section is autogenerated, DO NOT EDIT AFTER THIS LINE, run: pnpm docs:validate ${quotedPath}` : 'This section is autogenerated, DO NOT EDIT AFTER THIS LINE';
|
|
413
547
|
children.push(comment(doNotEditComment));
|
|
414
548
|
|
|
415
549
|
// Add detailed page sections (non-editable)
|
|
@@ -618,6 +752,9 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
618
752
|
children.push(paragraph([link(page.path, 'Read more')]));
|
|
619
753
|
}
|
|
620
754
|
|
|
755
|
+
// Add metadata marker (inside wrapper component if provided)
|
|
756
|
+
children.push(comment('The above section is autogenerated, but the remainder of the file can be modified.'));
|
|
757
|
+
|
|
621
758
|
// Close wrapper component if provided
|
|
622
759
|
if (indexWrapperComponent) {
|
|
623
760
|
children.push({
|
|
@@ -625,20 +762,16 @@ export function metadataToMarkdownAst(data, options = {}) {
|
|
|
625
762
|
value: `</${indexWrapperComponent}>`
|
|
626
763
|
});
|
|
627
764
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
index: false,
|
|
639
|
-
},
|
|
640
|
-
};`;
|
|
641
|
-
}
|
|
765
|
+
const metadataObj = pageMetadata && Object.keys(pageMetadata).length > 0 ? pageMetadata : {
|
|
766
|
+
robots: {
|
|
767
|
+
index: false
|
|
768
|
+
},
|
|
769
|
+
other: {
|
|
770
|
+
audience: 'private'
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const typeAnnotation = "/** @type {import('@mui/internal-docs-infra/createSitemap/types').NextMetadata} */";
|
|
774
|
+
const metadataCode = `export const metadata =\n ${typeAnnotation} (${serializeJsValue(metadataObj, 1)});`;
|
|
642
775
|
// Output as raw MDX/JSX code (mdxjsEsm node type)
|
|
643
776
|
children.push({
|
|
644
777
|
type: 'mdxjsEsm',
|
|
@@ -682,7 +815,7 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
682
815
|
}
|
|
683
816
|
|
|
684
817
|
// Add editable section marker
|
|
685
|
-
const marker = editableMarker ?? "[//]: # 'This
|
|
818
|
+
const marker = editableMarker ?? "[//]: # 'This section is autogenerated, but the following list order, title, and [Tag]s can be modified, but nothing within the parentheses.'";
|
|
686
819
|
lines.push(marker);
|
|
687
820
|
lines.push('');
|
|
688
821
|
|
|
@@ -694,14 +827,15 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
694
827
|
|
|
695
828
|
// Add page list (editable section)
|
|
696
829
|
for (const page of pages) {
|
|
697
|
-
|
|
830
|
+
// List items use displayTitle when the user has overridden the title
|
|
831
|
+
const listTitle = page.displayTitle ?? page.title ?? page.slug;
|
|
698
832
|
|
|
699
833
|
// Check if this is a single-link entry (external link or no detail section)
|
|
700
834
|
const isSingleLink = page.skipDetailSection || false;
|
|
701
835
|
let line;
|
|
702
836
|
if (isSingleLink) {
|
|
703
837
|
// Format: - [Title](./path) [Tag1] [Tag2]
|
|
704
|
-
line = `- [${
|
|
838
|
+
line = `- [${listTitle}](${page.path})`;
|
|
705
839
|
|
|
706
840
|
// Add tags if present (directly after link)
|
|
707
841
|
if (page.tags && page.tags.length > 0) {
|
|
@@ -710,8 +844,9 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
710
844
|
}
|
|
711
845
|
}
|
|
712
846
|
} else {
|
|
713
|
-
// Format: -
|
|
714
|
-
|
|
847
|
+
// Format: - Title [Tag1] [Tag2] - (StatusLabel, [Outline](#slug), [Contents](./path))
|
|
848
|
+
const statusLabel = getStatusLabel(page);
|
|
849
|
+
line = `- ${listTitle}`;
|
|
715
850
|
|
|
716
851
|
// Add tags if present (directly after component name)
|
|
717
852
|
if (page.tags && page.tags.length > 0) {
|
|
@@ -720,8 +855,8 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
720
855
|
}
|
|
721
856
|
}
|
|
722
857
|
|
|
723
|
-
// Add separator and
|
|
724
|
-
line += ` - [
|
|
858
|
+
// Add separator and parenthetical links (status label first if present)
|
|
859
|
+
line += statusLabel ? ` - (${statusLabel}, [Outline](#${page.slug}), [Contents](${page.path}))` : ` - ([Outline](#${page.slug}), [Contents](${page.path}))`;
|
|
725
860
|
}
|
|
726
861
|
lines.push(line);
|
|
727
862
|
}
|
|
@@ -732,7 +867,7 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
732
867
|
const normalizedPath = typeof path === 'string' ? path.replace(/\\/g, '/') : undefined;
|
|
733
868
|
const trimmedPath = normalizedPath?.replace(/^(src\/app\/|app\/)/, '').replace(/\/page\.mdx$/, '');
|
|
734
869
|
const quotedPath = trimmedPath && /[()]/.test(trimmedPath) ? `"${trimmedPath}"` : trimmedPath;
|
|
735
|
-
const doNotEditMarker = quotedPath ? `[//]: # 'This
|
|
870
|
+
const doNotEditMarker = quotedPath ? `[//]: # 'This section is autogenerated, DO NOT EDIT AFTER THIS LINE, run: pnpm docs:validate ${quotedPath}'` : "[//]: # 'This section is autogenerated, DO NOT EDIT AFTER THIS LINE'";
|
|
736
871
|
lines.push(doNotEditMarker);
|
|
737
872
|
lines.push('');
|
|
738
873
|
|
|
@@ -856,24 +991,27 @@ export function metadataToMarkdown(data, options = {}) {
|
|
|
856
991
|
lines.push('');
|
|
857
992
|
}
|
|
858
993
|
|
|
994
|
+
// Add metadata marker (inside wrapper component if provided)
|
|
995
|
+
lines.push("[//]: # 'The above section is autogenerated, but the remainder of the file can be modified.'");
|
|
996
|
+
lines.push('');
|
|
997
|
+
|
|
859
998
|
// Close wrapper component if provided
|
|
860
999
|
if (indexWrapperComponent) {
|
|
861
1000
|
lines.push(`</${indexWrapperComponent}>`);
|
|
862
1001
|
lines.push('');
|
|
863
1002
|
}
|
|
864
|
-
|
|
865
|
-
// Add metadata export at the end
|
|
866
|
-
lines.push("[//]: # 'This file is autogenerated, but the following metadata can be modified.'");
|
|
867
|
-
lines.push('');
|
|
1003
|
+
const TYPE_ANNOTATION = "/** @type {import('@mui/internal-docs-infra/createSitemap/types').NextMetadata} */";
|
|
868
1004
|
if (pageMetadata && Object.keys(pageMetadata).length > 0) {
|
|
869
|
-
lines.push(`export const metadata
|
|
1005
|
+
lines.push(`export const metadata =\n ${TYPE_ANNOTATION} (${serializeJsValue(pageMetadata, 1)});`);
|
|
870
1006
|
} else {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1007
|
+
lines.push(`export const metadata =\n ${TYPE_ANNOTATION} (${serializeJsValue({
|
|
1008
|
+
robots: {
|
|
1009
|
+
index: false
|
|
1010
|
+
},
|
|
1011
|
+
other: {
|
|
1012
|
+
audience: 'private'
|
|
1013
|
+
}
|
|
1014
|
+
}, 1)});`);
|
|
877
1015
|
}
|
|
878
1016
|
lines.push('');
|
|
879
1017
|
|
|
@@ -891,6 +1029,9 @@ export async function markdownToMetadata(markdown) {
|
|
|
891
1029
|
let pageMetadata;
|
|
892
1030
|
let indexWrapperComponent;
|
|
893
1031
|
const pages = [];
|
|
1032
|
+
// Track the title from the editable list for each page (by slug)
|
|
1033
|
+
// Used to detect user overrides after the detail section is parsed
|
|
1034
|
+
const listTitles = new Map();
|
|
894
1035
|
let currentSection = 'header';
|
|
895
1036
|
let currentPage = null;
|
|
896
1037
|
|
|
@@ -899,7 +1040,7 @@ export async function markdownToMetadata(markdown) {
|
|
|
899
1040
|
// Track sections based on definition nodes (HTML-style comments)
|
|
900
1041
|
if (node.type === 'definition') {
|
|
901
1042
|
const defNode = node;
|
|
902
|
-
if (defNode.title?.includes('following list can be modified')) {
|
|
1043
|
+
if (defNode.title?.includes('following list can be modified') || defNode.title?.includes('following list order')) {
|
|
903
1044
|
currentSection = 'editable';
|
|
904
1045
|
return;
|
|
905
1046
|
}
|
|
@@ -907,7 +1048,9 @@ export async function markdownToMetadata(markdown) {
|
|
|
907
1048
|
currentSection = 'details';
|
|
908
1049
|
return;
|
|
909
1050
|
}
|
|
910
|
-
if (defNode.title?.includes('
|
|
1051
|
+
if (defNode.title?.includes('remainder of the file can be modified') ||
|
|
1052
|
+
// TODO: Remove this old marker check once all index files have been migrated to the new format.
|
|
1053
|
+
defNode.title?.includes('following metadata can be modified')) {
|
|
911
1054
|
currentSection = 'metadata';
|
|
912
1055
|
return;
|
|
913
1056
|
}
|
|
@@ -1003,49 +1146,98 @@ export async function markdownToMetadata(markdown) {
|
|
|
1003
1146
|
tags: tags.length > 0 ? tags : undefined,
|
|
1004
1147
|
skipDetailSection: true // Mark as external/single-link entry
|
|
1005
1148
|
});
|
|
1149
|
+
listTitles.set(slug, pageTitle);
|
|
1006
1150
|
} else if (links.length >= 2) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
foundSectionLink = true;
|
|
1022
|
-
continue;
|
|
1151
|
+
const firstChild = paragraphNode.children[0];
|
|
1152
|
+
if (firstChild && firstChild.type === 'text' && firstChild.value.includes(' - (')) {
|
|
1153
|
+
// Format: Title [Tags] - (Status, [Outline](#slug), [Contents](./path))
|
|
1154
|
+
const firstText = firstChild.value;
|
|
1155
|
+
const dashParenIndex = firstText.lastIndexOf(' - (');
|
|
1156
|
+
const titlePart = firstText.substring(0, dashParenIndex);
|
|
1157
|
+
|
|
1158
|
+
// Extract tags from title part
|
|
1159
|
+
const tags = [];
|
|
1160
|
+
const tagRegex = /\[([^\]]+)\]/g;
|
|
1161
|
+
let match = tagRegex.exec(titlePart);
|
|
1162
|
+
while (match !== null) {
|
|
1163
|
+
tags.push(match[1]);
|
|
1164
|
+
match = tagRegex.exec(titlePart);
|
|
1023
1165
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1166
|
+
const cleanTitle = titlePart.replace(/\s*\[[^\]]+\]/g, '').trim();
|
|
1167
|
+
|
|
1168
|
+
// Find Outline and Contents links
|
|
1169
|
+
const outlineLink = links.find(l => extractPlainTextFromNode(l) === 'Outline' || l.url.startsWith('#'));
|
|
1170
|
+
const contentsLink = links.find(l => extractPlainTextFromNode(l) === 'Contents' || !l.url.startsWith('#'));
|
|
1171
|
+
if (outlineLink && contentsLink) {
|
|
1172
|
+
const slug = outlineLink.url.replace('#', '');
|
|
1173
|
+
const pagePath = contentsLink.url;
|
|
1174
|
+
|
|
1175
|
+
// Parse status label from the text before the first link
|
|
1176
|
+
// Format: " - (Status, " or " - ("
|
|
1177
|
+
let parsedAudience;
|
|
1178
|
+
let isIndex = false;
|
|
1179
|
+
const afterDashParen = firstText.substring(dashParenIndex + 4); // after ' - ('
|
|
1180
|
+
if (afterDashParen.length > 0) {
|
|
1181
|
+
// Status label is the text before the first comma that precedes a link
|
|
1182
|
+
const commaIndex = afterDashParen.indexOf(',');
|
|
1183
|
+
if (commaIndex >= 0) {
|
|
1184
|
+
const statusStr = afterDashParen.substring(0, commaIndex).trim();
|
|
1185
|
+
if (statusStr.length > 0) {
|
|
1186
|
+
const status = parseStatusLabel(statusStr);
|
|
1187
|
+
parsedAudience = status.audience;
|
|
1188
|
+
isIndex = status.index;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
pages.push({
|
|
1193
|
+
slug,
|
|
1194
|
+
path: pagePath,
|
|
1195
|
+
title: cleanTitle,
|
|
1196
|
+
description: 'No description available',
|
|
1197
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
1198
|
+
audience: parsedAudience,
|
|
1199
|
+
index: isIndex || undefined
|
|
1200
|
+
});
|
|
1201
|
+
listTitles.set(slug, cleanTitle);
|
|
1027
1202
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1203
|
+
} else {
|
|
1204
|
+
// TODO: Remove this old format parsing once all index files have been migrated.
|
|
1205
|
+
// Old format: - [Title](#slug) [Tag1] [Tag2] - [Full Docs](./path/page.mdx)
|
|
1206
|
+
const sectionLink = links[0];
|
|
1207
|
+
const docsLink = links[1];
|
|
1208
|
+
const pageTitle = extractPlainTextFromNode(sectionLink);
|
|
1209
|
+
const slug = sectionLink.url.replace('#', '');
|
|
1210
|
+
const pagePath = docsLink.url;
|
|
1211
|
+
const tags = [];
|
|
1212
|
+
let foundSectionLink = false;
|
|
1213
|
+
let foundDocsLink = false;
|
|
1214
|
+
for (const child of paragraphNode.children) {
|
|
1215
|
+
if (child === sectionLink) {
|
|
1216
|
+
foundSectionLink = true;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (child === docsLink) {
|
|
1220
|
+
foundDocsLink = true;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
if (foundSectionLink && !foundDocsLink && child.type === 'text') {
|
|
1224
|
+
const tagRegex = /\[(\w+)\]/g;
|
|
1225
|
+
let match = tagRegex.exec(child.value);
|
|
1226
|
+
while (match !== null) {
|
|
1227
|
+
tags.push(match[1]);
|
|
1228
|
+
match = tagRegex.exec(child.value);
|
|
1229
|
+
}
|
|
1035
1230
|
}
|
|
1036
1231
|
}
|
|
1232
|
+
pages.push({
|
|
1233
|
+
slug,
|
|
1234
|
+
path: pagePath,
|
|
1235
|
+
title: pageTitle,
|
|
1236
|
+
description: 'No description available',
|
|
1237
|
+
tags: tags.length > 0 ? tags : undefined
|
|
1238
|
+
});
|
|
1239
|
+
listTitles.set(slug, pageTitle);
|
|
1037
1240
|
}
|
|
1038
|
-
|
|
1039
|
-
// Only extract slug, path, title, and tags from the editable list
|
|
1040
|
-
// The description will be filled in from the details section
|
|
1041
|
-
pages.push({
|
|
1042
|
-
slug,
|
|
1043
|
-
path,
|
|
1044
|
-
title: pageTitle,
|
|
1045
|
-
description: 'No description available',
|
|
1046
|
-
// Will be updated from details section
|
|
1047
|
-
tags: tags.length > 0 ? tags : undefined
|
|
1048
|
-
});
|
|
1049
1241
|
}
|
|
1050
1242
|
}
|
|
1051
1243
|
return;
|
|
@@ -1069,8 +1261,14 @@ export async function markdownToMetadata(markdown) {
|
|
|
1069
1261
|
}
|
|
1070
1262
|
}
|
|
1071
1263
|
const pageTitle = extractPlainTextFromNode(headingNode);
|
|
1072
|
-
// Find the page in the existing pages array by matching the title
|
|
1073
|
-
|
|
1264
|
+
// Find the page in the existing pages array by matching the title first,
|
|
1265
|
+
// then fall back to slug matching. Slug matching is needed when the user
|
|
1266
|
+
// has renamed the list item (so list title ≠ heading title).
|
|
1267
|
+
let existingPage = pages.find(p => p.title === pageTitle);
|
|
1268
|
+
if (!existingPage) {
|
|
1269
|
+
const derivedSlug = titleToSlug(pageTitle);
|
|
1270
|
+
existingPage = pages.find(p => p.slug === derivedSlug);
|
|
1271
|
+
}
|
|
1074
1272
|
if (existingPage) {
|
|
1075
1273
|
// Start updating this existing page
|
|
1076
1274
|
currentPage = {
|
|
@@ -1129,8 +1327,21 @@ export async function markdownToMetadata(markdown) {
|
|
|
1129
1327
|
}
|
|
1130
1328
|
}
|
|
1131
1329
|
|
|
1132
|
-
//
|
|
1330
|
+
// Extract path from Read more link for robust page matching.
|
|
1331
|
+
// If the current page's slug doesn't match any page in the list
|
|
1332
|
+
// (e.g., because titleToSlug(heading) ≠ actual slug), the path match
|
|
1333
|
+
// provides a final correction.
|
|
1133
1334
|
if (paragraphText.startsWith('[Read more]')) {
|
|
1335
|
+
if (currentPage) {
|
|
1336
|
+
const linkNode = paragraphNode.children.find(child => child.type === 'link');
|
|
1337
|
+
if (linkNode) {
|
|
1338
|
+
const readMorePath = linkNode.url;
|
|
1339
|
+
const matchedPage = pages.find(p => p.path === readMorePath);
|
|
1340
|
+
if (matchedPage && currentPage.slug !== matchedPage.slug) {
|
|
1341
|
+
currentPage.slug = matchedPage.slug;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1134
1345
|
return;
|
|
1135
1346
|
}
|
|
1136
1347
|
|
|
@@ -1160,8 +1371,8 @@ export async function markdownToMetadata(markdown) {
|
|
|
1160
1371
|
if (currentSection === 'metadata' && node.type === 'code') {
|
|
1161
1372
|
const codeNode = node;
|
|
1162
1373
|
const codeValue = codeNode.value;
|
|
1163
|
-
// Parse the export const metadata = { ... } statement
|
|
1164
|
-
const metadataMatch = codeValue.match(/export\s+const\s+metadata\s*=\s*(\{[\s\S]*\})
|
|
1374
|
+
// Parse the export const metadata = /** @type ... */ ({ ... }) or { ... } statement
|
|
1375
|
+
const metadataMatch = codeValue.match(/export\s+const\s+metadata\s*=\s*(?:\/\*\*[^*]*\*\/\s*\()?\s*(\{[\s\S]*\})\)?/);
|
|
1165
1376
|
if (metadataMatch) {
|
|
1166
1377
|
try {
|
|
1167
1378
|
// Use Function constructor to safely parse the object literal
|
|
@@ -1177,7 +1388,13 @@ export async function markdownToMetadata(markdown) {
|
|
|
1177
1388
|
|
|
1178
1389
|
// Also try to parse metadata from raw export statement in the markdown
|
|
1179
1390
|
// This handles MDX files where the export is not in a code block
|
|
1180
|
-
|
|
1391
|
+
// The regex allows optional HTML closing tags (e.g., </PagesIndex>) between the marker and the export
|
|
1392
|
+
// Greedy `[\s\S]*` ensures the capture extends to the LAST `\n\s*\}` (outermost closing brace)
|
|
1393
|
+
// rather than stopping at an inner closing brace. This is safe because the metadata
|
|
1394
|
+
// export is always the last statement in the file.
|
|
1395
|
+
const metadataExportMatch = markdown.match(/\[\/\/\]: # 'The above section is autogenerated, but the remainder of the file can be modified\.'[\s\S]*?export\s+const\s+metadata\s*=\s*(?:\/\*\*[^*]*\*\/\s*\()?\s*(\{[\s\S]*\n\s*\})\)?\s*;?/) ??
|
|
1396
|
+
// TODO: Remove this old marker match once all index files have been migrated to the new format.
|
|
1397
|
+
markdown.match(/\[\/\/\]: # 'This (?:section|file) is autogenerated, but the following metadata can be modified\.'[\s\S]*?export\s+const\s+metadata\s*=\s*(?:\/\*\*[^*]*\*\/\s*\()?\s*(\{[\s\S]*\n\s*\})\)?\s*;?/);
|
|
1181
1398
|
if (metadataExportMatch && !pageMetadata) {
|
|
1182
1399
|
try {
|
|
1183
1400
|
// eslint-disable-next-line no-new-func
|
|
@@ -1200,6 +1417,16 @@ export async function markdownToMetadata(markdown) {
|
|
|
1200
1417
|
}
|
|
1201
1418
|
}
|
|
1202
1419
|
}
|
|
1420
|
+
|
|
1421
|
+
// Detect title overrides: compare the list title with the heading title.
|
|
1422
|
+
// After the detail section is parsed, page.title reflects the heading's title.
|
|
1423
|
+
// If the list had a different title, the user renamed it — store as displayTitle.
|
|
1424
|
+
for (const page of pages) {
|
|
1425
|
+
const listTitle = listTitles.get(page.slug);
|
|
1426
|
+
if (listTitle && listTitle !== page.title) {
|
|
1427
|
+
page.displayTitle = listTitle;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1203
1430
|
if (!title) {
|
|
1204
1431
|
return null;
|
|
1205
1432
|
}
|
|
@@ -173,7 +173,9 @@ export async function syncPageIndex(options) {
|
|
|
173
173
|
|
|
174
174
|
// Step 1.5: Verify the file has the autogeneration marker if it exists
|
|
175
175
|
if (fileExists && existingContent) {
|
|
176
|
-
const hasMarker = existingContent.includes("[//]: # 'This
|
|
176
|
+
const hasMarker = existingContent.includes("[//]: # 'This section is autogenerated") ||
|
|
177
|
+
// TODO: Remove this old marker check once all index files have been migrated to the new format.
|
|
178
|
+
existingContent.includes("[//]: # 'This file is autogenerated");
|
|
177
179
|
if (!hasMarker) {
|
|
178
180
|
// File exists but doesn't have the autogeneration marker - skip updating it
|
|
179
181
|
return;
|
|
@@ -226,6 +228,7 @@ export async function syncPageIndex(options) {
|
|
|
226
228
|
}
|
|
227
229
|
let release;
|
|
228
230
|
let mergedPages = []; // Store merged pages for parent update
|
|
231
|
+
let currentPageMetadata; // Store the index's own metadata for parent update
|
|
229
232
|
|
|
230
233
|
try {
|
|
231
234
|
// Step 5: Acquire lock on the index file
|
|
@@ -258,6 +261,7 @@ export async function syncPageIndex(options) {
|
|
|
258
261
|
const parsed = await markdownToMetadata(currentMarkdown);
|
|
259
262
|
if (parsed) {
|
|
260
263
|
currentPages = parsed.pages;
|
|
264
|
+
currentPageMetadata = parsed.pageMetadata;
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -349,6 +353,16 @@ export async function syncPageIndex(options) {
|
|
|
349
353
|
description: 'No description available'
|
|
350
354
|
};
|
|
351
355
|
|
|
356
|
+
// Determine audience/index flags from the index page's own metadata
|
|
357
|
+
const audience = currentPageMetadata?.other?.audience;
|
|
358
|
+
if (audience) {
|
|
359
|
+
indexMetadata.audience = audience;
|
|
360
|
+
}
|
|
361
|
+
// An index page with child pages is always an index
|
|
362
|
+
if (mergedPages.length > 0) {
|
|
363
|
+
indexMetadata.index = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
352
366
|
// Convert child pages to sections format (no subsections, just page names)
|
|
353
367
|
// Use mergedPages which contains the complete merged state
|
|
354
368
|
// Skip single-link entries (external links) as they don't have detail sections
|
|
@@ -360,13 +374,10 @@ export async function syncPageIndex(options) {
|
|
|
360
374
|
continue;
|
|
361
375
|
}
|
|
362
376
|
sections[childPage.slug] = {
|
|
363
|
-
title: childPage.title
|
|
364
|
-
titleMarkdown:
|
|
365
|
-
type: 'text',
|
|
366
|
-
value: childPage.title
|
|
367
|
-
}] : [{
|
|
377
|
+
title: childPage.displayTitle ?? childPage.title ?? childPage.slug,
|
|
378
|
+
titleMarkdown: [{
|
|
368
379
|
type: 'text',
|
|
369
|
-
value: childPage.slug
|
|
380
|
+
value: childPage.displayTitle ?? childPage.title ?? childPage.slug
|
|
370
381
|
}],
|
|
371
382
|
children: {} // Don't include any subsections in parent index
|
|
372
383
|
};
|
|
@@ -230,7 +230,8 @@ function toPageMetadata(metadata, filePath, options = {}) {
|
|
|
230
230
|
keywords: metadata.keywords,
|
|
231
231
|
sections: metadata.sections,
|
|
232
232
|
embeddings: metadata.embeddings,
|
|
233
|
-
image: metadata.image
|
|
233
|
+
image: metadata.image,
|
|
234
|
+
audience: metadata.other?.audience
|
|
234
235
|
};
|
|
235
236
|
}
|
|
236
237
|
|
|
@@ -637,7 +638,9 @@ export const transformMarkdownMetadata = (options = {}) => {
|
|
|
637
638
|
}
|
|
638
639
|
|
|
639
640
|
// Check if this is an autogenerated index file
|
|
640
|
-
if (fileContent && fileContent.includes("[//]: # 'This
|
|
641
|
+
if (fileContent && (fileContent.includes("[//]: # 'This section is autogenerated") ||
|
|
642
|
+
// TODO: Remove this old marker check once all index files have been migrated to the new format.
|
|
643
|
+
fileContent.includes("[//]: # 'This file is autogenerated"))) {
|
|
641
644
|
// Parse the page list metadata from the markdown
|
|
642
645
|
const pagesMetadata = await markdownToMetadata(fileContent);
|
|
643
646
|
if (pagesMetadata) {
|
|
@@ -687,7 +690,7 @@ export const transformMarkdownMetadata = (options = {}) => {
|
|
|
687
690
|
title: pagesMetadata.title,
|
|
688
691
|
prefix,
|
|
689
692
|
pages: pagesMetadata.pages.map(page => ({
|
|
690
|
-
title: page.title,
|
|
693
|
+
title: page.displayTitle ?? page.title,
|
|
691
694
|
slug: page.slug,
|
|
692
695
|
path: page.path,
|
|
693
696
|
description: page.description,
|
|
@@ -697,6 +700,8 @@ export const transformMarkdownMetadata = (options = {}) => {
|
|
|
697
700
|
exports: page.exports,
|
|
698
701
|
tags: page.tags,
|
|
699
702
|
skipDetailSection: page.skipDetailSection,
|
|
703
|
+
audience: page.audience,
|
|
704
|
+
index: page.index,
|
|
700
705
|
image: page.image
|
|
701
706
|
}))
|
|
702
707
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PhrasingContent } from 'mdast';
|
|
2
|
+
import { Audience } from "../../createSitemap/types.mjs";
|
|
2
3
|
/**
|
|
3
4
|
* Plugin options for transformMarkdownMetadata
|
|
4
5
|
*/
|
|
@@ -101,4 +102,11 @@ export interface ExtractedMetadata {
|
|
|
101
102
|
url: string;
|
|
102
103
|
alt?: string;
|
|
103
104
|
};
|
|
105
|
+
robots?: {
|
|
106
|
+
index?: boolean;
|
|
107
|
+
};
|
|
108
|
+
other?: {
|
|
109
|
+
audience?: Audience;
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
};
|
|
104
112
|
}
|
package/useSearch/types.d.mts
CHANGED
|
@@ -89,6 +89,19 @@ export interface UseSearchOptions {
|
|
|
89
89
|
boost?: Partial<Record<string, number>>;
|
|
90
90
|
/** Include page categories in groups: "Overview Pages" vs "Pages" */
|
|
91
91
|
includeCategoryInGroup?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* When true, pages with `audience: 'private'` are included in the search index
|
|
94
|
+
* and default results. Use this for internal deployments where private pages
|
|
95
|
+
* should be discoverable.
|
|
96
|
+
*
|
|
97
|
+
* Typically driven by an environment variable:
|
|
98
|
+
* ```ts
|
|
99
|
+
* showPrivatePages: process.env.SHOW_PRIVATE_PAGES === 'true'
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @default false
|
|
103
|
+
*/
|
|
104
|
+
showPrivatePages?: boolean;
|
|
92
105
|
/**
|
|
93
106
|
* When true, excludes `sections` and `subsections` fields from page-type results.
|
|
94
107
|
* The individual section and subsection entries are still created.
|
package/useSearch/useSearch.mjs
CHANGED
|
@@ -308,7 +308,8 @@ export function useSearch(options) {
|
|
|
308
308
|
enableStemming = true,
|
|
309
309
|
generateSlug,
|
|
310
310
|
flattenPage = defaultFlattenPage,
|
|
311
|
-
formatResult = defaultFormatResult
|
|
311
|
+
formatResult = defaultFormatResult,
|
|
312
|
+
showPrivatePages = false
|
|
312
313
|
} = options;
|
|
313
314
|
const [index, setIndex] = React.useState(null);
|
|
314
315
|
const [defaultResults, setDefaultResults] = React.useState({
|
|
@@ -356,6 +357,10 @@ export function useSearch(options) {
|
|
|
356
357
|
let pageResultsCount = 0;
|
|
357
358
|
Object.entries(sitemap.data).forEach(([_sectionKey, sectionData]) => {
|
|
358
359
|
(sectionData.pages || []).forEach(page => {
|
|
360
|
+
// Skip private pages in public deployments
|
|
361
|
+
if (!showPrivatePages && page.audience === 'private') {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
359
364
|
const flattened = flattenPage(page, sectionData, options.includeCategoryInGroup || false, options.excludeSections, generateSlug);
|
|
360
365
|
pages.push(...flattened);
|
|
361
366
|
|
|
@@ -414,7 +419,7 @@ export function useSearch(options) {
|
|
|
414
419
|
setDefaultResults(defaultResultsValue);
|
|
415
420
|
setResults(defaultResultsValue);
|
|
416
421
|
})();
|
|
417
|
-
}, [sitemapImport, maxDefaultResults, flattenPage, generateSlug, enableStemming, options.includeCategoryInGroup, options.excludeSections]);
|
|
422
|
+
}, [sitemapImport, maxDefaultResults, flattenPage, generateSlug, enableStemming, options.includeCategoryInGroup, options.excludeSections, showPrivatePages]);
|
|
418
423
|
const search = React.useCallback(async (value, {
|
|
419
424
|
facets,
|
|
420
425
|
groupBy,
|
|
@@ -34,6 +34,8 @@ if ((process.env.CONTEXT === 'production' || process.env.CONTEXT === 'branch-dep
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
process.env.DEPLOY_ENV = DEPLOY_ENV;
|
|
37
|
+
const SHOW_PRIVATE_PAGES = String(process.env.DEPLOY_ENV !== 'production' && process.env.DEPLOY_ENV !== 'staging');
|
|
38
|
+
process.env.SHOW_PRIVATE_PAGES = SHOW_PRIVATE_PAGES;
|
|
37
39
|
export function withDeploymentConfig(nextConfig) {
|
|
38
40
|
return {
|
|
39
41
|
trailingSlash: true,
|
|
@@ -43,6 +45,7 @@ export function withDeploymentConfig(nextConfig) {
|
|
|
43
45
|
env: {
|
|
44
46
|
// production | staging | pull-request | development
|
|
45
47
|
DEPLOY_ENV,
|
|
48
|
+
SHOW_PRIVATE_PAGES,
|
|
46
49
|
...nextConfig.env,
|
|
47
50
|
// https://docs.netlify.com/configure-builds/environment-variables/#git-metadata
|
|
48
51
|
// reference ID (also known as "SHA" or "hash") of the commit we're building.
|