@writedocs/generator 0.6.0 → 0.7.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/bin/writedocs.js +12 -5
- package/package.json +90 -87
- package/src/cli/convert.js +44 -20
- package/src/cli/generate-api-pages.js +24 -2
- package/src/lib/content-check.js +41 -0
- package/src/lib/json-schema-descriptions.js +204 -0
- package/src/lib/json-schema.js +91 -0
- package/src/lib/link-check.js +23 -11
- package/src/lib/mintlify-convert.js +2 -2
- package/src/lib/writedocs-legacy-convert.js +496 -0
- package/writedocs.schema.json +2291 -0
package/bin/writedocs.js
CHANGED
|
@@ -238,20 +238,27 @@ program
|
|
|
238
238
|
program
|
|
239
239
|
.command('convert')
|
|
240
240
|
.description("Convert another docs tool's config into writedocs.json")
|
|
241
|
-
.argument('[dir]', 'project directory (contains docs.json)', '.')
|
|
241
|
+
.argument('[dir]', 'project directory (contains docs.json or config.json)', '.')
|
|
242
242
|
.option('--mintlify', "convert a Mintlify project's docs.json")
|
|
243
243
|
.option('--docs.json', 'same as --mintlify')
|
|
244
|
+
.option('--writedocs', "convert a project from the previous writedocs (its config.json)")
|
|
245
|
+
.option('--config.json', 'same as --writedocs')
|
|
244
246
|
.option('--force', 'overwrite an existing writedocs.json')
|
|
245
247
|
.option('--dry-run', 'print the converted writedocs.json instead of writing it')
|
|
246
248
|
.action(async (dir, options) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
log.error(
|
|
249
|
+
const mintlify = options.mintlify || options.docsJson;
|
|
250
|
+
const legacy = options.writedocs || options.configJson;
|
|
251
|
+
if (mintlify === legacy) {
|
|
252
|
+
log.error(
|
|
253
|
+
mintlify
|
|
254
|
+
? 'Pick one: --mintlify or --writedocs.'
|
|
255
|
+
: 'Say what to convert from: writedocs convert --mintlify [dir], or --writedocs for the previous writedocs.'
|
|
256
|
+
);
|
|
251
257
|
throw new CliExit(1);
|
|
252
258
|
}
|
|
253
259
|
const { runConvert } = await import('../src/cli/convert.js');
|
|
254
260
|
await runConvert({
|
|
261
|
+
source: legacy ? 'writedocs' : 'mintlify',
|
|
255
262
|
contentDir: path.resolve(process.cwd(), dir),
|
|
256
263
|
force: Boolean(options.force),
|
|
257
264
|
dryRun: Boolean(options.dryRun),
|
package/package.json
CHANGED
|
@@ -1,88 +1,91 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@writedocs/generator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"writedocs": "./bin/writedocs.js"
|
|
8
|
-
},
|
|
9
|
-
"exports": {
|
|
10
|
-
"./components": "./src/components/index.ts",
|
|
11
|
-
"./config-schema": "./src/lib/config-schema.js"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"build
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"url": "https://github.com/writedocs/writedocs
|
|
40
|
-
},
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"@
|
|
51
|
-
"@astrojs/
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
-
"@iconify-json/
|
|
56
|
-
"@iconify-json/
|
|
57
|
-
"@iconify-json/
|
|
58
|
-
"@iconify-json/
|
|
59
|
-
"@iconify-json/
|
|
60
|
-
"@iconify-json/
|
|
61
|
-
"@iconify-json/
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"@
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
},
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@writedocs/generator",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"writedocs": "./bin/writedocs.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./components": "./src/components/index.ts",
|
|
11
|
+
"./config-schema": "./src/lib/config-schema.js",
|
|
12
|
+
"./writedocs.schema.json": "./writedocs.schema.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"astro.config.mjs",
|
|
18
|
+
"writedocs.schema.json"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "node bin/writedocs.js dev",
|
|
22
|
+
"build": "node bin/writedocs.js build",
|
|
23
|
+
"init": "node bin/writedocs.js init",
|
|
24
|
+
"build:schema": "esbuild src/lib/config-schema.ts --format=esm --target=node22 --outfile=src/lib/config-schema.js",
|
|
25
|
+
"prepare": "npm run build:schema && npm run build:json-schema",
|
|
26
|
+
"test": "node --test \"scripts/*.test.js\"",
|
|
27
|
+
"build:json-schema": "node scripts/build-json-schema.mjs"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"docs",
|
|
31
|
+
"documentation",
|
|
32
|
+
"static-site-generator",
|
|
33
|
+
"mdx",
|
|
34
|
+
"astro"
|
|
35
|
+
],
|
|
36
|
+
"homepage": "https://github.com/writedocs/writedocs#readme",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/writedocs/writedocs.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/writedocs/writedocs/issues"
|
|
43
|
+
},
|
|
44
|
+
"author": "Gabriel Raeder",
|
|
45
|
+
"license": "ISC",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
51
|
+
"@astrojs/markdown-remark": "7.2.4",
|
|
52
|
+
"@astrojs/mdx": "^7.0.5",
|
|
53
|
+
"@astrojs/react": "^6.0.2",
|
|
54
|
+
"@astrojs/sitemap": "^3.7.3",
|
|
55
|
+
"@iconify-json/bi": "^1.2.7",
|
|
56
|
+
"@iconify-json/fa6-brands": "^1.2.6",
|
|
57
|
+
"@iconify-json/fa6-solid": "^1.2.4",
|
|
58
|
+
"@iconify-json/heroicons": "^1.2.3",
|
|
59
|
+
"@iconify-json/ion": "^1.2.7",
|
|
60
|
+
"@iconify-json/lucide": "^1.2.116",
|
|
61
|
+
"@iconify-json/mdi": "^1.2.3",
|
|
62
|
+
"@iconify-json/ri": "^1.2.10",
|
|
63
|
+
"@iconify-json/simple-icons": "^1.2.89",
|
|
64
|
+
"@iconify-json/tabler": "^1.2.35",
|
|
65
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
66
|
+
"@shikijs/transformers": "^4.3.1",
|
|
67
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
68
|
+
"astro": "7.2.9",
|
|
69
|
+
"astro-icon": "^1.1.5",
|
|
70
|
+
"commander": "^15.0.0",
|
|
71
|
+
"dotenv": "^17.4.2",
|
|
72
|
+
"gray-matter": "^4.0.3",
|
|
73
|
+
"jsonc-parser": "3.3.1",
|
|
74
|
+
"katex": "^0.16.47",
|
|
75
|
+
"mermaid": "^11.16.0",
|
|
76
|
+
"pagefind": "^1.5.2",
|
|
77
|
+
"react": "^19.2.8",
|
|
78
|
+
"react-dom": "^19.2.8",
|
|
79
|
+
"rehype-katex": "^7.0.1",
|
|
80
|
+
"remark-math": "^6.0.0",
|
|
81
|
+
"shiki": "^4.3.1",
|
|
82
|
+
"tailwindcss": "^4.3.2",
|
|
83
|
+
"zod": "^4.4.3"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=22.12.0"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"esbuild": "0.28.2"
|
|
90
|
+
}
|
|
88
91
|
}
|
package/src/cli/convert.js
CHANGED
|
@@ -1,24 +1,48 @@
|
|
|
1
|
-
// `writedocs convert --mintlify [dir]` - turns a
|
|
2
|
-
// docs.json
|
|
3
|
-
// the same
|
|
4
|
-
//
|
|
1
|
+
// `writedocs convert --mintlify [dir]` / `--writedocs [dir]` - turns a
|
|
2
|
+
// Mintlify project's docs.json, or a config.json from the previous writedocs,
|
|
3
|
+
// into writedocs.json, in the same folder, then checks the pages the same way
|
|
4
|
+
// `writedocs validate` does. The conversions themselves are
|
|
5
|
+
// lib/mintlify-convert.js and lib/writedocs-legacy-convert.js; this file is
|
|
6
|
+
// the CLI around them.
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { loadMintlifyConfig, convertMintlifyConfig, formatNotes } from '../lib/mintlify-convert.js';
|
|
10
|
+
import { loadLegacyConfig, convertLegacyConfig } from '../lib/writedocs-legacy-convert.js';
|
|
8
11
|
import { validateDocsConfig, formatValidationIssuesDetailed } from '../lib/config-schema.js';
|
|
9
12
|
import { checkContent, formatContentIssues } from '../lib/content-check.js';
|
|
10
13
|
import { log, step, plural, color, displayPath, CliExit } from './output.js';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const SOURCES = {
|
|
16
|
+
mintlify: {
|
|
17
|
+
file: 'docs.json',
|
|
18
|
+
load: loadMintlifyConfig,
|
|
19
|
+
convert: async (input) => convertMintlifyConfig(input),
|
|
20
|
+
missing(contentDir) {
|
|
21
|
+
if (fs.existsSync(path.join(contentDir, 'mint.json'))) {
|
|
22
|
+
log.error("Found mint.json, Mintlify's older config format.");
|
|
23
|
+
log.detail(color.dim(`Run "npx mint upgrade" in ${contentDir} to turn it into docs.json, then run this again.`));
|
|
24
|
+
} else {
|
|
25
|
+
log.error(`No docs.json found in ${contentDir}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
domainHint: '(Mintlify sets this in its dashboard, not docs.json.)',
|
|
29
|
+
},
|
|
30
|
+
writedocs: {
|
|
31
|
+
file: 'config.json',
|
|
32
|
+
load: loadLegacyConfig,
|
|
33
|
+
convert: (input, contentDir) => convertLegacyConfig(input, contentDir),
|
|
34
|
+
missing(contentDir) {
|
|
35
|
+
log.error(`No config.json found in ${contentDir}`);
|
|
36
|
+
},
|
|
37
|
+
domainHint: '(config.json had no field for it.)',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function runConvert({ source = 'mintlify', contentDir, force = false, dryRun = false }) {
|
|
42
|
+
const from = SOURCES[source];
|
|
43
|
+
const inputPath = path.join(contentDir, from.file);
|
|
44
|
+
if (!fs.existsSync(inputPath)) {
|
|
45
|
+
from.missing(contentDir);
|
|
22
46
|
throw new CliExit(1);
|
|
23
47
|
}
|
|
24
48
|
const outPath = path.join(contentDir, 'writedocs.json');
|
|
@@ -28,15 +52,15 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
28
52
|
throw new CliExit(1);
|
|
29
53
|
}
|
|
30
54
|
|
|
31
|
-
let
|
|
55
|
+
let input;
|
|
32
56
|
try {
|
|
33
|
-
|
|
57
|
+
input = from.load(inputPath);
|
|
34
58
|
} catch (err) {
|
|
35
|
-
log.error(`Couldn't read ${displayPath(
|
|
59
|
+
log.error(`Couldn't read ${displayPath(inputPath)}: ${err.message}`);
|
|
36
60
|
throw new CliExit(1);
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
const { config, notes } =
|
|
63
|
+
const { config, notes } = await from.convert(input, contentDir);
|
|
40
64
|
const text = `${JSON.stringify(config, null, 2)}\n`;
|
|
41
65
|
|
|
42
66
|
// The result must pass writedocs' own schema - if it doesn't, that's a
|
|
@@ -54,7 +78,7 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
54
78
|
log.line(text);
|
|
55
79
|
} else {
|
|
56
80
|
fs.writeFileSync(outPath, text);
|
|
57
|
-
log.success(`Converted ${
|
|
81
|
+
log.success(`Converted ${from.file} to ${color.bold(displayPath(outPath))}`);
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
if (notes.length) {
|
|
@@ -87,7 +111,7 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
87
111
|
else log.success(summary);
|
|
88
112
|
if (!config.domain) {
|
|
89
113
|
log.info(
|
|
90
|
-
|
|
114
|
+
`Next: set "domain" in writedocs.json to your site's URL - it turns on sitemap.xml and absolute links for social previews. ${from.domainHint}`
|
|
91
115
|
);
|
|
92
116
|
}
|
|
93
117
|
}
|
|
@@ -168,6 +168,28 @@ function buildOperations(spec) {
|
|
|
168
168
|
return operations;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** An operation as JSON. A recursive schema - a tree whose nodes contain
|
|
172
|
+
* nodes - dereferences into a cycle (SwaggerParser.dereference() resolves
|
|
173
|
+
* the $ref to the very same object), which JSON can't hold and the API
|
|
174
|
+
* pages couldn't render anyway. Where a schema would contain itself, a
|
|
175
|
+
* short stand-in takes its place: its type and title, marked
|
|
176
|
+
* x-circular. Shared, non-recursive schemas are written in full each time,
|
|
177
|
+
* as before. */
|
|
178
|
+
function operationJson(operation) {
|
|
179
|
+
const ancestors = [];
|
|
180
|
+
const cut = (node) => {
|
|
181
|
+
if (node === null || typeof node !== 'object') return node;
|
|
182
|
+
if (ancestors.includes(node)) {
|
|
183
|
+
return { ...(node.type ? { type: node.type } : {}), ...(node.title ? { title: node.title } : {}), 'x-circular': true };
|
|
184
|
+
}
|
|
185
|
+
ancestors.push(node);
|
|
186
|
+
const out = Array.isArray(node) ? node.map(cut) : Object.fromEntries(Object.entries(node).map(([k, v]) => [k, cut(v)]));
|
|
187
|
+
ancestors.pop();
|
|
188
|
+
return out;
|
|
189
|
+
};
|
|
190
|
+
return JSON.stringify(cut(operation), null, 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
function rmrf(dir) {
|
|
172
194
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
173
195
|
}
|
|
@@ -233,7 +255,7 @@ async function generateApiPagesForGroup({ contentDir, generatedDocsDir, group, o
|
|
|
233
255
|
manifest.push({ slug, method: operation.method, path: operation.path, tags: operation.tags, title, generated });
|
|
234
256
|
|
|
235
257
|
const opFile = path.join(openapiOutDir, 'operations', operationFileName(operation.method, operation.path));
|
|
236
|
-
fs.writeFileSync(opFile,
|
|
258
|
+
fs.writeFileSync(opFile, operationJson(operation));
|
|
237
259
|
}
|
|
238
260
|
|
|
239
261
|
fs.writeFileSync(path.join(openapiOutDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
@@ -355,7 +377,7 @@ async function parsePageSpecs({ contentDir, openapiOutDir, groupSpecs, summary }
|
|
|
355
377
|
const outDir = path.join(openapiOutDir, '_pages', specDirName(spec), 'operations');
|
|
356
378
|
fs.mkdirSync(outDir, { recursive: true });
|
|
357
379
|
for (const operation of operations) {
|
|
358
|
-
fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)),
|
|
380
|
+
fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)), operationJson(operation));
|
|
359
381
|
}
|
|
360
382
|
summary.pageSpecs.push({ spec, operations: operations.length });
|
|
361
383
|
}
|
package/src/lib/content-check.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// - writedocs.json's navigation lists a page that doesn't exist
|
|
11
11
|
// - a redirect is a pattern (`/old/:slug`) rather than one exact path
|
|
12
12
|
// - a Markdown image with a relative path names a file that doesn't exist
|
|
13
|
+
// - an import the build can't resolve: a Docusaurus path (@site/...), or a
|
|
14
|
+
// relative or /snippets/ import of a file that isn't there
|
|
13
15
|
// - an OpenAPI group's spec is missing or doesn't parse, or two groups
|
|
14
16
|
// share one `openapi.path`
|
|
15
17
|
// Warnings - the build succeeds, but not as written:
|
|
@@ -129,6 +131,45 @@ async function checkPage(contentDir, rel, errors, warnings, openapiRefs) {
|
|
|
129
131
|
);
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
134
|
+
// Imports the build can't resolve fail it: Docusaurus' path aliases
|
|
135
|
+
// (common in pages from the previous writedocs), and a relative or
|
|
136
|
+
// /snippets/ import of a file that isn't there. Package imports aren't
|
|
137
|
+
// checked - whether they resolve depends on what's installed.
|
|
138
|
+
visit(tree, 'mdxjsEsm', (node) => {
|
|
139
|
+
const code = String(node.value ?? '');
|
|
140
|
+
for (const m of code.matchAll(/\b(?:import|export)\b[^'";]*?\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]/g)) {
|
|
141
|
+
const spec = m[1] ?? m[2];
|
|
142
|
+
const at = node.position?.start?.line ? node.position.start.line + lineOffset + code.slice(0, m.index).split('\n').length - 1 : undefined;
|
|
143
|
+
if (/^@(site|theme|docusaurus|generated)(\/|$)/.test(spec)) {
|
|
144
|
+
errors.push(
|
|
145
|
+
issue(
|
|
146
|
+
rel,
|
|
147
|
+
at,
|
|
148
|
+
`Imports "${spec}", a Docusaurus path - the build fails on it.`,
|
|
149
|
+
/^@site\/src\/components\/?$/.test(spec)
|
|
150
|
+
? "writedocs' components (Card, Callout, Tabs, Accordion, ...) need no import - remove this line."
|
|
151
|
+
: spec.startsWith('@site/src/components/')
|
|
152
|
+
? 'A custom component of the old site - move it into a snippet (snippets/), or remove it and what uses it.'
|
|
153
|
+
: 'Remove the import, and what uses it.'
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let target = null;
|
|
159
|
+
if (spec.startsWith('./') || spec.startsWith('../')) target = path.resolve(path.dirname(path.join(contentDir, rel)), spec);
|
|
160
|
+
else if (spec.startsWith('/snippets/')) target = path.join(contentDir, spec);
|
|
161
|
+
if (!target) continue;
|
|
162
|
+
const exists = ['', '.mdx', '.md', '.js', '.jsx', '.ts', '.tsx', '.json'].some((ext) => {
|
|
163
|
+
try {
|
|
164
|
+
return fs.statSync(target + ext).isFile();
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
if (!exists) errors.push(issue(rel, at, `Imports ${spec}, which doesn't exist - the build fails on it.`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
132
173
|
// A Markdown image with a relative path is imported by the build (Astro's
|
|
133
174
|
// image pipeline), so a missing file fails the whole build.
|
|
134
175
|
visit(tree, 'image', (node) => {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// The editor help text for writedocs.schema.json (see lib/json-schema.js):
|
|
2
|
+
// one entry per field of writedocs.json, keyed by its path - `a.b.c`, with
|
|
3
|
+
// `[]` for an array's items. Fields inside the navigation's recursive types
|
|
4
|
+
// are keyed by the type's name instead (`Tab.icon`, `NavigationItem.group`).
|
|
5
|
+
//
|
|
6
|
+
// scripts/json-schema.test.js fails when a field has no entry here, or an
|
|
7
|
+
// entry names a field that doesn't exist - so a new field in
|
|
8
|
+
// config-schema.ts needs its description added here too.
|
|
9
|
+
|
|
10
|
+
const CHILDREN = {
|
|
11
|
+
pages: 'The pages and groups in this section of the sidebar.',
|
|
12
|
+
tabs: 'Tabs inside this one.',
|
|
13
|
+
versions: 'Versions inside this one - a version picker in the sidebar.',
|
|
14
|
+
languages: 'Languages inside this one - a language picker.',
|
|
15
|
+
dropdowns: 'Dropdowns inside this one - a menu of sections in the sidebar.',
|
|
16
|
+
products: 'Products inside this one - a product picker.',
|
|
17
|
+
href: 'Makes this a link to another address instead of a section with pages of its own.',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CONTAINERS = {
|
|
21
|
+
Tab: {
|
|
22
|
+
tab: 'The tab\'s name, shown in the topbar.',
|
|
23
|
+
icon: 'Icon next to the name: a Lucide name ("book-open"), "collection:name" ("mdi:server"), an emoji, or an image path.',
|
|
24
|
+
},
|
|
25
|
+
Version: {
|
|
26
|
+
version: 'The version\'s name, like "v2".',
|
|
27
|
+
label: 'Text shown in the version picker instead of `version`.',
|
|
28
|
+
tag: 'A short badge next to the version, like "Latest" or "Deprecated".',
|
|
29
|
+
default: 'Open this version first. Without it, the first version is the default.',
|
|
30
|
+
},
|
|
31
|
+
Language: {
|
|
32
|
+
language: 'The language code, like "en" or "pt-BR".',
|
|
33
|
+
label: 'Text shown in the language picker instead of `language`, like "Português".',
|
|
34
|
+
},
|
|
35
|
+
Dropdown: {
|
|
36
|
+
dropdown: 'The dropdown\'s name.',
|
|
37
|
+
icon: 'Icon next to the name: a Lucide name, "collection:name", an emoji, or an image path.',
|
|
38
|
+
},
|
|
39
|
+
Product: {
|
|
40
|
+
product: 'The product\'s name, shown in the product picker.',
|
|
41
|
+
icon: 'Icon next to the name: a Lucide name, "collection:name", an emoji, or an image path.',
|
|
42
|
+
description: 'One line under the name in the product picker.',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const containerEntries = Object.entries(CONTAINERS).flatMap(([type, fields]) => [
|
|
47
|
+
...Object.entries(fields).map(([field, text]) => [`${type}.${field}`, text]),
|
|
48
|
+
...Object.entries(CHILDREN).map(([field, text]) => [`${type}.${field}`, text]),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const logo = (where) => ({
|
|
52
|
+
[where]: 'The logo: one image path for both color modes, or `{ light, dark, label }` with an image per mode.',
|
|
53
|
+
[`${where}.light`]: 'Logo image shown in light mode.',
|
|
54
|
+
[`${where}.dark`]: 'Logo image shown in dark mode.',
|
|
55
|
+
[`${where}.label`]: 'Text shown next to the logo. No text is shown by default - a logo is usually already a wordmark.',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const font = (where, what) => ({
|
|
59
|
+
[`${where}.family`]: `Font family for ${what}. A Google Fonts name loads automatically; add \`source\` for a font file instead.`,
|
|
60
|
+
[`${where}.weight`]: `Font weight for ${what}, like 400 or 700.`,
|
|
61
|
+
[`${where}.source`]: 'Path or URL of a font file to load, instead of Google Fonts.',
|
|
62
|
+
[`${where}.format`]: 'Format of the `source` font file.',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const link = (where) => ({
|
|
66
|
+
[`${where}.label`]: 'Link text. A link needs a label, an icon, or both.',
|
|
67
|
+
[`${where}.href`]: 'Where the link goes - a site path like "/docs/intro/" or a full URL.',
|
|
68
|
+
[`${where}.icon`]: 'Icon shown with the link: a Lucide name, "collection:name", an emoji, or an image path.',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const script = (where) => ({
|
|
72
|
+
[`${where}.src`]: 'URL of the script to load. Use either `src` or `content`, not both.',
|
|
73
|
+
[`${where}.content`]: 'The script\'s code, inline. Use either `src` or `content`, not both.',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const ROOT_DESCRIPTION = 'Configuration of a writedocs site: its name, look, navigation and integrations.';
|
|
77
|
+
|
|
78
|
+
export const TYPE_DESCRIPTIONS = {
|
|
79
|
+
NavigationItem: 'A page (its path from the project folder, without the extension), a group of pages, or a link.',
|
|
80
|
+
Tab: 'A tab in the topbar, with its own sidebar.',
|
|
81
|
+
Version: 'A version of the docs, chosen from a version picker.',
|
|
82
|
+
Language: 'A language of the docs, chosen from a language picker.',
|
|
83
|
+
Dropdown: 'A section chosen from a dropdown menu.',
|
|
84
|
+
Product: 'A product, chosen from a product picker.',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const DESCRIPTIONS = {
|
|
88
|
+
$schema: 'The JSON Schema this file follows - for editor autocompletion and checks.',
|
|
89
|
+
name: 'Required. The site\'s name - shown in the browser tab ("Page title · name") and as the topbar text when there\'s no logo.',
|
|
90
|
+
description: 'Description used for pages that don\'t set their own `description` in frontmatter.',
|
|
91
|
+
|
|
92
|
+
styles: 'Colors, logo, favicon, fonts, code block themes, navbar and background.',
|
|
93
|
+
'styles.colors': 'The site\'s colors.',
|
|
94
|
+
'styles.colors.primary': 'Main color: links, buttons, the active page in the sidebar. Default "#6366f1".',
|
|
95
|
+
'styles.colors.text': 'Body text color in light mode. Default "#0f172a".',
|
|
96
|
+
'styles.colors.dark': 'Dark-mode colors. Anything left out uses the light-mode value.',
|
|
97
|
+
'styles.colors.dark.primary': 'Main color in dark mode. Pick a lighter one if `primary` is dark - links use it on a dark background.',
|
|
98
|
+
'styles.colors.dark.text': 'Body text color in dark mode. Default "#e2e8f0".',
|
|
99
|
+
...logo('styles.logo'),
|
|
100
|
+
'styles.favicon': 'Favicon image path.',
|
|
101
|
+
'styles.fonts': 'Fonts. Default: Inter. `heading` and `body` override just that part; anything they leave out comes from the top-level fields.',
|
|
102
|
+
...font('styles.fonts', 'all text'),
|
|
103
|
+
'styles.fonts.heading': 'Font for headings only.',
|
|
104
|
+
...font('styles.fonts.heading', 'headings'),
|
|
105
|
+
'styles.fonts.body': 'Font for body text only.',
|
|
106
|
+
...font('styles.fonts.body', 'body text'),
|
|
107
|
+
'styles.codeblocks': 'Syntax-highlighting themes for code blocks and the API playground.',
|
|
108
|
+
'styles.codeblocks.light': 'Shiki theme name for light mode (https://shiki.style/themes). Default "github-light".',
|
|
109
|
+
'styles.codeblocks.dark': 'Shiki theme name for dark mode. Default "github-dark".',
|
|
110
|
+
'styles.codeblocks.langAlias': 'Extra language names for code blocks, mapped to a language Shiki knows - like { "curl": "bash" }.',
|
|
111
|
+
'styles.navbar': 'The topbar\'s own background. Without it, the topbar matches the page background. Its text turns black or white automatically.',
|
|
112
|
+
'styles.navbar.light': 'Topbar in light mode: a background color, or `{ background, accent }`.',
|
|
113
|
+
'styles.navbar.light.background': 'Topbar background color in light mode.',
|
|
114
|
+
'styles.navbar.light.accent': 'Color of the active tab and hover states in the topbar, in place of `styles.colors.primary`.',
|
|
115
|
+
'styles.navbar.dark': 'Topbar in dark mode: a background color, or `{ background, accent }`.',
|
|
116
|
+
'styles.navbar.dark.background': 'Topbar background color in dark mode.',
|
|
117
|
+
'styles.navbar.dark.accent': 'Color of the active tab and hover states in the topbar, in dark mode.',
|
|
118
|
+
'styles.background': 'Background color of the site, and an optional background image.',
|
|
119
|
+
'styles.background.colors': 'Background colors.',
|
|
120
|
+
'styles.background.colors.light': 'Background color in light mode. Default "#ffffff".',
|
|
121
|
+
'styles.background.colors.dark': 'Background color in dark mode. Default "#0b1120".',
|
|
122
|
+
'styles.background.images': 'Background images, shown behind the content and sidebar.',
|
|
123
|
+
'styles.background.images.light': 'Background image in light mode.',
|
|
124
|
+
'styles.background.images.dark': 'Background image in dark mode.',
|
|
125
|
+
|
|
126
|
+
navigation:
|
|
127
|
+
'Required. The site\'s structure: a list of pages and groups, or an object with exactly one of `tabs`, `versions`, `languages`, `dropdowns` or `products`. Those can nest inside each other.',
|
|
128
|
+
'navigation.global': 'Dropdowns shown on every page, whatever section is open.',
|
|
129
|
+
'navigation.global.dropdowns': 'Dropdowns shown on every page.',
|
|
130
|
+
'navigation.tabs': 'Tabs in the topbar, each with its own sidebar.',
|
|
131
|
+
'navigation.versions': 'Versions of the docs, with a version picker.',
|
|
132
|
+
'navigation.languages': 'Languages of the docs, with a language picker.',
|
|
133
|
+
'navigation.dropdowns': 'Sections chosen from a dropdown menu.',
|
|
134
|
+
'navigation.products': 'Products, with a product picker.',
|
|
135
|
+
|
|
136
|
+
'NavigationItem.group': 'The group\'s name in the sidebar.',
|
|
137
|
+
'NavigationItem.page': 'A page the group\'s name links to.',
|
|
138
|
+
'NavigationItem.pages': 'The pages and groups inside this group.',
|
|
139
|
+
'NavigationItem.openapi': 'Generate this group\'s pages from an OpenAPI spec - a page per operation, grouped by tag.',
|
|
140
|
+
'NavigationItem.openapi.src': 'Path of the OpenAPI spec file, relative to writedocs.json.',
|
|
141
|
+
'NavigationItem.openapi.path': 'URL path the generated pages go under, like "/api". Each OpenAPI group needs its own.',
|
|
142
|
+
'NavigationItem.label': 'Link text in the sidebar.',
|
|
143
|
+
'NavigationItem.href': 'Where the link goes.',
|
|
144
|
+
...Object.fromEntries(containerEntries),
|
|
145
|
+
|
|
146
|
+
socials: 'Icon links in the footer: platform -> URL, like { "github": "https://github.com/acme" }. The key is also the icon.',
|
|
147
|
+
topbar: 'The topbar.',
|
|
148
|
+
'topbar.links': 'Links at the right of the topbar.',
|
|
149
|
+
...link('topbar.links[]'),
|
|
150
|
+
footer: 'The footer: columns of links, and the `socials` icons.',
|
|
151
|
+
'footer.columns': 'Columns of links.',
|
|
152
|
+
'footer.columns[].title': 'The column\'s heading. Optional.',
|
|
153
|
+
'footer.columns[].links': 'The links in the column.',
|
|
154
|
+
...link('footer.columns[].links[]'),
|
|
155
|
+
...logo('footer.logo'),
|
|
156
|
+
|
|
157
|
+
api: 'The API playground.',
|
|
158
|
+
'api.proxy': 'Send "Try it" requests through writedocs\' proxy, for APIs that don\'t allow calls from other sites (CORS). Default true.',
|
|
159
|
+
domain: 'The site\'s address, like "docs.example.com". Turns on sitemap.xml and absolute URLs in link previews.',
|
|
160
|
+
|
|
161
|
+
seo: 'Default metadata for every page. A page\'s own `seo` frontmatter overrides it field by field.',
|
|
162
|
+
'seo.ogImage': 'Image shown in link previews - a path or a full URL.',
|
|
163
|
+
'seo.ogType': 'The og:type of pages. Default "website".',
|
|
164
|
+
'seo.twitterCard': 'X/Twitter card style. Default "summary_large_image" with an `ogImage`, "summary" without.',
|
|
165
|
+
'seo.keywords': 'Keywords for the <meta name="keywords"> tag.',
|
|
166
|
+
'seo.noindex': 'Ask search engines not to index pages, and leave them out of sitemap.xml.',
|
|
167
|
+
contextMenu: 'Adds a "Copy page" menu to pages - copy as Markdown, or open the page in an AI assistant - and a Markdown copy of each page at its address + ".md".',
|
|
168
|
+
'contextMenu.openIn': 'Which AI assistants the menu offers. Default: all three.',
|
|
169
|
+
redirects: 'Redirects from old addresses. Each matches one exact path.',
|
|
170
|
+
'redirects[].source': 'The old path, like "/old-page".',
|
|
171
|
+
'redirects[].destination': 'Where to send it, like "/docs/new-page/".',
|
|
172
|
+
variables: 'Values pages can insert with [[name]], like { "productName": "Acme" }.',
|
|
173
|
+
banner: 'A banner above the topbar on every page.',
|
|
174
|
+
'banner.content': 'The banner\'s text. Markdown links work.',
|
|
175
|
+
'banner.dismissible': 'Let readers close it. Default false.',
|
|
176
|
+
'banner.type': 'Its color: "info", "warning" or "critical". Default "info".',
|
|
177
|
+
notFound: 'The 404 page.',
|
|
178
|
+
'notFound.title': 'The 404 page\'s title.',
|
|
179
|
+
'notFound.description': 'Text under the title on the 404 page.',
|
|
180
|
+
scripts: 'Scripts added to every page - for tools `integrations` doesn\'t cover.',
|
|
181
|
+
'scripts.head': 'Scripts added at the end of <head>.',
|
|
182
|
+
...script('scripts.head[]'),
|
|
183
|
+
'scripts.body': 'Scripts added at the end of <body>.',
|
|
184
|
+
...script('scripts.body[]'),
|
|
185
|
+
|
|
186
|
+
integrations: 'Analytics and chat tools.',
|
|
187
|
+
'integrations.ga4': 'Google Analytics 4.',
|
|
188
|
+
'integrations.ga4.measurementId': 'Measurement ID, like "G-XXXXXXXXXX".',
|
|
189
|
+
'integrations.googleTagManager': 'Google Tag Manager.',
|
|
190
|
+
'integrations.googleTagManager.containerId': 'Container ID, like "GTM-XXXXXXX".',
|
|
191
|
+
'integrations.plausible': 'Plausible Analytics.',
|
|
192
|
+
'integrations.plausible.domain': 'The site\'s domain, as registered in Plausible.',
|
|
193
|
+
'integrations.plausible.src': 'Script URL, for a self-hosted Plausible. Default "https://plausible.io/js/script.js".',
|
|
194
|
+
'integrations.fathom': 'Fathom Analytics.',
|
|
195
|
+
'integrations.fathom.siteId': 'Fathom\'s site ID, like "ABCDEFGH".',
|
|
196
|
+
'integrations.posthog': 'PostHog.',
|
|
197
|
+
'integrations.posthog.apiKey': 'Project API key, starting with "phc_".',
|
|
198
|
+
'integrations.posthog.apiHost': 'API host. Default "https://us.i.posthog.com" - use yours for EU cloud or self-hosting.',
|
|
199
|
+
'integrations.umami': 'Umami Analytics.',
|
|
200
|
+
'integrations.umami.websiteId': 'Umami\'s website ID.',
|
|
201
|
+
'integrations.umami.src': 'Script URL. Default "https://cloud.umami.is/script.js" - use yours when self-hosting.',
|
|
202
|
+
'integrations.askAi': 'AI chat over the docs, with DocsBot.',
|
|
203
|
+
'integrations.askAi.id': 'DocsBot\'s "teamId/botId". The WRITEDOCS_ASK_AI_ID environment variable overrides it.',
|
|
204
|
+
};
|