@pagenary/publisher 2026.5.3 → 2026.6.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/package.json +1 -1
- package/scripts/build-tenants.js +118 -22
- package/site/app.js +1 -1
- package/site/index.html +4 -4
- package/site/lib/manifest-utils.js +1 -1
- package/site/manifest.js +9 -9
- package/site/pages/api.html +85 -2
- package/site/pages/architecture.html +50 -7
- package/site/pages/deployment.html +1 -1
- package/site/pages/developer-guide.html +35 -1
- package/site/pages/extending.html +1 -1
- package/site/pages/quickstart.html +1 -1
- package/site/pages/seo-strategy.html +1 -1
- package/site/pages/tenant-config.html +1 -1
- package/site/pages/welcome.html +1 -1
- package/site/robots.txt +1 -1
- package/site/sections/api.js +1 -1
- package/site/sections/architecture.js +1 -1
- package/site/sections/developer-guide.js +1 -1
- package/site/sitemap.xml +10 -10
- package/site/styles.css +30 -0
- package/src/app.js +46 -0
- package/src/index.html +3 -3
- package/src/lib/manifest-utils.js +1 -1
- package/src/manifest.js +1 -1
- package/src/styles.css +30 -0
package/package.json
CHANGED
package/scripts/build-tenants.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createHash } from 'crypto';
|
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import { generateSeoArtifacts, resolveBaseUrl, resolveOgImage } from './lib/seo-generator.js';
|
|
10
10
|
import { generateCollections } from './lib/collections-generator.js';
|
|
11
|
+
import { parseFrontmatter } from './lib/frontmatter.js';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
14
|
const root = process.cwd();
|
|
@@ -1374,6 +1375,12 @@ function parseInlineMarkdown(input, linkContext = null) {
|
|
|
1374
1375
|
* @returns {string} HTML string
|
|
1375
1376
|
*/
|
|
1376
1377
|
function markdownToHtml(markdown, linkContext = null) {
|
|
1378
|
+
// Strip YAML frontmatter before rendering so the fence block doesn't leak
|
|
1379
|
+
// into the page as <hr>/<p>… text (#19). #18 made frontmatter mandatory on
|
|
1380
|
+
// collection posts; this wires the same parser the collections generator
|
|
1381
|
+
// already uses into the page render path so every caller benefits.
|
|
1382
|
+
const parsed = parseFrontmatter(markdown);
|
|
1383
|
+
markdown = parsed.body;
|
|
1377
1384
|
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
1378
1385
|
const chunks = [];
|
|
1379
1386
|
let inList = false;
|
|
@@ -1665,6 +1672,35 @@ async function ensureJavascriptModule(sourcePath, targetPath) {
|
|
|
1665
1672
|
await fsp.copyFile(sourcePath, targetPath);
|
|
1666
1673
|
}
|
|
1667
1674
|
|
|
1675
|
+
async function readMarkdownMetadata(sourcePath) {
|
|
1676
|
+
const raw = await fsp.readFile(sourcePath, 'utf8');
|
|
1677
|
+
const { data, body } = parseFrontmatter(raw);
|
|
1678
|
+
return {
|
|
1679
|
+
title: data.title || firstHeadingFromMarkdown(body) || null,
|
|
1680
|
+
summary: data.summary || data.description || '',
|
|
1681
|
+
date: data.date || null,
|
|
1682
|
+
reading_time: estimateReadingTime(body)
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function firstHeadingFromMarkdown(body) {
|
|
1687
|
+
const match = body.match(/^#\s+(.+)$/m);
|
|
1688
|
+
return match ? match[1].trim() : null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function estimateReadingTime(body) {
|
|
1692
|
+
const words = String(body || '').trim().split(/\s+/).filter(Boolean);
|
|
1693
|
+
return Math.max(1, Math.ceil(words.length / 200));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function readContentMetadata(sourcePath) {
|
|
1697
|
+
const ext = path.extname(sourcePath).toLowerCase();
|
|
1698
|
+
if (ext === '.md' || ext === '.markdown') {
|
|
1699
|
+
return readMarkdownMetadata(sourcePath);
|
|
1700
|
+
}
|
|
1701
|
+
return {};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1668
1704
|
// ============================================================================
|
|
1669
1705
|
// Internal Link Transformation (ADR-011)
|
|
1670
1706
|
// ============================================================================
|
|
@@ -1985,6 +2021,46 @@ function deriveSectionId(parentId, name, isIndex = false) {
|
|
|
1985
2021
|
return stem;
|
|
1986
2022
|
}
|
|
1987
2023
|
|
|
2024
|
+
function routePath(value) {
|
|
2025
|
+
return String(value || '').replace(/^\/+|\/+$/g, '');
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function collectionForRelPath(relPath, collections = []) {
|
|
2029
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
2030
|
+
return collections.find((collection) => {
|
|
2031
|
+
const collectionPath = routePath(collection.path);
|
|
2032
|
+
return collectionPath && (normalized === collectionPath || normalized.startsWith(`${collectionPath}/`));
|
|
2033
|
+
}) || null;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function decorateCollectionEntry(entry, metadata, collection) {
|
|
2037
|
+
if (!collection || !entry) return entry;
|
|
2038
|
+
entry.collection = routePath(collection.path);
|
|
2039
|
+
entry.showDate = collection.showDate === true;
|
|
2040
|
+
entry.showSummary = collection.showSummary === true;
|
|
2041
|
+
entry.showReadingTime = collection.showReadingTime !== false;
|
|
2042
|
+
if (metadata.date) entry.date = metadata.date;
|
|
2043
|
+
if (metadata.reading_time) entry.reading_time = metadata.reading_time;
|
|
2044
|
+
return entry;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function sortCollectionEntries(entries, collection) {
|
|
2048
|
+
if (!collection || !Array.isArray(entries)) return entries;
|
|
2049
|
+
const sortBy = collection.sortBy || 'date';
|
|
2050
|
+
const dir = (collection.order || 'desc').toLowerCase() === 'asc' ? 1 : -1;
|
|
2051
|
+
entries.sort((a, b) => {
|
|
2052
|
+
const av = a?.[sortBy];
|
|
2053
|
+
const bv = b?.[sortBy];
|
|
2054
|
+
if (av == null && bv == null) return 0;
|
|
2055
|
+
if (av == null) return 1;
|
|
2056
|
+
if (bv == null) return -1;
|
|
2057
|
+
if (av < bv) return -1 * dir;
|
|
2058
|
+
if (av > bv) return 1 * dir;
|
|
2059
|
+
return 0;
|
|
2060
|
+
});
|
|
2061
|
+
return entries;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1988
2064
|
/**
|
|
1989
2065
|
* Encode section ID for use in output filename
|
|
1990
2066
|
* Replaces / with -- to create flat output structure
|
|
@@ -2100,19 +2176,21 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2100
2176
|
const indexFile = files.find(f => /^index\.(md|markdown|html|htm|js|mjs)$/i.test(f.name));
|
|
2101
2177
|
if (indexFile) {
|
|
2102
2178
|
const sectionId = parentId || path.basename(dirPath);
|
|
2103
|
-
const title = manifest?.title || humanizeTitle(path.basename(dirPath));
|
|
2104
|
-
const summary = manifest?.summary || '';
|
|
2105
|
-
|
|
2106
|
-
// Calculate relative path from content root
|
|
2107
2179
|
const relPath = path.relative(context.contentRoot, path.join(dirPath, indexFile.name));
|
|
2180
|
+
const metadata = await readContentMetadata(path.join(dirPath, indexFile.name));
|
|
2181
|
+
const collection = collectionForRelPath(relPath, context.collections);
|
|
2182
|
+
const title = manifest?.title || metadata.title || humanizeTitle(path.basename(dirPath));
|
|
2183
|
+
const summary = manifest?.summary || metadata.summary || '';
|
|
2108
2184
|
|
|
2109
|
-
|
|
2185
|
+
const sectionEntry = {
|
|
2110
2186
|
id: sectionId,
|
|
2111
2187
|
title,
|
|
2112
2188
|
summary,
|
|
2113
2189
|
file: relPath,
|
|
2114
2190
|
_isIndex: true
|
|
2115
|
-
}
|
|
2191
|
+
};
|
|
2192
|
+
decorateCollectionEntry(sectionEntry, metadata, collection);
|
|
2193
|
+
sections.push(sectionEntry);
|
|
2116
2194
|
}
|
|
2117
2195
|
|
|
2118
2196
|
// Process other content files
|
|
@@ -2124,10 +2202,12 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2124
2202
|
s.id === sectionId || s.file === file.name || s.id === file.name.replace(/\.[^.]+$/, '')
|
|
2125
2203
|
);
|
|
2126
2204
|
|
|
2127
|
-
const title = manifestEntry?.title || humanizeTitle(file.name);
|
|
2128
|
-
const summary = manifestEntry?.summary || '';
|
|
2129
|
-
const type = manifestEntry?.type || null;
|
|
2130
2205
|
const relPath = path.relative(context.contentRoot, path.join(dirPath, file.name));
|
|
2206
|
+
const metadata = await readContentMetadata(path.join(dirPath, file.name));
|
|
2207
|
+
const collection = collectionForRelPath(relPath, context.collections);
|
|
2208
|
+
const title = manifestEntry?.title || metadata.title || humanizeTitle(file.name);
|
|
2209
|
+
const summary = manifestEntry?.summary || metadata.summary || '';
|
|
2210
|
+
const type = manifestEntry?.type || null;
|
|
2131
2211
|
|
|
2132
2212
|
const sectionEntry = {
|
|
2133
2213
|
id: sectionId,
|
|
@@ -2135,6 +2215,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2135
2215
|
summary,
|
|
2136
2216
|
file: relPath
|
|
2137
2217
|
};
|
|
2218
|
+
decorateCollectionEntry(sectionEntry, metadata, collection);
|
|
2138
2219
|
if (type) sectionEntry.type = type;
|
|
2139
2220
|
sections.push(sectionEntry);
|
|
2140
2221
|
}
|
|
@@ -2163,6 +2244,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2163
2244
|
if (indexEntry) {
|
|
2164
2245
|
// Remove index from subsections, it becomes the group itself
|
|
2165
2246
|
const otherSections = subsections.filter(s => !s._isIndex);
|
|
2247
|
+
sortCollectionEntries(otherSections, collectionForRelPath(path.relative(context.contentRoot, subdirPath), context.collections));
|
|
2166
2248
|
const entry = {
|
|
2167
2249
|
id: subdirId,
|
|
2168
2250
|
title,
|
|
@@ -2178,7 +2260,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2178
2260
|
id: subdirId,
|
|
2179
2261
|
title,
|
|
2180
2262
|
summary,
|
|
2181
|
-
subsections
|
|
2263
|
+
subsections: sortCollectionEntries(subsections, collectionForRelPath(path.relative(context.contentRoot, subdirPath), context.collections))
|
|
2182
2264
|
};
|
|
2183
2265
|
if (collapsed) entry.collapsed = true;
|
|
2184
2266
|
sections.push(entry);
|
|
@@ -2434,7 +2516,7 @@ async function materializeScannedSections(sections, context) {
|
|
|
2434
2516
|
const processed = [];
|
|
2435
2517
|
|
|
2436
2518
|
for (const section of sections) {
|
|
2437
|
-
const { id, title, summary, file, subsections, url, type, collapsed } = section;
|
|
2519
|
+
const { id, title, summary, file, subsections, url, type, collapsed, _isIndex, ...metadata } = section;
|
|
2438
2520
|
|
|
2439
2521
|
// Pass through external links without processing
|
|
2440
2522
|
if (url) {
|
|
@@ -2492,21 +2574,22 @@ async function materializeScannedSections(sections, context) {
|
|
|
2492
2574
|
id,
|
|
2493
2575
|
title,
|
|
2494
2576
|
summary,
|
|
2495
|
-
|
|
2577
|
+
...metadata,
|
|
2578
|
+
module: `/sections/${outFile}`,
|
|
2496
2579
|
subsections: processedSubsections
|
|
2497
2580
|
};
|
|
2498
2581
|
if (type) entry.type = type;
|
|
2499
2582
|
if (collapsed) entry.collapsed = true;
|
|
2500
2583
|
processed.push(entry);
|
|
2501
2584
|
} else {
|
|
2502
|
-
const entry = { id, title, summary, module:
|
|
2585
|
+
const entry = { id, title, summary, ...metadata, module: `/sections/${outFile}` };
|
|
2503
2586
|
if (type) entry.type = type;
|
|
2504
2587
|
if (collapsed) entry.collapsed = true;
|
|
2505
2588
|
processed.push(entry);
|
|
2506
2589
|
}
|
|
2507
2590
|
} else if (processedSubsections && processedSubsections.length > 0) {
|
|
2508
2591
|
// Group without its own content
|
|
2509
|
-
const entry = { id, title, summary, subsections: processedSubsections };
|
|
2592
|
+
const entry = { id, title, summary, ...metadata, subsections: processedSubsections };
|
|
2510
2593
|
if (type) entry.type = type;
|
|
2511
2594
|
if (collapsed) entry.collapsed = true;
|
|
2512
2595
|
processed.push(entry);
|
|
@@ -2568,6 +2651,9 @@ function applyManifestHierarchy(scannedSections, rootManifest) {
|
|
|
2568
2651
|
title: mEntry.title || scanned?.title || mEntry.id,
|
|
2569
2652
|
summary: mEntry.summary || scanned?.summary || ''
|
|
2570
2653
|
};
|
|
2654
|
+
for (const key of ['collection', 'showDate', 'showSummary', 'showReadingTime', 'date', 'reading_time']) {
|
|
2655
|
+
if (scanned?.[key] !== undefined) node[key] = scanned[key];
|
|
2656
|
+
}
|
|
2571
2657
|
|
|
2572
2658
|
if (mEntry.collapsed) node.collapsed = true;
|
|
2573
2659
|
if (mEntry.type) node.type = mEntry.type;
|
|
@@ -2664,6 +2750,7 @@ async function processNestedContent(sourceDir, distDir, tenantId, contentRoot, o
|
|
|
2664
2750
|
keepFiles,
|
|
2665
2751
|
leafOrder: [],
|
|
2666
2752
|
siteConfig,
|
|
2753
|
+
collections: Array.isArray(config.collections) ? config.collections : [],
|
|
2667
2754
|
// Link transformation context (populated after scan)
|
|
2668
2755
|
sectionIndex: null,
|
|
2669
2756
|
linkWarnings,
|
|
@@ -2772,7 +2859,7 @@ async function materializeSectionModule(entry, context) {
|
|
|
2772
2859
|
}
|
|
2773
2860
|
|
|
2774
2861
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
2775
|
-
const outFile = `${id}.js`;
|
|
2862
|
+
const outFile = `${encodePathForFilename(id)}.js`;
|
|
2776
2863
|
const targetPath = path.join(context.sectionsDir, outFile);
|
|
2777
2864
|
context.keepFiles.add(outFile);
|
|
2778
2865
|
|
|
@@ -2800,7 +2887,7 @@ async function materializeSectionModule(entry, context) {
|
|
|
2800
2887
|
return null;
|
|
2801
2888
|
}
|
|
2802
2889
|
|
|
2803
|
-
return
|
|
2890
|
+
return `/sections/${outFile}`;
|
|
2804
2891
|
}
|
|
2805
2892
|
|
|
2806
2893
|
function buildManifestModuleSource(manifestEntries, defaultSection, siteConfig = {}, exportConfig = {}) {
|
|
@@ -3332,7 +3419,7 @@ async function processIncrementalManifest(sourceDir, distDir, tenantId, changedF
|
|
|
3332
3419
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
3333
3420
|
// Use manifest section ID if available, otherwise fall back to filename
|
|
3334
3421
|
const sectionId = fileToSectionId.get(relPath) || path.basename(relPath, ext);
|
|
3335
|
-
const targetPath = path.join(sectionsDir, `${sectionId}.js`);
|
|
3422
|
+
const targetPath = path.join(sectionsDir, `${encodePathForFilename(sectionId)}.js`);
|
|
3336
3423
|
|
|
3337
3424
|
try {
|
|
3338
3425
|
if (ext === '.md' || ext === '.markdown') {
|
|
@@ -3342,7 +3429,8 @@ async function processIncrementalManifest(sourceDir, distDir, tenantId, changedF
|
|
|
3342
3429
|
contentRoot: contentRoot.basePath || contentDir,
|
|
3343
3430
|
sectionIndex,
|
|
3344
3431
|
linkWarnings,
|
|
3345
|
-
strictLinks: options.strictLinks !== false
|
|
3432
|
+
strictLinks: options.strictLinks !== false,
|
|
3433
|
+
collections: Array.isArray(config.collections) ? config.collections : []
|
|
3346
3434
|
};
|
|
3347
3435
|
await ensureMarkdownModule(sourcePath, targetPath, linkContext);
|
|
3348
3436
|
console.log(` ↳ updated: ${sectionId} (markdown)`);
|
|
@@ -3608,7 +3696,15 @@ async function main() {
|
|
|
3608
3696
|
return results;
|
|
3609
3697
|
}
|
|
3610
3698
|
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3699
|
+
// Only auto-run when this file is the process entrypoint, so unit tests can
|
|
3700
|
+
// import named exports (e.g. markdownToHtml — #19) without triggering main().
|
|
3701
|
+
const __isMainModule = process.argv[1]
|
|
3702
|
+
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
3703
|
+
if (__isMainModule) {
|
|
3704
|
+
main().catch((err) => {
|
|
3705
|
+
console.error(err);
|
|
3706
|
+
process.exit(1);
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
export { markdownToHtml };
|
package/site/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContent as c,flattenManifest as r,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const y=document.getElementById("app"),g=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),N=document.getElementById("commandList"),w=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,I=new Map,M=new Set;let A=[],S=0,P=!1,q=(localStorage.getItem(k)||"").trim(),B=!1;function R(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function F(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);P&&_(),o&&j(o,!0),B=a||Boolean(q),location.hash.replace("#","")===s?O():location.hash=`#${s}`}function D(){return location.hash.replace("#","")||t}async function O(){const e=D(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:c}=t;l===e?(c&&j(c,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,c),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(y.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();y.innerHTML=o.html||"",await h(y),await f(y),function(e){const t=y.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const c=document.createElement("nav");if(c.className="bottom-nav",o&&c.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),F(i.id)}),e.appendChild(t),c.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",c.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),F(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',c.appendChild(e)}(y.querySelector("section")||y).appendChild(c)}(e.id),y.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(y),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=B;B=!1,K(l),requestAnimationFrame(()=>y.focus())}(o)):location.replace(`#${l}`)}function j(e,t){if(!e)return;const n=I.get(e);t?(M.add(e),n&&n.group.classList.add("expanded")):(M.delete(e),n&&n.group.classList.remove("expanded"))}function W(){if(!T||!x)return;P=!0,T.hidden=!1;const e=q;x.value=e,z(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function _(){T&&x&&(P=!1,T.hidden=!0,x.blur())}!function(){g.innerHTML="",H.clear(),I.clear();let t=M.size>0;e.forEach((e,n)=>{if(e.url){const t=R(e,"nav-leaf");return void g.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),F(e.id,{scrollToHighlight:Boolean(q)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!M.has(e.id);j(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!M.has(e.id);j(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!M.has(e.id);j(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=R(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);j(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=R(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);j(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=R(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);j(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=R(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>F(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const o=M.has(e.id)&&!e.collapsed;return void j(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>F(e.id,{scrollToHighlight:Boolean(q)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:s});const o=M.has(e.id)&&!e.collapsed;return void j(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>F(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const s=M.has(e.id)&&!e.collapsed;return void j(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>F(e.id,{scrollToHighlight:Boolean(q)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),g.appendChild(n),H.set(e.id,s),I.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(M.has(e.id)||!t&&!M.size);j(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>F(e.id,{scrollToHighlight:Boolean(q)})),g.appendChild(t),H.set(e.id,t)}})}(),q&&(B=!0),window.addEventListener("hashchange",()=>{q&&(B=!0),O()}),b.textContent=(new Date).getFullYear(),O(),L&&T&&x&&N&&(L.addEventListener("click",()=>{P?_():W()}),x.addEventListener("input",()=>{const e=x.value;J(e,!0),z(e)}),x.addEventListener("keydown",e=>{const t=A.length-1;if("ArrowDown"===e.key)e.preventDefault(),S=Math.min(t,S+1),X();else if("ArrowUp"===e.key)e.preventDefault(),S=Math.max(0,S-1),X();else if("Enter"===e.key){e.preventDefault();const t=A[S];t&&(J(x.value,!0),F(t.id,{scrollToHighlight:!0}),_())}else"Escape"===e.key&&(e.preventDefault(),_())}),N.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(J(x.value,!0),F(n,{scrollToHighlight:!0}),_())}),T.addEventListener("click",e=>{e.target===T&&_()}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),P?_():W()):"Escape"===e.key&&P&&(e.preventDefault(),_())}));let V=null,U=!1;async function z(t){N&&(!U&&t.trim()&&(U=!0,N.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(V),V=setTimeout(async()=>{A=await c(e,t);const n=D();S=d(A,n),function(){if(N){if(N.innerHTML="",!A.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void N.appendChild(e)}A.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),N.appendChild(t)})}}(),X(),U=!1},t.trim()?150:0))}function X(){N&&Array.from(N.children).forEach((e,t)=>{const n=t===S&&A.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function G(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function J(e,t=!1){q=e.trim(),t&&(q?localStorage.setItem(k,q):localStorage.removeItem(k)),K()}function K(e=!1){y&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),c=[];let r;for(;r=i.nextNode();){const e=r.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&c.push(r)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");c.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(y,q,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=D(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],c=n.length;let r=0;for(const e of n){r++;const n=r/c*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${r} of ${c}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=G((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),w&&C&&(w.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),w.setAttribute("aria-expanded","true"))}),g.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!w.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}));
|
|
1
|
+
import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContent as r,flattenManifest as c,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const y=document.getElementById("app"),g=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),w=document.getElementById("commandList"),N=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,I=new Map,M=new Set;let S=[],A=0,P=!1,q=(localStorage.getItem(k)||"").trim(),B=!1;function D(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function R(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);P&&W(),o&&O(o,!0),B=a||Boolean(q),location.hash.replace("#","")===s?j():location.hash=`#${s}`}function F(){return location.hash.replace("#","")||t}async function j(){const e=F(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:r}=t;l===e?(r&&O(r,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,r),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(y.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();y.innerHTML=o.html||"",function(e){if(!e||!e.showDate&&!e.showReadingTime&&!e.showSummary)return;const t=(y.querySelector(".doc-content")||y.querySelector("article, section")||y).querySelector("h1");if(!t)return;const n=[];e.showDate&&e.date&&n.push(function(e){const t=String(e||"").trim();if(!t)return"";const n=/^\d{4}-\d{2}-\d{2}$/.test(t)?`${t}T00:00:00Z`:t,a=new Date(n);return Number.isNaN(a.getTime())?t:new Intl.DateTimeFormat(void 0,{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}).format(a)}(e.date)),e.showReadingTime&&e.reading_time&&n.push(`${e.reading_time} min read`);let a=t;if(n.length>0){const e=document.createElement("p");e.className="doc-meta",e.textContent=n.join(" · "),t.after(e),a=e}if(e.showSummary&&e.summary){const t=document.createElement("p");t.className="doc-summary",t.textContent=e.summary,a.after(t)}}(e),await h(y),await f(y),function(e){const t=y.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const r=document.createElement("nav");if(r.className="bottom-nav",o&&r.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),R(i.id)}),e.appendChild(t),r.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",r.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),R(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',r.appendChild(e)}(y.querySelector("section")||y).appendChild(r)}(e.id),y.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(y),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=B;B=!1,K(l),requestAnimationFrame(()=>y.focus())}(o)):location.replace(`#${l}`)}function O(e,t){if(!e)return;const n=I.get(e);t?(M.add(e),n&&n.group.classList.add("expanded")):(M.delete(e),n&&n.group.classList.remove("expanded"))}function _(){if(!T||!x)return;P=!0,T.hidden=!1;const e=q;x.value=e,z(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function W(){T&&x&&(P=!1,T.hidden=!0,x.blur())}!function(){g.innerHTML="",H.clear(),I.clear();let t=M.size>0;e.forEach((e,n)=>{if(e.url){const t=D(e,"nav-leaf");return void g.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),R(e.id,{scrollToHighlight:Boolean(q)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!M.has(e.id);O(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!M.has(e.id);O(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:s});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const s=M.has(e.id)&&!e.collapsed;return void O(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),g.appendChild(n),H.set(e.id,s),I.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(M.has(e.id)||!t&&!M.size);O(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),g.appendChild(t),H.set(e.id,t)}})}(),q&&(B=!0),window.addEventListener("hashchange",()=>{q&&(B=!0),j()}),b.textContent=(new Date).getFullYear(),j(),L&&T&&x&&w&&(L.addEventListener("click",()=>{P?W():_()}),x.addEventListener("input",()=>{const e=x.value;J(e,!0),z(e)}),x.addEventListener("keydown",e=>{const t=S.length-1;if("ArrowDown"===e.key)e.preventDefault(),A=Math.min(t,A+1),X();else if("ArrowUp"===e.key)e.preventDefault(),A=Math.max(0,A-1),X();else if("Enter"===e.key){e.preventDefault();const t=S[A];t&&(J(x.value,!0),R(t.id,{scrollToHighlight:!0}),W())}else"Escape"===e.key&&(e.preventDefault(),W())}),w.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(J(x.value,!0),R(n,{scrollToHighlight:!0}),W())}),T.addEventListener("click",e=>{e.target===T&&W()}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),P?W():_()):"Escape"===e.key&&P&&(e.preventDefault(),W())}));let U=null,V=!1;async function z(t){w&&(!V&&t.trim()&&(V=!0,w.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(U),U=setTimeout(async()=>{S=await r(e,t);const n=F();A=d(S,n),function(){if(w){if(w.innerHTML="",!S.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void w.appendChild(e)}S.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),w.appendChild(t)})}}(),X(),V=!1},t.trim()?150:0))}function X(){w&&Array.from(w.children).forEach((e,t)=>{const n=t===A&&S.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function G(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function J(e,t=!1){q=e.trim(),t&&(q?localStorage.setItem(k,q):localStorage.removeItem(k)),K()}function K(e=!1){y&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),r=[];let c;for(;c=i.nextNode();){const e=c.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&r.push(c)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");r.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(y,q,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=F(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],r=n.length;let c=0;for(const e of n){c++;const n=c/r*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${c} of ${r}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=G((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),N&&C&&(N.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),N.setAttribute("aria-expanded","true"))}),g.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!N.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}));
|
package/site/index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
6
|
<title>Pagenary Docs</title>
|
|
7
7
|
<meta name="description" content="Pagenary developer documentation — building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
|
|
8
|
-
<link rel="icon" type="image/png" href="
|
|
9
|
-
<link rel="stylesheet" href="
|
|
10
|
-
<meta name="x-build" content="2026-
|
|
8
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
9
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
10
|
+
<meta name="x-build" content="2026-06-13T16:49:25.891Z" />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<a class="skip-link" href="#app">Skip to content</a>
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
<ul id="commandList" class="cmd-list" role="listbox"></ul>
|
|
53
53
|
</div>
|
|
54
54
|
</div>
|
|
55
|
-
<script type="module" src="
|
|
55
|
+
<script type="module" src="/app.js"></script>
|
|
56
56
|
</body>
|
|
57
57
|
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function sectionEntry(t,n){const e="string"==typeof t?t:t.id,r=n(e,"string"==typeof t?{}:{title:t.title,summary:t.summary});return{id:e,title:r.title,summary:r.summary,module
|
|
1
|
+
export function sectionEntry(t,n){const e="string"==typeof t?t:t.id,r=n(e,"string"==typeof t?{}:{title:t.title,summary:t.summary});return{id:e,title:r.title,summary:r.summary,module:`/sections/${e}.js`}}export function groupEntry(t,n){const{id:e,title:r,summary:i,sections:s}=t;return{id:e,title:r,summary:i,subsections:s.map(t=>sectionEntry(t,n))}}export function buildSectionIndex(t){const n=new Map;function e(t,r=null){r&&(t.parentId=r),n.set(t.id,t),Array.isArray(t.subsections)&&t.subsections.forEach(n=>e(n,t.id))}return t.forEach(t=>e(t)),n}export function createFindSection(t){return function(n){return t.get(n)||null}}
|
package/site/manifest.js
CHANGED
|
@@ -3,7 +3,7 @@ export const MANIFEST = [
|
|
|
3
3
|
"id": "welcome",
|
|
4
4
|
"title": "Welcome",
|
|
5
5
|
"summary": "What Pagenary is and how this dogfooded portal is built.",
|
|
6
|
-
"module": "
|
|
6
|
+
"module": "/sections/welcome.js"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"id": "getting-started",
|
|
@@ -14,7 +14,7 @@ export const MANIFEST = [
|
|
|
14
14
|
"id": "quickstart",
|
|
15
15
|
"title": "Quickstart",
|
|
16
16
|
"summary": "Install, build the default bundle, and serve it locally.",
|
|
17
|
-
"module": "
|
|
17
|
+
"module": "/sections/quickstart.js"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
@@ -27,19 +27,19 @@ export const MANIFEST = [
|
|
|
27
27
|
"id": "developer-guide",
|
|
28
28
|
"title": "Developer Guide",
|
|
29
29
|
"summary": "Project layout, scripts, and the content authoring workflow.",
|
|
30
|
-
"module": "
|
|
30
|
+
"module": "/sections/developer-guide.js"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"id": "tenant-config",
|
|
34
34
|
"title": "Tenant Configuration",
|
|
35
35
|
"summary": "Every config.json option: branding, theming, SEO, and export.",
|
|
36
|
-
"module": "
|
|
36
|
+
"module": "/sections/tenant-config.js"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"id": "extending",
|
|
40
40
|
"title": "Extending",
|
|
41
41
|
"summary": "Add section templates, content types, and build behaviors.",
|
|
42
|
-
"module": "
|
|
42
|
+
"module": "/sections/extending.js"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
},
|
|
@@ -52,25 +52,25 @@ export const MANIFEST = [
|
|
|
52
52
|
"id": "architecture",
|
|
53
53
|
"title": "Architecture",
|
|
54
54
|
"summary": "The static SPA pattern, build pipeline, and tenant content model.",
|
|
55
|
-
"module": "
|
|
55
|
+
"module": "/sections/architecture.js"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"id": "api",
|
|
59
59
|
"title": "API Reference",
|
|
60
60
|
"summary": "Module-level documentation for the publisher internals.",
|
|
61
|
-
"module": "
|
|
61
|
+
"module": "/sections/api.js"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"id": "deployment",
|
|
65
65
|
"title": "Deployment",
|
|
66
66
|
"summary": "Hosting the static output and multi-tenant domain routing.",
|
|
67
|
-
"module": "
|
|
67
|
+
"module": "/sections/deployment.js"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"id": "seo-strategy",
|
|
71
71
|
"title": "SEO Strategy",
|
|
72
72
|
"summary": "Metadata, hash-routing considerations, and discoverability.",
|
|
73
|
-
"module": "
|
|
73
|
+
"module": "/sections/seo-strategy.js"
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
}
|
package/site/pages/api.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "API Reference",
|
|
28
28
|
"description": "Module-level documentation for the publisher internals.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/api.html",
|
|
30
|
-
"dateModified": "2026-
|
|
30
|
+
"dateModified": "2026-06-13",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/api.html"
|
|
@@ -318,19 +318,102 @@ interface Chapter {
|
|
|
318
318
|
<li>`--dev` - Skip minification</li>
|
|
319
319
|
</ul>
|
|
320
320
|
<h3 id="scriptsbuild-tenantsjs">scripts/build-tenants.js</h3>
|
|
321
|
-
<p>Multi-tenant build orchestrator
|
|
321
|
+
<p>Multi-tenant build orchestrator. It processes tenant content, applies branding</p>
|
|
322
|
+
<p>and overrides, copies public assets, then calls the build library modules for</p>
|
|
323
|
+
<p>SEO artifacts and collections.</p>
|
|
322
324
|
<pre><code class="language-bash">node scripts/build-tenants.js [tenant-id] [--incremental]</code></pre>
|
|
323
325
|
<p>Arguments:</p>
|
|
324
326
|
<ul>
|
|
325
327
|
<li>`tenant-id` - Build specific tenant (omit for all)</li>
|
|
326
328
|
<li>`--incremental` - Only rebuild changed files</li>
|
|
327
329
|
</ul>
|
|
330
|
+
<p>See <a href="#build-library-modules">Build Library Modules</a> for the helper modules used</p>
|
|
331
|
+
<p>by this orchestrator.</p>
|
|
328
332
|
<h3 id="scriptsservejs">scripts/serve.js</h3>
|
|
329
333
|
<p>Development server.</p>
|
|
330
334
|
<pre><code class="language-bash">node scripts/serve.js [--port=5173]</code></pre>
|
|
331
335
|
<h3 id="scriptssync-docsjs">scripts/sync-docs.js</h3>
|
|
332
336
|
<p>Regenerate section template modules.</p>
|
|
333
337
|
<pre><code class="language-bash">node scripts/sync-docs.js</code></pre>
|
|
338
|
+
<h2 id="build-library-modules">Build Library Modules</h2>
|
|
339
|
+
<p>These modules are called by `scripts/build-tenants.js` during tenant builds.</p>
|
|
340
|
+
<p>They generate files that ship in each tenant output, so they are part of the</p>
|
|
341
|
+
<p>build-time API surface even though they do not run in the browser.</p>
|
|
342
|
+
<h3 id="scriptslibseo-generatorjs">scripts/lib/seo-generator.js</h3>
|
|
343
|
+
<p>Generates crawler-facing SEO artifacts after tenant content, branding, theme,</p>
|
|
344
|
+
<p>welcome, and public assets have been written.</p>
|
|
345
|
+
<h4 id="exports-2">Exports</h4>
|
|
346
|
+
<p><strong>`resolveBaseUrl(config?: object): string`</strong></p>
|
|
347
|
+
<p>Resolve the tenant absolute base URL. `seo.siteUrl` takes precedence over</p>
|
|
348
|
+
<p>`domain`; domains without a scheme are treated as HTTPS. Returns an empty</p>
|
|
349
|
+
<p>string when neither value is configured.</p>
|
|
350
|
+
<pre><code class="language-javascript">const baseUrl = resolveBaseUrl({ domain: 'docs.example.com' });
|
|
351
|
+
// 'https://docs.example.com'</code></pre>
|
|
352
|
+
<p><strong>`resolveOgImage(config?: object, baseUrl?: string): string`</strong></p>
|
|
353
|
+
<p>Resolve `seo.ogImage` for Open Graph and Twitter metadata. Absolute image URLs</p>
|
|
354
|
+
<p>pass through; site-relative paths are joined to `baseUrl` when available.</p>
|
|
355
|
+
<p><strong>`generateSeoArtifacts(distDir: string, config: object): Promise<void>`</strong></p>
|
|
356
|
+
<p>Generate all enabled SEO artifacts for the tenant output directory:</p>
|
|
357
|
+
<ul>
|
|
358
|
+
<li>`sitemap.xml`</li>
|
|
359
|
+
<li>`robots.txt`</li>
|
|
360
|
+
<li>`llms.txt`</li>
|
|
361
|
+
<li>static crawler snapshots under `pages/`</li>
|
|
362
|
+
<li>JSON-LD embedded in generated static pages</li>
|
|
363
|
+
</ul>
|
|
364
|
+
<p>Called from `scripts/build-tenants.js` after `.public/` assets are copied and</p>
|
|
365
|
+
<p>before collection manifests are generated.</p>
|
|
366
|
+
<pre><code class="language-javascript">await generateSeoArtifacts(distDir, config);</code></pre>
|
|
367
|
+
<p><strong>`generateSitemap(distDir: string, manifest: SectionEntry[], config: object): Promise<void>`</strong></p>
|
|
368
|
+
<p>Write `sitemap.xml` from the generated navigation manifest.</p>
|
|
369
|
+
<p><strong>`generateRobotsTxt(distDir: string, config: object): Promise<void>`</strong></p>
|
|
370
|
+
<p>Write `robots.txt`, including a sitemap pointer when a base URL is configured.</p>
|
|
371
|
+
<p><strong>`generateStaticSnapshots(distDir: string, manifest: SectionEntry[], config: object): Promise<void>`</strong></p>
|
|
372
|
+
<p>Write static HTML snapshots for each navigable section so crawlers can consume</p>
|
|
373
|
+
<p>content without executing the SPA.</p>
|
|
374
|
+
<p><strong>`generateLlmsTxt(distDir: string, manifest: SectionEntry[], config: object): Promise<void>`</strong></p>
|
|
375
|
+
<p>Write `llms.txt` with tenant-level metadata and links to generated static pages.</p>
|
|
376
|
+
<h3 id="scriptslibcollections-generatorjs">scripts/lib/collections-generator.js</h3>
|
|
377
|
+
<p>Generates per-collection manifests and optional RSS feeds from Markdown posts'</p>
|
|
378
|
+
<p>front matter. Collections are opt-in through `config.collections`.</p>
|
|
379
|
+
<h4 id="exports-3">Exports</h4>
|
|
380
|
+
<p><strong>`generateCollections(distDir: string, config: object, contentBasePath: string): Promise<void>`</strong></p>
|
|
381
|
+
<p>For each collection config, read posts under `contentBasePath/<collection.path>`</p>
|
|
382
|
+
<p>and emit artifacts under the configured route:</p>
|
|
383
|
+
<ul>
|
|
384
|
+
<li>`<route>/index.json` when `manifest !== false`</li>
|
|
385
|
+
<li>`<route>/feed.xml` when `feed === true`</li>
|
|
386
|
+
</ul>
|
|
387
|
+
<p>The `index.json` entry shape is:</p>
|
|
388
|
+
<pre><code class="language-typescript">interface CollectionEntry {
|
|
389
|
+
slug: string;
|
|
390
|
+
title: string;
|
|
391
|
+
date: string | null;
|
|
392
|
+
summary: string;
|
|
393
|
+
hero: string | null;
|
|
394
|
+
tags: string[];
|
|
395
|
+
reading_time: number;
|
|
396
|
+
canonical: string;
|
|
397
|
+
path: string;
|
|
398
|
+
}</code></pre>
|
|
399
|
+
<p>Called from `scripts/build-tenants.js` after SEO artifacts are generated:</p>
|
|
400
|
+
<pre><code class="language-javascript">const collectionRoot = await findContentRoot(sourceDir);
|
|
401
|
+
await generateCollections(distDir, config, collectionRoot.basePath);</code></pre>
|
|
402
|
+
<h3 id="scriptslibfrontmatterjs">scripts/lib/frontmatter.js</h3>
|
|
403
|
+
<p>Parses the Markdown front-matter subset used by collection posts and tenant</p>
|
|
404
|
+
<p>content metadata.</p>
|
|
405
|
+
<h4 id="exports-4">Exports</h4>
|
|
406
|
+
<p><strong>`parseFrontmatter(raw: string): { data: Record<string, any>, body: string }`</strong></p>
|
|
407
|
+
<p>Parse a leading `---` fenced block of `key: value` pairs. Values are coerced to</p>
|
|
408
|
+
<p>booleans, numbers, `null`, quoted strings, or inline lists such as</p>
|
|
409
|
+
<p>`[docs, release]`. Nested maps are not supported; unsupported values remain</p>
|
|
410
|
+
<p>strings.</p>
|
|
411
|
+
<pre><code class="language-javascript">const { data, body } = parseFrontmatter(markdown);</code></pre>
|
|
412
|
+
<p><strong>`estimateReadingTime(body: string): number`</strong></p>
|
|
413
|
+
<p>Estimate reading time in minutes at roughly 200 words per minute, with a</p>
|
|
414
|
+
<p>minimum of `1`.</p>
|
|
415
|
+
<p><strong>`firstHeading(body: string): string | null`</strong></p>
|
|
416
|
+
<p>Return the first Markdown H1 (`# Title`) in the body, or `null` when none is present.</p>
|
|
334
417
|
</div>
|
|
335
418
|
</section>
|
|
336
419
|
</div>
|