@ctcstack/ctcdocs-core 0.17.0 → 0.19.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/dist/corpus-structure.d.ts +4 -0
- package/dist/corpus-structure.js +5 -0
- package/dist/project-layout.d.ts +8 -0
- package/dist/project-layout.js +8 -0
- package/dist/published-markdown.d.ts +11 -0
- package/dist/published-markdown.js +19 -4
- package/dist/site-configuration.d.ts +14 -3
- package/dist/site-configuration.js +17 -1
- package/package.json +1 -1
- package/src/corpus-structure.ts +9 -0
- package/src/project-layout.ts +8 -0
- package/src/published-markdown.ts +26 -5
- package/src/site-configuration.ts +39 -4
|
@@ -19,10 +19,14 @@ export interface CorpusDocument {
|
|
|
19
19
|
* on its new chain to be confirmed.
|
|
20
20
|
*/
|
|
21
21
|
readonly publishedReaders?: '*' | readonly string[];
|
|
22
|
+
/** Set while such a move waits, as the manifest's `readersHeld` says. */
|
|
23
|
+
readonly readersHeld?: true;
|
|
22
24
|
/** The permanent short ID (ADR-022), the title and Drive's modified time. */
|
|
23
25
|
readonly shortId?: string;
|
|
24
26
|
readonly title?: string;
|
|
25
27
|
readonly modified?: string;
|
|
28
|
+
/** The Google Doc or PDF in Drive, as the page links it. */
|
|
29
|
+
readonly source?: string;
|
|
26
30
|
}
|
|
27
31
|
export interface CorpusStructure {
|
|
28
32
|
readonly rootFolderId: string | null;
|
package/dist/corpus-structure.js
CHANGED
|
@@ -65,6 +65,7 @@ export function parseCorpusStructure(manifest) {
|
|
|
65
65
|
const shortId = optional('shortId');
|
|
66
66
|
const title = optional('displayTitle');
|
|
67
67
|
const modified = optional('googleModifiedTime');
|
|
68
|
+
const source = optional('sourceUrl');
|
|
68
69
|
documents.set(id, {
|
|
69
70
|
id,
|
|
70
71
|
parentId: field(raw, 'googleParentId', path, true),
|
|
@@ -72,6 +73,10 @@ export function parseCorpusStructure(manifest) {
|
|
|
72
73
|
...(shortId ? { shortId } : {}),
|
|
73
74
|
...(title ? { title } : {}),
|
|
74
75
|
...(modified ? { modified } : {}),
|
|
76
|
+
...(source ? { source } : {}),
|
|
77
|
+
...(typeof record.readersHeld === 'object' && record.readersHeld !== null
|
|
78
|
+
? { readersHeld: true }
|
|
79
|
+
: {}),
|
|
75
80
|
...(published === '*' ||
|
|
76
81
|
(Array.isArray(published) &&
|
|
77
82
|
published.every((group) => typeof group === 'string'))
|
package/dist/project-layout.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const PROJECT_LAYOUT: {
|
|
|
26
26
|
readonly publicDirectory: "public";
|
|
27
27
|
readonly robotsFile: "public/robots.txt";
|
|
28
28
|
readonly headersFile: "public/_headers";
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
readonly documentsDirectory: "src/content/docs";
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
readonly generatedDocumentsDirectory: "src/content/docs/_generated";
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -68,6 +70,11 @@ export declare const PLATFORM_ROUTES: {
|
|
|
68
70
|
* docs/ADR/024-content-health-page.md.
|
|
69
71
|
*/
|
|
70
72
|
readonly contentHealth: "content-health";
|
|
73
|
+
/**
|
|
74
|
+
* Who may read each folder, for admins. See
|
|
75
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
76
|
+
*/
|
|
77
|
+
readonly accessReview: "access-review";
|
|
71
78
|
};
|
|
72
79
|
/**
|
|
73
80
|
* The platform's Workers, as a project's Wrangler configuration names them
|
|
@@ -100,6 +107,7 @@ export declare const ACCESS_CLASSES_ROUTE = "/_kb/classes";
|
|
|
100
107
|
export declare const PLATFORM_ROUTE_HREFS: {
|
|
101
108
|
readonly fullIndex: "/documents/";
|
|
102
109
|
readonly contentHealth: "/content-health/";
|
|
110
|
+
readonly accessReview: "/access-review/";
|
|
103
111
|
};
|
|
104
112
|
export declare const RESERVED_SLUGS: readonly string[];
|
|
105
113
|
/**
|
package/dist/project-layout.js
CHANGED
|
@@ -26,6 +26,8 @@ export const PROJECT_LAYOUT = {
|
|
|
26
26
|
publicDirectory: 'public',
|
|
27
27
|
robotsFile: 'public/robots.txt',
|
|
28
28
|
headersFile: 'public/_headers',
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
documentsDirectory: 'src/content/docs',
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
generatedDocumentsDirectory: 'src/content/docs/_generated',
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -68,6 +70,11 @@ export const PLATFORM_ROUTES = {
|
|
|
68
70
|
* docs/ADR/024-content-health-page.md.
|
|
69
71
|
*/
|
|
70
72
|
contentHealth: 'content-health',
|
|
73
|
+
/**
|
|
74
|
+
* Who may read each folder, for admins. See
|
|
75
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
76
|
+
*/
|
|
77
|
+
accessReview: 'access-review',
|
|
71
78
|
};
|
|
72
79
|
/**
|
|
73
80
|
* The platform's Workers, as a project's Wrangler configuration names them
|
|
@@ -100,6 +107,7 @@ export const ACCESS_CLASSES_ROUTE = '/_kb/classes';
|
|
|
100
107
|
export const PLATFORM_ROUTE_HREFS = {
|
|
101
108
|
fullIndex: `/${PLATFORM_ROUTES.fullIndex}/`,
|
|
102
109
|
contentHealth: `/${PLATFORM_ROUTES.contentHealth}/`,
|
|
110
|
+
accessReview: `/${PLATFORM_ROUTES.accessReview}/`,
|
|
103
111
|
};
|
|
104
112
|
/**
|
|
105
113
|
* Top-level addresses the platform serves beside the corpus: the Worker's
|
|
@@ -30,4 +30,15 @@ export declare function publishedFileUrl(googleFileId: string, file: string): st
|
|
|
30
30
|
* titles, which come from Drive names and may contain any of them.
|
|
31
31
|
*/
|
|
32
32
|
export declare function inlineMarkdown(value: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The page's Markdown without its front matter: its title as a heading and
|
|
35
|
+
* its body. It is what the MCP server stores, indexes and returns from
|
|
36
|
+
* `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
|
|
37
|
+
*/
|
|
38
|
+
export declare function publishedMarkdownBody(input: Pick<PublishedMarkdownInput, 'title' | 'ownershipHeader' | 'body' | 'stableSlugs' | 'permanentLinks'>): string;
|
|
39
|
+
/**
|
|
40
|
+
* The page's Markdown version (ADR-010): front matter, then
|
|
41
|
+
* `publishedMarkdownBody`. Each front matter value is one JSON string, so no
|
|
42
|
+
* value spans a line and the first `---` line after the opening one closes it.
|
|
43
|
+
*/
|
|
33
44
|
export declare function serializePublishedMarkdown(input: PublishedMarkdownInput): string;
|
|
@@ -98,8 +98,25 @@ export function inlineMarkdown(value) {
|
|
|
98
98
|
.trim()
|
|
99
99
|
.replace(/([\\`*_[\]<>#])/gu, '\\$1');
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
/**
|
|
102
|
+
* The page's Markdown without its front matter: its title as a heading and
|
|
103
|
+
* its body. It is what the MCP server stores, indexes and returns from
|
|
104
|
+
* `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
|
|
105
|
+
*/
|
|
106
|
+
export function publishedMarkdownBody(input) {
|
|
102
107
|
const body = rewriteInternalLinks(cleanBody(input.body, input.ownershipHeader), input.stableSlugs, input.permanentLinks);
|
|
108
|
+
return [
|
|
109
|
+
`# ${inlineMarkdown(input.title)}`,
|
|
110
|
+
'',
|
|
111
|
+
...(body ? [body, ''] : []),
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The page's Markdown version (ADR-010): front matter, then
|
|
116
|
+
* `publishedMarkdownBody`. Each front matter value is one JSON string, so no
|
|
117
|
+
* value spans a line and the first `---` line after the opening one closes it.
|
|
118
|
+
*/
|
|
119
|
+
export function serializePublishedMarkdown(input) {
|
|
103
120
|
return [
|
|
104
121
|
'---',
|
|
105
122
|
`title: ${yamlString(input.title)}`,
|
|
@@ -110,8 +127,6 @@ export function serializePublishedMarkdown(input) {
|
|
|
110
127
|
`content_hash: ${yamlString(input.contentHash)}`,
|
|
111
128
|
'---',
|
|
112
129
|
'',
|
|
113
|
-
|
|
114
|
-
'',
|
|
115
|
-
...(body ? [body, ''] : []),
|
|
130
|
+
publishedMarkdownBody(input),
|
|
116
131
|
].join('\n');
|
|
117
132
|
}
|
|
@@ -33,6 +33,14 @@ export interface HomeConfiguration {
|
|
|
33
33
|
* outgrown one page may prefer the home page to stay an entrance.
|
|
34
34
|
*/
|
|
35
35
|
readonly corpusIndex: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The address of the hand-authored page the home page sends a newcomer to,
|
|
38
|
+
* as a slug: `about` for `src/content/docs/about.md`. The link reads that
|
|
39
|
+
* page's own title, so renaming the page renames the link. Without it the
|
|
40
|
+
* home page offers no such link: the platform does not know what a
|
|
41
|
+
* deployment calls its introduction, or whether it has one.
|
|
42
|
+
*/
|
|
43
|
+
readonly start?: string;
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* Who a deployment is for.
|
|
@@ -152,13 +160,16 @@ export interface McpSearchConfiguration {
|
|
|
152
160
|
readonly chunks: number;
|
|
153
161
|
/**
|
|
154
162
|
* The vector similarity below which AI Search drops a chunk, 0 to 1. It
|
|
155
|
-
*
|
|
156
|
-
*
|
|
163
|
+
* drops only chunks vector search alone found, and AI Search ignores it
|
|
164
|
+
* while reranking is on (ADR-042), so it acts only with reranking off.
|
|
157
165
|
*/
|
|
158
166
|
readonly vectorThreshold: number;
|
|
159
167
|
/** Whether a keyword match needs every word of the query, or any. */
|
|
160
168
|
readonly keywordMatch: 'and' | 'or';
|
|
161
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Neighbouring chunks AI Search joins to each side of a match: 0 to 3. A
|
|
171
|
+
* chunk of AI Search's default size is already longer than a passage.
|
|
172
|
+
*/
|
|
162
173
|
readonly contextChunks: number;
|
|
163
174
|
readonly reranking: {
|
|
164
175
|
readonly enabled: boolean;
|
|
@@ -21,7 +21,7 @@ export const MCP_DEFAULTS = Object.freeze({
|
|
|
21
21
|
chunks: 50,
|
|
22
22
|
vectorThreshold: 0.2,
|
|
23
23
|
keywordMatch: 'or',
|
|
24
|
-
contextChunks:
|
|
24
|
+
contextChunks: 0,
|
|
25
25
|
reranking: Object.freeze({
|
|
26
26
|
enabled: true,
|
|
27
27
|
model: '@cf/baai/bge-reranker-base',
|
|
@@ -121,6 +121,21 @@ function flag(source, key, path) {
|
|
|
121
121
|
function optionalFlag(source, key, path, fallback) {
|
|
122
122
|
return source[key] === undefined ? fallback : flag(source, key, path);
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A page's address as Starlight derives it from a file under
|
|
126
|
+
* `src/content/docs`: lowercase segments joined by `/`, with no leading or
|
|
127
|
+
* trailing slash, so it is the same string the content collection knows.
|
|
128
|
+
*/
|
|
129
|
+
function optionalPageSlug(source, key, path) {
|
|
130
|
+
if (source[key] === undefined) {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
const value = text(source, key, path);
|
|
134
|
+
if (!/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/u.test(value)) {
|
|
135
|
+
fail(path, 'must be a page address such as "about", without slashes at either end');
|
|
136
|
+
}
|
|
137
|
+
return { [key]: value };
|
|
138
|
+
}
|
|
124
139
|
/**
|
|
125
140
|
* A list of distinct Unicode script names, as a `\p{Script=…}` property
|
|
126
141
|
* escape spells them. Each name is proved by compiling that escape, so a typo
|
|
@@ -405,6 +420,7 @@ export function parseSiteConfiguration(input) {
|
|
|
405
420
|
corpusIndex: optionalFlag(homeSource, 'corpusIndex', 'home.corpusIndex', true),
|
|
406
421
|
lede: text(homeSource, 'lede', 'home.lede'),
|
|
407
422
|
recentLimit: optionalCount(homeSource, 'recentLimit', 'home.recentLimit', 6),
|
|
423
|
+
...optionalPageSlug(homeSource, 'start', 'home.start'),
|
|
408
424
|
},
|
|
409
425
|
navigation: {
|
|
410
426
|
landingDocumentTitles: titleList(navigationSource, 'landingDocumentTitles', 'navigation.landingDocumentTitles'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctcstack/ctcdocs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Configuration schema, project layout, generated-path allowlist and the published Markdown form shared by the CTCDocs platform packages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ctcdocs",
|
package/src/corpus-structure.ts
CHANGED
|
@@ -33,10 +33,14 @@ export interface CorpusDocument {
|
|
|
33
33
|
* on its new chain to be confirmed.
|
|
34
34
|
*/
|
|
35
35
|
readonly publishedReaders?: '*' | readonly string[];
|
|
36
|
+
/** Set while such a move waits, as the manifest's `readersHeld` says. */
|
|
37
|
+
readonly readersHeld?: true;
|
|
36
38
|
/** The permanent short ID (ADR-022), the title and Drive's modified time. */
|
|
37
39
|
readonly shortId?: string;
|
|
38
40
|
readonly title?: string;
|
|
39
41
|
readonly modified?: string;
|
|
42
|
+
/** The Google Doc or PDF in Drive, as the page links it. */
|
|
43
|
+
readonly source?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export interface CorpusStructure {
|
|
@@ -114,6 +118,7 @@ export function parseCorpusStructure(manifest: unknown): CorpusStructure {
|
|
|
114
118
|
const shortId = optional('shortId');
|
|
115
119
|
const title = optional('displayTitle');
|
|
116
120
|
const modified = optional('googleModifiedTime');
|
|
121
|
+
const source = optional('sourceUrl');
|
|
117
122
|
documents.set(id, {
|
|
118
123
|
id,
|
|
119
124
|
parentId: field(raw, 'googleParentId', path, true),
|
|
@@ -121,6 +126,10 @@ export function parseCorpusStructure(manifest: unknown): CorpusStructure {
|
|
|
121
126
|
...(shortId ? { shortId } : {}),
|
|
122
127
|
...(title ? { title } : {}),
|
|
123
128
|
...(modified ? { modified } : {}),
|
|
129
|
+
...(source ? { source } : {}),
|
|
130
|
+
...(typeof record.readersHeld === 'object' && record.readersHeld !== null
|
|
131
|
+
? { readersHeld: true as const }
|
|
132
|
+
: {}),
|
|
124
133
|
...(published === '*' ||
|
|
125
134
|
(Array.isArray(published) &&
|
|
126
135
|
published.every((group) => typeof group === 'string'))
|
package/src/project-layout.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const PROJECT_LAYOUT = {
|
|
|
26
26
|
publicDirectory: 'public',
|
|
27
27
|
robotsFile: 'public/robots.txt',
|
|
28
28
|
headersFile: 'public/_headers',
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
documentsDirectory: 'src/content/docs',
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
generatedDocumentsDirectory: 'src/content/docs/_generated',
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -69,6 +71,11 @@ export const PLATFORM_ROUTES = {
|
|
|
69
71
|
* docs/ADR/024-content-health-page.md.
|
|
70
72
|
*/
|
|
71
73
|
contentHealth: 'content-health',
|
|
74
|
+
/**
|
|
75
|
+
* Who may read each folder, for admins. See
|
|
76
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
77
|
+
*/
|
|
78
|
+
accessReview: 'access-review',
|
|
72
79
|
} as const;
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -104,6 +111,7 @@ export const ACCESS_CLASSES_ROUTE = '/_kb/classes';
|
|
|
104
111
|
export const PLATFORM_ROUTE_HREFS = {
|
|
105
112
|
fullIndex: `/${PLATFORM_ROUTES.fullIndex}/`,
|
|
106
113
|
contentHealth: `/${PLATFORM_ROUTES.contentHealth}/`,
|
|
114
|
+
accessReview: `/${PLATFORM_ROUTES.accessReview}/`,
|
|
107
115
|
} as const;
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -153,14 +153,37 @@ export function inlineMarkdown(value: string): string {
|
|
|
153
153
|
.replace(/([\\`*_[\]<>#])/gu, '\\$1');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
/**
|
|
157
|
+
* The page's Markdown without its front matter: its title as a heading and
|
|
158
|
+
* its body. It is what the MCP server stores, indexes and returns from
|
|
159
|
+
* `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
|
|
160
|
+
*/
|
|
161
|
+
export function publishedMarkdownBody(
|
|
162
|
+
input: Pick<
|
|
163
|
+
PublishedMarkdownInput,
|
|
164
|
+
'title' | 'ownershipHeader' | 'body' | 'stableSlugs' | 'permanentLinks'
|
|
165
|
+
>,
|
|
158
166
|
): string {
|
|
159
167
|
const body = rewriteInternalLinks(
|
|
160
168
|
cleanBody(input.body, input.ownershipHeader),
|
|
161
169
|
input.stableSlugs,
|
|
162
170
|
input.permanentLinks,
|
|
163
171
|
);
|
|
172
|
+
return [
|
|
173
|
+
`# ${inlineMarkdown(input.title)}`,
|
|
174
|
+
'',
|
|
175
|
+
...(body ? [body, ''] : []),
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The page's Markdown version (ADR-010): front matter, then
|
|
181
|
+
* `publishedMarkdownBody`. Each front matter value is one JSON string, so no
|
|
182
|
+
* value spans a line and the first `---` line after the opening one closes it.
|
|
183
|
+
*/
|
|
184
|
+
export function serializePublishedMarkdown(
|
|
185
|
+
input: PublishedMarkdownInput,
|
|
186
|
+
): string {
|
|
164
187
|
return [
|
|
165
188
|
'---',
|
|
166
189
|
`title: ${yamlString(input.title)}`,
|
|
@@ -171,8 +194,6 @@ export function serializePublishedMarkdown(
|
|
|
171
194
|
`content_hash: ${yamlString(input.contentHash)}`,
|
|
172
195
|
'---',
|
|
173
196
|
'',
|
|
174
|
-
|
|
175
|
-
'',
|
|
176
|
-
...(body ? [body, ''] : []),
|
|
197
|
+
publishedMarkdownBody(input),
|
|
177
198
|
].join('\n');
|
|
178
199
|
}
|
|
@@ -55,6 +55,14 @@ export interface HomeConfiguration {
|
|
|
55
55
|
* outgrown one page may prefer the home page to stay an entrance.
|
|
56
56
|
*/
|
|
57
57
|
readonly corpusIndex: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The address of the hand-authored page the home page sends a newcomer to,
|
|
60
|
+
* as a slug: `about` for `src/content/docs/about.md`. The link reads that
|
|
61
|
+
* page's own title, so renaming the page renames the link. Without it the
|
|
62
|
+
* home page offers no such link: the platform does not know what a
|
|
63
|
+
* deployment calls its introduction, or whether it has one.
|
|
64
|
+
*/
|
|
65
|
+
readonly start?: string;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/**
|
|
@@ -183,13 +191,16 @@ export interface McpSearchConfiguration {
|
|
|
183
191
|
readonly chunks: number;
|
|
184
192
|
/**
|
|
185
193
|
* The vector similarity below which AI Search drops a chunk, 0 to 1. It
|
|
186
|
-
*
|
|
187
|
-
*
|
|
194
|
+
* drops only chunks vector search alone found, and AI Search ignores it
|
|
195
|
+
* while reranking is on (ADR-042), so it acts only with reranking off.
|
|
188
196
|
*/
|
|
189
197
|
readonly vectorThreshold: number;
|
|
190
198
|
/** Whether a keyword match needs every word of the query, or any. */
|
|
191
199
|
readonly keywordMatch: 'and' | 'or';
|
|
192
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Neighbouring chunks AI Search joins to each side of a match: 0 to 3. A
|
|
202
|
+
* chunk of AI Search's default size is already longer than a passage.
|
|
203
|
+
*/
|
|
193
204
|
readonly contextChunks: number;
|
|
194
205
|
readonly reranking: {
|
|
195
206
|
readonly enabled: boolean;
|
|
@@ -224,7 +235,7 @@ export const MCP_DEFAULTS: Omit<McpConfiguration, 'enabled'> = Object.freeze({
|
|
|
224
235
|
chunks: 50,
|
|
225
236
|
vectorThreshold: 0.2,
|
|
226
237
|
keywordMatch: 'or',
|
|
227
|
-
contextChunks:
|
|
238
|
+
contextChunks: 0,
|
|
228
239
|
reranking: Object.freeze({
|
|
229
240
|
enabled: true,
|
|
230
241
|
model: '@cf/baai/bge-reranker-base',
|
|
@@ -377,6 +388,29 @@ function optionalFlag(
|
|
|
377
388
|
return source[key] === undefined ? fallback : flag(source, key, path);
|
|
378
389
|
}
|
|
379
390
|
|
|
391
|
+
/**
|
|
392
|
+
* A page's address as Starlight derives it from a file under
|
|
393
|
+
* `src/content/docs`: lowercase segments joined by `/`, with no leading or
|
|
394
|
+
* trailing slash, so it is the same string the content collection knows.
|
|
395
|
+
*/
|
|
396
|
+
function optionalPageSlug(
|
|
397
|
+
source: Record<string, unknown>,
|
|
398
|
+
key: string,
|
|
399
|
+
path: string,
|
|
400
|
+
): Record<string, string> {
|
|
401
|
+
if (source[key] === undefined) {
|
|
402
|
+
return {};
|
|
403
|
+
}
|
|
404
|
+
const value = text(source, key, path);
|
|
405
|
+
if (!/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/u.test(value)) {
|
|
406
|
+
fail(
|
|
407
|
+
path,
|
|
408
|
+
'must be a page address such as "about", without slashes at either end',
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return { [key]: value };
|
|
412
|
+
}
|
|
413
|
+
|
|
380
414
|
/**
|
|
381
415
|
* A list of distinct Unicode script names, as a `\p{Script=…}` property
|
|
382
416
|
* escape spells them. Each name is proved by compiling that escape, so a typo
|
|
@@ -819,6 +853,7 @@ export function parseSiteConfiguration(input: unknown): SiteConfiguration {
|
|
|
819
853
|
'home.recentLimit',
|
|
820
854
|
6,
|
|
821
855
|
),
|
|
856
|
+
...optionalPageSlug(homeSource, 'start', 'home.start'),
|
|
822
857
|
},
|
|
823
858
|
navigation: {
|
|
824
859
|
landingDocumentTitles: titleList(
|