@writedocs/generator 0.9.1 → 0.9.2
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/package.json
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Any delay: Astro waits 2 seconds on a 302's page (the automatic "/"), none on a 301's.
|
|
18
|
+
const REFRESH = /<meta http-equiv="refresh" content="\d+;\s*url=([^"]*)"\s*\/?>/i;
|
|
18
19
|
const ASTRO_REDIRECT = /<title>Redirecting to:/;
|
|
19
20
|
const CANONICAL = /<link rel="canonical" href="([^"]*)"\s*\/?>/i;
|
|
20
21
|
|
|
@@ -58,7 +58,9 @@ export function writeRedirectsFile(distDir, contentDir) {
|
|
|
58
58
|
if (!hasExplicitRootRedirect && fs.existsSync(indexHtmlPath)) {
|
|
59
59
|
const html = fs.readFileSync(indexHtmlPath, 'utf8');
|
|
60
60
|
const match = html.match(/<meta http-equiv="refresh" content="\d+;\s*url=([^"]+)"/i);
|
|
61
|
-
|
|
61
|
+
// 302, not 301: see [...slug].astro - a browser keeps a 301 for good,
|
|
62
|
+
// and "/" stops redirecting once the site gets a home page.
|
|
63
|
+
if (match) lines.push(`/ ${match[1]} 302`);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
if (lines.length === 0) return;
|
package/src/lib/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { EXCLUDED_TOP_LEVEL_DIRS } from './pages.js';
|
|
3
|
+
import { EXCLUDED_TOP_LEVEL_DIRS, navPageId } from './pages.js';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { writedocsTempDir } from './writedocs-temp-dir.js';
|
|
6
6
|
import { readableTextOn } from './color.js';
|
|
@@ -419,10 +419,27 @@ export function loadDocsConfig(contentDir: string): DocsConfig {
|
|
|
419
419
|
// A no-op pass over ordinary navigation trees (no openapi groups) -
|
|
420
420
|
// always run, rather than gated behind a global manifest check, since
|
|
421
421
|
// there's no longer a single global spec to check for.
|
|
422
|
-
result.data.navigation = expandOpenApiInNavigation(result.data.navigation, contentDir);
|
|
422
|
+
result.data.navigation = withPageIds(expandOpenApiInNavigation(result.data.navigation, contentDir));
|
|
423
423
|
return result.data;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/** The navigation with every page entry as its page id - "guides/index"
|
|
427
|
+
* becomes "guides" (navPageId() in lib/pages.js), so everything that
|
|
428
|
+
* matches entries against pages (routes, the sidebar, prev/next, llms.txt,
|
|
429
|
+
* the MCP index) finds a folder's index.mdx however writedocs.json names it. */
|
|
430
|
+
function withPageIds<T>(node: T): T {
|
|
431
|
+
if (Array.isArray(node)) return node.map((item) => (typeof item === 'string' ? navPageId(item) : withPageIds(item))) as T;
|
|
432
|
+
if (node && typeof node === 'object') {
|
|
433
|
+
return Object.fromEntries(
|
|
434
|
+
Object.entries(node).map(([key, value]) => [
|
|
435
|
+
key,
|
|
436
|
+
key === 'page' && typeof value === 'string' ? navPageId(value) : key === 'href' || key === 'openapi' ? value : withPageIds(value),
|
|
437
|
+
])
|
|
438
|
+
) as T;
|
|
439
|
+
}
|
|
440
|
+
return node;
|
|
441
|
+
}
|
|
442
|
+
|
|
426
443
|
// --- Locating a page by file id, independent of its effective slug -----
|
|
427
444
|
//
|
|
428
445
|
// writedocs.json's `pages` arrays, and every helper above/below that walks
|
package/src/lib/content-check.js
CHANGED
|
@@ -30,7 +30,7 @@ import fs from 'node:fs';
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import matter from 'gray-matter';
|
|
32
32
|
import { visit } from 'unist-util-visit';
|
|
33
|
-
import { findAllPages, fileIdForPath, titleFromPath } from './pages.js';
|
|
33
|
+
import { findAllPages, fileIdForPath, titleFromPath, navPageId } from './pages.js';
|
|
34
34
|
import { iconExists } from './icons.js';
|
|
35
35
|
import { findUnknownComponents } from './mdx-unknown-components.js';
|
|
36
36
|
import { findInteractiveComponents, simplifiedBuiltinMessage } from './mdx-inline-react.js';
|
|
@@ -400,16 +400,14 @@ export async function checkContent(contentDir, configText) {
|
|
|
400
400
|
const locate = createJsonLocator(configText);
|
|
401
401
|
const ids = new Set(pages.map(fileIdForPath));
|
|
402
402
|
for (const ref of navigationReferences(config.navigation)) {
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
// "guides/index" and "guides" both name guides/index.mdx (navPageId()).
|
|
404
|
+
if (ids.has(navPageId(ref.id))) continue;
|
|
405
405
|
errors.push(
|
|
406
406
|
issue(
|
|
407
407
|
'writedocs.json',
|
|
408
408
|
locate(ref.path)?.line,
|
|
409
409
|
`The navigation lists page "${ref.id}", but there's no page with that path.`,
|
|
410
|
-
|
|
411
|
-
? `A folder's index page is listed by the folder's path - write "${ref.id.replace(/\/index$/, '')}".`
|
|
412
|
-
: `Create ${ref.id}.mdx, or fix the path (it's relative to the folder writedocs.json is in, without the extension).`
|
|
410
|
+
`Create ${ref.id}.mdx, or fix the path (it's relative to the folder writedocs.json is in, without the extension).`
|
|
413
411
|
)
|
|
414
412
|
);
|
|
415
413
|
}
|
package/src/lib/pages.js
CHANGED
|
@@ -16,9 +16,16 @@ export const EXCLUDED_TOP_LEVEL_DIRS = new Set(['node_modules', 'dist', '.astro'
|
|
|
16
16
|
/** A page's title when its frontmatter has none - Mintlify's rule, so a
|
|
17
17
|
* migrated page that relied on it keeps the same title: the file name
|
|
18
18
|
* without its extension, dashes and underscores as spaces, first letter
|
|
19
|
-
* capitalized. `guides/getting-started.mdx` -> "Getting started".
|
|
19
|
+
* capitalized. `guides/getting-started.mdx` -> "Getting started". A
|
|
20
|
+
* folder's index page is named for the folder (`api/index.mdx` -> "Api"),
|
|
21
|
+
* and the site's own index.mdx is "Home" - "Index" names neither. */
|
|
20
22
|
export function titleFromPath(filePath) {
|
|
21
|
-
const
|
|
23
|
+
const parts = String(filePath).split(/[\\/]/).filter(Boolean);
|
|
24
|
+
let base = (parts.pop() ?? '').replace(/\.mdx?$/i, '');
|
|
25
|
+
if (base.toLowerCase() === 'index') {
|
|
26
|
+
if (parts.length === 0) return 'Home';
|
|
27
|
+
base = parts.pop();
|
|
28
|
+
}
|
|
22
29
|
const words = base.replace(/[-_]+/g, ' ').trim();
|
|
23
30
|
return words ? words.charAt(0).toUpperCase() + words.slice(1) : 'Untitled';
|
|
24
31
|
}
|
|
@@ -61,15 +68,23 @@ function readIgnoreFile(contentDir) {
|
|
|
61
68
|
* groups' `page`) - read straight from the file, since this runs before
|
|
62
69
|
* (and independently of) config validation. Empty when there's no
|
|
63
70
|
* readable writedocs.json. */
|
|
71
|
+
/** The page id a navigation entry names. "guides/index" and "guides" are
|
|
72
|
+
* the same page: a folder's index.mdx has the folder's id (fileIdForPath()
|
|
73
|
+
* below), and Mintlify projects write it either way. The root "index"
|
|
74
|
+
* stays as it is. */
|
|
75
|
+
export function navPageId(id) {
|
|
76
|
+
return id.endsWith('/index') ? id.slice(0, -'/index'.length) : id;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
/** Every page id a `navigation` lists, in the order a reader meets them -
|
|
65
80
|
* top to bottom, each group's own `page` before its `pages` - without
|
|
66
81
|
* repeats. */
|
|
67
82
|
export function navigationPageOrder(navigation) {
|
|
68
83
|
const ids = new Set();
|
|
69
84
|
(function walk(node) {
|
|
70
|
-
if (Array.isArray(node)) node.forEach((item) => (typeof item === 'string' ? ids.add(item) : walk(item)));
|
|
85
|
+
if (Array.isArray(node)) node.forEach((item) => (typeof item === 'string' ? ids.add(navPageId(item)) : walk(item)));
|
|
71
86
|
else if (node && typeof node === 'object') {
|
|
72
|
-
if (typeof node.page === 'string') ids.add(node.page);
|
|
87
|
+
if (typeof node.page === 'string') ids.add(navPageId(node.page));
|
|
73
88
|
for (const [key, value] of Object.entries(node)) {
|
|
74
89
|
if (key !== 'page' && key !== 'openapi' && key !== 'href') walk(value);
|
|
75
90
|
}
|
|
@@ -158,7 +158,7 @@ export async function getStaticPaths() {
|
|
|
158
158
|
// rootAlreadyClaimed rules out), so the ternary below always resolves
|
|
159
159
|
// to the non-root branch.
|
|
160
160
|
const rootRedirect = firstEntry
|
|
161
|
-
? [{ params: { slug: undefined }, props: { redirectTo: `/${normalizeEntryId(firstEntry.id)}
|
|
161
|
+
? [{ params: { slug: undefined }, props: { redirectTo: `/${normalizeEntryId(firstEntry.id)}/`, temporary: true } }]
|
|
162
162
|
: [];
|
|
163
163
|
|
|
164
164
|
// Tracks every file id a nav-driven route below already claims, so
|
|
@@ -259,6 +259,8 @@ export async function getStaticPaths() {
|
|
|
259
259
|
|
|
260
260
|
interface Props {
|
|
261
261
|
redirectTo?: string;
|
|
262
|
+
// The automatic "/" redirect: temporary, see below.
|
|
263
|
+
temporary?: boolean;
|
|
262
264
|
entry?: DocsEntry;
|
|
263
265
|
prev?: { slug: string; group: string | null } | null;
|
|
264
266
|
next?: { slug: string; group: string | null } | null;
|
|
@@ -274,11 +276,15 @@ interface Props {
|
|
|
274
276
|
|
|
275
277
|
const rawProps = Astro.props as Props;
|
|
276
278
|
if (rawProps.redirectTo) {
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// "
|
|
280
|
-
//
|
|
281
|
-
|
|
279
|
+
// The automatic "/" redirect is temporary (302): its target is just the
|
|
280
|
+
// navigation's first page, and it goes away once the site has a page at
|
|
281
|
+
// "/". A 301 is remembered by the browser for good - after that, "/" kept
|
|
282
|
+
// redirecting even with a home page in place, in `writedocs dev` (every
|
|
283
|
+
// project's preview shares localhost:4321) and for returning visitors.
|
|
284
|
+
// A frontmatter `url` page stays 301. In a static build the status only
|
|
285
|
+
// picks the refresh delay of Astro's redirect page, and the build rewrites
|
|
286
|
+
// those pages to redirect at once either way (rewrite-redirect-pages.js).
|
|
287
|
+
return Astro.redirect(rawProps.redirectTo, rawProps.temporary ? 302 : 301);
|
|
282
288
|
}
|
|
283
289
|
const {
|
|
284
290
|
entry,
|