@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagenary/publisher",
3
- "version": "2026.5.3",
3
+ "version": "2026.6.0",
4
4
  "type": "module",
5
5
  "description": "Multi-tenant static publishing component for Pagenary platform.",
6
6
  "license": "AGPL-3.0-or-later",
@@ -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
- sections.push({
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
- module: `./sections/${outFile}`,
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: `./sections/${outFile}` };
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 `./sections/${outFile}`;
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
- main().catch((err) => {
3612
- console.error(err);
3613
- process.exit(1);
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="./favicon.png" />
9
- <link rel="stylesheet" href="./styles.css" />
10
- <meta name="x-build" content="2026-05-27T07:30:04.796Z" />
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="./app.js"></script>
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:`./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}}
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": "./sections/welcome.js"
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": "./sections/quickstart.js"
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": "./sections/developer-guide.js"
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": "./sections/tenant-config.js"
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": "./sections/extending.js"
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": "./sections/architecture.js"
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": "./sections/api.js"
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": "./sections/deployment.js"
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": "./sections/seo-strategy.js"
73
+ "module": "/sections/seo-strategy.js"
74
74
  }
75
75
  ]
76
76
  }
@@ -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-05-27",
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.</p>
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: &#39;docs.example.com&#39; });
351
+ // &#39;https://docs.example.com&#39;</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&lt;void&gt;`</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&lt;void&gt;`</strong></p>
368
+ <p>Write `sitemap.xml` from the generated navigation manifest.</p>
369
+ <p><strong>`generateRobotsTxt(distDir: string, config: object): Promise&lt;void&gt;`</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&lt;void&gt;`</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&lt;void&gt;`</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&#39;</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&lt;void&gt;`</strong></p>
381
+ <p>For each collection config, read posts under `contentBasePath/&lt;collection.path&gt;`</p>
382
+ <p>and emit artifacts under the configured route:</p>
383
+ <ul>
384
+ <li>`&lt;route&gt;/index.json` when `manifest !== false`</li>
385
+ <li>`&lt;route&gt;/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&lt;string, any&gt;, 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>