@forwardimpact/libdoc 0.2.3 → 0.2.4
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/fit-doc.js +11 -6
- package/builder.js +163 -8
- package/package.json +1 -1
- package/server.js +2 -0
package/bin/fit-doc.js
CHANGED
|
@@ -22,8 +22,9 @@ Options:
|
|
|
22
22
|
-h, --help Show this help message
|
|
23
23
|
|
|
24
24
|
Build options:
|
|
25
|
-
--src=<dir>
|
|
26
|
-
--out=<dir>
|
|
25
|
+
--src=<dir> Source directory (default: website)
|
|
26
|
+
--out=<dir> Output directory (default: dist)
|
|
27
|
+
--base-url=<url> Base URL for sitemap, canonical links, and llms.txt
|
|
27
28
|
|
|
28
29
|
Serve options:
|
|
29
30
|
--src=<dir> Source directory (default: website)
|
|
@@ -44,13 +45,14 @@ function error(message) {
|
|
|
44
45
|
* @param {import("../builder.js").DocsBuilder} builder
|
|
45
46
|
* @param {string} docsDir
|
|
46
47
|
* @param {string} distDir
|
|
48
|
+
* @param {string} [baseUrl]
|
|
47
49
|
*/
|
|
48
|
-
async function runBuild(builder, docsDir, distDir) {
|
|
50
|
+
async function runBuild(builder, docsDir, distDir, baseUrl) {
|
|
49
51
|
if (!fs.existsSync(docsDir)) {
|
|
50
52
|
error(`Source directory not found: ${docsDir}`);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
await builder.build(docsDir, distDir);
|
|
55
|
+
await builder.build(docsDir, distDir, baseUrl);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -65,7 +67,7 @@ async function runServe(builder, server, docsDir, distDir, options) {
|
|
|
65
67
|
error(`Source directory not found: ${docsDir}`);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
await builder.build(docsDir, distDir);
|
|
70
|
+
await builder.build(docsDir, distDir, options.baseUrl);
|
|
69
71
|
|
|
70
72
|
if (options.watch) {
|
|
71
73
|
server.watch(docsDir, distDir);
|
|
@@ -95,6 +97,7 @@ async function main() {
|
|
|
95
97
|
options: {
|
|
96
98
|
src: { type: "string", default: "website" },
|
|
97
99
|
out: { type: "string", default: "dist" },
|
|
100
|
+
"base-url": { type: "string" },
|
|
98
101
|
port: { type: "string", short: "p", default: "3000" },
|
|
99
102
|
watch: { type: "boolean", short: "w", default: false },
|
|
100
103
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -110,6 +113,7 @@ async function main() {
|
|
|
110
113
|
const workingDir = process.env.INIT_CWD || process.cwd();
|
|
111
114
|
const docsDir = path.join(workingDir, values.src);
|
|
112
115
|
const distDir = path.join(workingDir, values.out);
|
|
116
|
+
const baseUrl = values["base-url"];
|
|
113
117
|
|
|
114
118
|
const builder = new DocsBuilder(
|
|
115
119
|
fs,
|
|
@@ -122,12 +126,13 @@ async function main() {
|
|
|
122
126
|
|
|
123
127
|
try {
|
|
124
128
|
if (command === "build") {
|
|
125
|
-
await runBuild(builder, docsDir, distDir);
|
|
129
|
+
await runBuild(builder, docsDir, distDir, baseUrl);
|
|
126
130
|
} else {
|
|
127
131
|
const server = new DocsServer(fs, Hono, serve, builder);
|
|
128
132
|
await runServe(builder, server, docsDir, distDir, {
|
|
129
133
|
port: parseInt(values.port, 10),
|
|
130
134
|
watch: values.watch,
|
|
135
|
+
baseUrl,
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
138
|
} catch (err) {
|
package/builder.js
CHANGED
|
@@ -82,6 +82,28 @@ export class DocsBuilder {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Transform markdown-syntax links from .md references to directory-style URLs
|
|
87
|
+
* Same rules as #transformMarkdownLinks but for [text](path) syntax
|
|
88
|
+
* @param {string} markdown - Markdown content to transform
|
|
89
|
+
* @returns {string} Markdown with transformed links
|
|
90
|
+
*/
|
|
91
|
+
#transformMarkdownBodyLinks(markdown) {
|
|
92
|
+
return markdown.replace(
|
|
93
|
+
/\[([^\]]*)\]\(([^)]*?)\.md(#[^)]*)?\)/g,
|
|
94
|
+
(_match, text, path, hash) => {
|
|
95
|
+
const fragment = hash || "";
|
|
96
|
+
if (path === "index" || path === "./index") {
|
|
97
|
+
return `[${text}](./${fragment})`;
|
|
98
|
+
}
|
|
99
|
+
if (path.endsWith("/index")) {
|
|
100
|
+
return `[${text}](${path.slice(0, -5)}${fragment})`;
|
|
101
|
+
}
|
|
102
|
+
return `[${text}](${path}/${fragment})`;
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
/**
|
|
86
108
|
* Generate table of contents from h2 headings
|
|
87
109
|
* @param {string} html - HTML content to extract headings from
|
|
@@ -133,16 +155,19 @@ export class DocsBuilder {
|
|
|
133
155
|
console.log(" ✓ assets/");
|
|
134
156
|
}
|
|
135
157
|
|
|
136
|
-
// Copy
|
|
137
|
-
const
|
|
138
|
-
if (!this.#fs.existsSync(publicDir)) return;
|
|
139
|
-
|
|
158
|
+
// Copy root-level static files (robots.txt, llms.txt, etc.)
|
|
159
|
+
const skipFiles = new Set(["index.template.html", "CNAME"]);
|
|
140
160
|
this.#fs
|
|
141
|
-
.readdirSync(
|
|
142
|
-
.filter(
|
|
161
|
+
.readdirSync(docsDir, { withFileTypes: true })
|
|
162
|
+
.filter(
|
|
163
|
+
(entry) =>
|
|
164
|
+
entry.isFile() &&
|
|
165
|
+
!entry.name.endsWith(".md") &&
|
|
166
|
+
!skipFiles.has(entry.name),
|
|
167
|
+
)
|
|
143
168
|
.forEach((entry) => {
|
|
144
169
|
this.#fs.copyFileSync(
|
|
145
|
-
this.#path.join(
|
|
170
|
+
this.#path.join(docsDir, entry.name),
|
|
146
171
|
this.#path.join(distDir, entry.name),
|
|
147
172
|
);
|
|
148
173
|
console.log(` ✓ ${entry.name}`);
|
|
@@ -225,15 +250,115 @@ export class DocsBuilder {
|
|
|
225
250
|
return results;
|
|
226
251
|
}
|
|
227
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Generate sitemap.xml from page inventory
|
|
255
|
+
* @param {Array<{urlPath: string}>} pages - Sorted page inventory
|
|
256
|
+
* @param {string} baseUrl - Base URL for the site
|
|
257
|
+
* @param {string} distDir - Destination distribution directory
|
|
258
|
+
*/
|
|
259
|
+
#generateSitemap(pages, baseUrl, distDir) {
|
|
260
|
+
const urls = pages
|
|
261
|
+
.map((p) => ` <url>\n <loc>${baseUrl}${p.urlPath}</loc>\n </url>`)
|
|
262
|
+
.join("\n");
|
|
263
|
+
const xml = [
|
|
264
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
265
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
266
|
+
urls,
|
|
267
|
+
"</urlset>",
|
|
268
|
+
"",
|
|
269
|
+
].join("\n");
|
|
270
|
+
this.#fs.writeFileSync(
|
|
271
|
+
this.#path.join(distDir, "sitemap.xml"),
|
|
272
|
+
xml,
|
|
273
|
+
"utf-8",
|
|
274
|
+
);
|
|
275
|
+
console.log(" ✓ sitemap.xml");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Augment llms.txt with auto-generated page links under each H2 section
|
|
280
|
+
* @param {Array<{urlPath: string, title: string, description: string}>} pages - Sorted page inventory
|
|
281
|
+
* @param {string} baseUrl - Base URL for the site
|
|
282
|
+
* @param {string} distDir - Destination distribution directory
|
|
283
|
+
*/
|
|
284
|
+
#augmentLlmsTxt(pages, baseUrl, distDir) {
|
|
285
|
+
const llmsPath = this.#path.join(distDir, "llms.txt");
|
|
286
|
+
if (!this.#fs.existsSync(llmsPath)) return;
|
|
287
|
+
|
|
288
|
+
const content = this.#fs.readFileSync(llmsPath, "utf-8");
|
|
289
|
+
const lines = content.split("\n");
|
|
290
|
+
|
|
291
|
+
// Map pages to sections
|
|
292
|
+
const sections = { Products: [], Documentation: [], Optional: [] };
|
|
293
|
+
const productSlugs = new Set([
|
|
294
|
+
"map",
|
|
295
|
+
"pathway",
|
|
296
|
+
"basecamp",
|
|
297
|
+
"guide",
|
|
298
|
+
"landmark",
|
|
299
|
+
"summit",
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
for (const page of pages) {
|
|
303
|
+
const topSegment = page.urlPath.split("/").filter(Boolean)[0];
|
|
304
|
+
if (page.urlPath.startsWith("/docs/")) {
|
|
305
|
+
sections.Documentation.push(page);
|
|
306
|
+
} else if (topSegment && productSlugs.has(topSegment)) {
|
|
307
|
+
sections.Products.push(page);
|
|
308
|
+
} else {
|
|
309
|
+
sections.Optional.push(page);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const linkLine = (page) => {
|
|
314
|
+
const mdUrl =
|
|
315
|
+
page.urlPath === "/"
|
|
316
|
+
? `${baseUrl}/index.md`
|
|
317
|
+
: `${baseUrl}${page.urlPath}index.md`;
|
|
318
|
+
const desc = page.description ? `: ${page.description}` : "";
|
|
319
|
+
return `- [${page.title}](${mdUrl})${desc}`;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Reassemble: insert links after each H2
|
|
323
|
+
const output = [];
|
|
324
|
+
for (const line of lines) {
|
|
325
|
+
output.push(line);
|
|
326
|
+
const h2Match = line.match(/^## (.+)$/);
|
|
327
|
+
if (h2Match) {
|
|
328
|
+
const sectionName = h2Match[1].trim();
|
|
329
|
+
const pageList = sections[sectionName];
|
|
330
|
+
if (pageList?.length) {
|
|
331
|
+
output.push("");
|
|
332
|
+
for (const page of pageList) {
|
|
333
|
+
output.push(linkLine(page));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.#fs.writeFileSync(llmsPath, output.join("\n"), "utf-8");
|
|
340
|
+
console.log(" ✓ llms.txt (augmented)");
|
|
341
|
+
}
|
|
342
|
+
|
|
228
343
|
/**
|
|
229
344
|
* Build documentation from Markdown files
|
|
230
345
|
* @param {string} docsDir - Source documentation directory
|
|
231
346
|
* @param {string} distDir - Destination distribution directory
|
|
347
|
+
* @param {string} [baseUrl] - Base URL for sitemap, canonical links, and llms.txt
|
|
232
348
|
* @returns {Promise<void>}
|
|
233
349
|
*/
|
|
234
|
-
async build(docsDir, distDir) {
|
|
350
|
+
async build(docsDir, distDir, baseUrl) {
|
|
235
351
|
console.log("Building documentation...");
|
|
236
352
|
|
|
353
|
+
// Resolve base URL: explicit flag > CNAME fallback > undefined
|
|
354
|
+
if (!baseUrl) {
|
|
355
|
+
const cnamePath = this.#path.join(docsDir, "CNAME");
|
|
356
|
+
if (this.#fs.existsSync(cnamePath)) {
|
|
357
|
+
const hostname = this.#fs.readFileSync(cnamePath, "utf-8").trim();
|
|
358
|
+
baseUrl = `https://${hostname}`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
237
362
|
// Clean and create dist directory
|
|
238
363
|
if (this.#fs.existsSync(distDir)) {
|
|
239
364
|
this.#fs.rmSync(distDir, { recursive: true });
|
|
@@ -265,6 +390,9 @@ export class DocsBuilder {
|
|
|
265
390
|
}
|
|
266
391
|
}
|
|
267
392
|
|
|
393
|
+
// Page inventory for sitemap and llms.txt generation
|
|
394
|
+
const pages = [];
|
|
395
|
+
|
|
268
396
|
for (const mdFile of mdFiles) {
|
|
269
397
|
const { data: frontMatter, content: markdown } = this.#matter(
|
|
270
398
|
this.#fs.readFileSync(this.#path.join(docsDir, mdFile), "utf-8"),
|
|
@@ -292,6 +420,14 @@ export class DocsBuilder {
|
|
|
292
420
|
const urlPath = this.#urlPathFromMdFile(mdFile);
|
|
293
421
|
const breadcrumbs = this.#buildBreadcrumbs(urlPath, pageTitles);
|
|
294
422
|
|
|
423
|
+
// Collect page inventory for sitemap and llms.txt
|
|
424
|
+
pages.push({
|
|
425
|
+
mdFile,
|
|
426
|
+
urlPath,
|
|
427
|
+
title: frontMatter.title,
|
|
428
|
+
description: frontMatter.description || "",
|
|
429
|
+
});
|
|
430
|
+
|
|
295
431
|
// Render template with context
|
|
296
432
|
const outputHtml = this.#mustacheRender(template, {
|
|
297
433
|
title: frontMatter.title,
|
|
@@ -309,6 +445,8 @@ export class DocsBuilder {
|
|
|
309
445
|
hasHeroCta: heroCta.length > 0,
|
|
310
446
|
hasBreadcrumbs: !!breadcrumbs,
|
|
311
447
|
breadcrumbs,
|
|
448
|
+
markdownUrl: "index.md",
|
|
449
|
+
canonicalUrl: baseUrl ? baseUrl + urlPath : "",
|
|
312
450
|
});
|
|
313
451
|
|
|
314
452
|
// Format HTML with prettier
|
|
@@ -361,9 +499,26 @@ export class DocsBuilder {
|
|
|
361
499
|
"utf-8",
|
|
362
500
|
);
|
|
363
501
|
console.log(` ✓ ${outputPath}.html`);
|
|
502
|
+
|
|
503
|
+
// Write markdown companion (index.md alongside index.html)
|
|
504
|
+
const companionContent = `# ${frontMatter.title}\n\n${this.#transformMarkdownBodyLinks(markdown)}`;
|
|
505
|
+
this.#fs.writeFileSync(
|
|
506
|
+
this.#path.join(distDir, outputPath + ".md"),
|
|
507
|
+
companionContent,
|
|
508
|
+
"utf-8",
|
|
509
|
+
);
|
|
364
510
|
}
|
|
365
511
|
|
|
512
|
+
// Sort page inventory for deterministic output
|
|
513
|
+
pages.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
514
|
+
|
|
366
515
|
this.#copyStaticAssets(docsDir, distDir);
|
|
516
|
+
|
|
517
|
+
if (baseUrl) {
|
|
518
|
+
this.#generateSitemap(pages, baseUrl, distDir);
|
|
519
|
+
this.#augmentLlmsTxt(pages, baseUrl, distDir);
|
|
520
|
+
}
|
|
521
|
+
|
|
367
522
|
console.log("Documentation build complete!");
|
|
368
523
|
}
|
|
369
524
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED