@pagenary/publisher 2026.5.3 → 2026.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagenary/publisher",
3
- "version": "2026.5.3",
3
+ "version": "2026.5.4",
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;
@@ -3608,7 +3615,15 @@ async function main() {
3608
3615
  return results;
3609
3616
  }
3610
3617
 
3611
- main().catch((err) => {
3612
- console.error(err);
3613
- process.exit(1);
3614
- });
3618
+ // Only auto-run when this file is the process entrypoint, so unit tests can
3619
+ // import named exports (e.g. markdownToHtml — #19) without triggering main().
3620
+ const __isMainModule = process.argv[1]
3621
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
3622
+ if (__isMainModule) {
3623
+ main().catch((err) => {
3624
+ console.error(err);
3625
+ process.exit(1);
3626
+ });
3627
+ }
3628
+
3629
+ export { markdownToHtml };
package/site/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <meta name="description" content="Pagenary developer documentation — building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
8
8
  <link rel="icon" type="image/png" href="./favicon.png" />
9
9
  <link rel="stylesheet" href="./styles.css" />
10
- <meta name="x-build" content="2026-05-27T07:30:04.796Z" />
10
+ <meta name="x-build" content="2026-05-28T16:02:34.220Z" />
11
11
  </head>
12
12
  <body>
13
13
  <a class="skip-link" href="#app">Skip to content</a>
@@ -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-05-28",
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>
@@ -27,7 +27,7 @@
27
27
  "headline": "Architecture",
28
28
  "description": "The static SPA pattern, build pipeline, and tenant content model.",
29
29
  "url": "https://docs.pagenary.com/pages/architecture.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/architecture.html"
@@ -144,6 +144,42 @@
144
144
  <p>2. <strong>Minify</strong> - JavaScript via Terser (production)</p>
145
145
  <p>3. <strong>Brand</strong> - Apply tenant config (colors, text)</p>
146
146
  <p>4. <strong>Override</strong> - Replace files from `overrides/`</p>
147
+ <h3 id="seo-artifact-generation">SEO Artifact Generation</h3>
148
+ <p>After content processing, branding, tenant overrides, and `.public/` assets are</p>
149
+ <p>in place, `scripts/build-tenants.js` calls `scripts/lib/seo-generator.js` to emit</p>
150
+ <p>crawler-facing files into the tenant output directory:</p>
151
+ <ul>
152
+ <li>`sitemap.xml` from the generated manifest</li>
153
+ <li>`robots.txt` with a sitemap pointer</li>
154
+ <li>`llms.txt` for LLM-friendly site discovery</li>
155
+ <li>static snapshots under `pages/` for each navigable section</li>
156
+ <li>JSON-LD metadata embedded in the generated snapshots</li>
157
+ </ul>
158
+ <p>The generator resolves absolute URLs from `seo.siteUrl` or `domain`. Tenants can</p>
159
+ <p>disable the whole stage with `seo.enabled: false` or individual artifact switches.</p>
160
+ <h3 id="collection-manifests">Collection Manifests</h3>
161
+ <p>If `config.collections` is configured, `buildTenant()` resolves the tenant content</p>
162
+ <p>root and calls `scripts/lib/collections-generator.js` after SEO artifacts are</p>
163
+ <p>written. Each collection reads Markdown posts from its configured `path`, parses</p>
164
+ <p>front matter with `scripts/lib/frontmatter.js`, sorts entries by the configured</p>
165
+ <p>field/order, and writes machine-readable output under the collection route:</p>
166
+ <ul>
167
+ <li>`index.json` with `slug`, `title`, `date`, `summary`, `hero`, `tags`,</li>
168
+ </ul>
169
+ <p>`reading_time`, `canonical`, and `path`</p>
170
+ <ul>
171
+ <li>optional `feed.xml` when `feed: true`</li>
172
+ </ul>
173
+ <h3 id="build-flow">Build Flow</h3>
174
+ <pre><code class="language-text">resolve source
175
+ -&gt; run base build
176
+ -&gt; process tenant content and manifest
177
+ -&gt; apply overrides
178
+ -&gt; apply branding/theme/navigation/welcome
179
+ -&gt; copy .public assets
180
+ -&gt; generate SEO artifacts
181
+ -&gt; generate collection manifests/feeds
182
+ -&gt; copy or sync to target</code></pre>
147
183
  <h2 id="runtime-architecture">Runtime Architecture</h2>
148
184
  <h3 id="shell-layout">Shell Layout</h3>
149
185
  <pre><code>┌─────────────────────────────────────────────────────────┐
@@ -168,13 +204,20 @@
168
204
  └── lib/
169
205
  ├── search.js # Full-text search
170
206
  ├── router.js # Hash routing utilities
171
- └── export.js # Document export</code></pre>
207
+ └── export.js # Document export
208
+
209
+ scripts/lib/
210
+ ├── seo-generator.js # Build-time SEO artifacts
211
+ ├── collections-generator.js # Collection manifests and feeds
212
+ └── frontmatter.js # Markdown front-matter parsing</code></pre>
172
213
  <h3 id="core-flow">Core Flow</h3>
173
- <pre><code>Hash ChangeRouterManifest Lookup Module ImportRenderPost-Process
174
-
175
- ├── Mermaid Diagrams
176
- ├── Syntax Highlighting
177
- └── SEO Meta Tags</code></pre>
214
+ <pre><code>Build: ContentManifestBrandingPublic AssetsSEO Artifacts Collections
215
+
216
+ Runtime: Hash Change → Router → Manifest Lookup → Module Import → Render → Post-Process
217
+
218
+ ├── Mermaid Diagrams
219
+ ├── Syntax Highlighting
220
+ └── SEO Meta Tags</code></pre>
178
221
  <h2 id="key-components">Key Components</h2>
179
222
  <h3 id="router-appjs">Router (app.js)</h3>
180
223
  <p>Hash-based navigation with history support:</p>
@@ -27,7 +27,7 @@
27
27
  "headline": "Deployment",
28
28
  "description": "Hosting the static output and multi-tenant domain routing.",
29
29
  "url": "https://docs.pagenary.com/pages/deployment.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/deployment.html"
@@ -27,7 +27,7 @@
27
27
  "headline": "Developer Guide",
28
28
  "description": "Project layout, scripts, and the content authoring workflow.",
29
29
  "url": "https://docs.pagenary.com/pages/developer-guide.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/developer-guide.html"
@@ -137,6 +137,40 @@ npm run dev</code></pre>
137
137
  <li>Run `npm run build:tenants` after editing content. The script regenerates `dist/&lt;id&gt;/manifest.js` plus section modules so the navigation immediately reflects the manifest.</li>
138
138
  <li>Provide per-tenant overrides (styles, assets, app shell tweaks) inside `tenants/&lt;id&gt;/overrides/`; they copy into the tenant bundle after content generation, so you can replace generated files if needed.</li>
139
139
  </ul>
140
+ <h2 id="authoring-collection-posts">Authoring Collection Posts</h2>
141
+ <p>Use collections when a tenant needs a feed-like content group such as a blog,</p>
142
+ <p>release notes, news, or changelog. Configure the collection in</p>
143
+ <p>`TENANT-CONFIG.md` under `collections`, then add Markdown posts under the</p>
144
+ <p>configured content path.</p>
145
+ <p>Each post can start with front matter parsed by `scripts/lib/frontmatter.js`:</p>
146
+ <pre><code class="language-markdown">---
147
+ title: Launch Notes
148
+ date: 2026-05-28
149
+ summary: What changed in this release
150
+ tags: [release, platform]
151
+ hero: /assets/blog/launch.png
152
+ ---
153
+
154
+ # Launch Notes
155
+
156
+ Post content starts here.</code></pre>
157
+ <p>During `npm run build:tenants`, `scripts/lib/collections-generator.js` reads those</p>
158
+ <p>posts and writes `index.json` plus optional `feed.xml` under the collection</p>
159
+ <p>route. See `TENANT-CONFIG.md` for the collection config schema and `API.md` for</p>
160
+ <p>the generated entry shape.</p>
161
+ <h2 id="extending-seo-output">Extending SEO Output</h2>
162
+ <p>Tenant SEO settings live in `TENANT-CONFIG.md` under `seo`. The runtime</p>
163
+ <p>`src/seo.js` module updates browser metadata after navigation, while the build-time</p>
164
+ <p>`scripts/lib/seo-generator.js` module emits crawler-facing artifacts:</p>
165
+ <ul>
166
+ <li>`sitemap.xml`</li>
167
+ <li>`robots.txt`</li>
168
+ <li>`llms.txt`</li>
169
+ <li>static snapshots under `pages/`</li>
170
+ <li>JSON-LD metadata for generated pages</li>
171
+ </ul>
172
+ <p>Extend `scripts/lib/seo-generator.js` when the output artifact set changes, and update</p>
173
+ <p>`API.md` when adding or changing exported helpers.</p>
140
174
  <h2 id="tooling-philosophy">Tooling Philosophy</h2>
141
175
  <ul>
142
176
  <li>Zero framework lock-in; replace `app.js` with your own router if you outgrow it.</li>
@@ -27,7 +27,7 @@
27
27
  "headline": "Extending",
28
28
  "description": "Add section templates, content types, and build behaviors.",
29
29
  "url": "https://docs.pagenary.com/pages/extending.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/extending.html"
@@ -27,7 +27,7 @@
27
27
  "headline": "Quickstart",
28
28
  "description": "Install, build the default bundle, and serve it locally.",
29
29
  "url": "https://docs.pagenary.com/pages/quickstart.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/quickstart.html"
@@ -27,7 +27,7 @@
27
27
  "headline": "SEO Strategy",
28
28
  "description": "Metadata, hash-routing considerations, and discoverability.",
29
29
  "url": "https://docs.pagenary.com/pages/seo-strategy.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/seo-strategy.html"
@@ -27,7 +27,7 @@
27
27
  "headline": "Tenant Configuration",
28
28
  "description": "Every config.json option: branding, theming, SEO, and export.",
29
29
  "url": "https://docs.pagenary.com/pages/tenant-config.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/tenant-config.html"
@@ -27,7 +27,7 @@
27
27
  "headline": "Welcome",
28
28
  "description": "What Pagenary is and how this dogfooded portal is built.",
29
29
  "url": "https://docs.pagenary.com/pages/welcome.html",
30
- "dateModified": "2026-05-27",
30
+ "dateModified": "2026-05-28",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/welcome.html"
package/site/robots.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  # Pagenary Docs
2
- # Generated: 2026-05-27T07:30:05.130Z
2
+ # Generated: 2026-05-28T16:02:34.576Z
3
3
 
4
4
  User-agent: *
5
5
  Allow: /
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"api-reference\">API Reference</h1>\n<p>Complete reference for Pagenary Publisher modules and functions.</p>\n<h2 id=\"core-modules\">Core Modules</h2>\n<h3 id=\"appjs-shell-controller\">app.js - Shell Controller</h3>\n<p>Main application controller handling routing, navigation, and UI state.</p>\n<h4 id=\"functions\">Functions</h4>\n<p><strong>`navigate(id: string): void`</strong></p>\n<p>Navigate to a section by ID.</p>\n<pre><code class=\"language-javascript\">navigate(&#39;guides/getting-started&#39;);\n// Updates hash to #/guides/getting-started and renders section</code></pre>\n<p><strong>`handleRoute(): Promise&lt;void&gt;`</strong></p>\n<p>Process current URL hash and render corresponding section.</p>\n<p><strong>`loadSection(entry: SectionEntry): Promise&lt;void&gt;`</strong></p>\n<p>Load and render a section from the manifest.</p>\n<pre><code class=\"language-javascript\">const section = findSection(&#39;welcome&#39;);\nawait loadSection(section);</code></pre>\n<p><strong>`updateNavState(activeId: string): void`</strong></p>\n<p>Update navigation UI to reflect active section.</p>\n<p><strong>`openCommandPalette(): void`</strong></p>\n<p>Open the command palette (search interface).</p>\n<p><strong>`closeCommandPalette(): void`</strong></p>\n<p>Close the command palette.</p>\n<hr>\n<h3 id=\"manifestjs-navigation-registry\">manifest.js - Navigation Registry</h3>\n<p>Defines navigation structure and section metadata.</p>\n<h4 id=\"exports\">Exports</h4>\n<p><strong>`MANIFEST: SectionEntry[]`</strong></p>\n<p>Ordered array of sections defining navigation.</p>\n<pre><code class=\"language-javascript\">export const MANIFEST = [\n {\n id: &#39;welcome&#39;,\n title: &#39;Welcome&#39;,\n summary: &#39;Introduction&#39;,\n module: &#39;./sections/welcome.js&#39;\n },\n {\n id: &#39;guides&#39;,\n title: &#39;Guides&#39;,\n subsections: [\n { id: &#39;guides/setup&#39;, title: &#39;Setup&#39;, module: &#39;./sections/guides--setup.js&#39; }\n ]\n }\n];</code></pre>\n<p><strong>`DEFAULT_SECTION: string`</strong></p>\n<p>ID of the default section (first in manifest).</p>\n<p><strong>`SITE_CONFIG: SiteConfig`</strong></p>\n<p>Site configuration from build.</p>\n<pre><code class=\"language-javascript\">export const SITE_CONFIG = {\n title: &#39;My Docs&#39;,\n description: &#39;Documentation for My Product&#39;,\n brandMark: &#39;MY&#39;,\n brandSub: &#39;DOCS&#39;\n};</code></pre>\n<p><strong>`findSection(id: string): SectionEntry | undefined`</strong></p>\n<p>Look up a section by ID.</p>\n<pre><code class=\"language-javascript\">const section = findSection(&#39;guides/setup&#39;);\n// Returns { id: &#39;guides/setup&#39;, title: &#39;Setup&#39;, ... }</code></pre>\n<p><strong>`getAdjacentSections(id: string): { prev?: SectionEntry, next?: SectionEntry }`</strong></p>\n<p>Get previous and next sections for navigation.</p>\n<hr>\n<h3 id=\"seojs-metadata-helper\">seo.js - Metadata Helper</h3>\n<p>Manages document metadata for SEO.</p>\n<h4 id=\"functions-2\">Functions</h4>\n<p><strong>`updateMetaTags({ title: string, description?: string }): void`</strong></p>\n<p>Update document title and meta description.</p>\n<pre><code class=\"language-javascript\">updateMetaTags({\n title: &#39;Getting Started - My Docs&#39;,\n description: &#39;Learn how to get started with My Product&#39;\n});</code></pre>\n<hr>\n<h2 id=\"library-modules\">Library Modules</h2>\n<h3 id=\"libsearchjs-full-text-search\">lib/search.js - Full-Text Search</h3>\n<p>Search functionality with lazy content indexing.</p>\n<h4 id=\"functions-3\">Functions</h4>\n<p><strong>`escapeRegExp(value: string): string`</strong></p>\n<p>Escape special regex characters.</p>\n<pre><code class=\"language-javascript\">escapeRegExp(&#39;foo.bar&#39;); // &#39;foo\\\\.bar&#39;</code></pre>\n<p><strong>`flattenManifest(manifest: SectionEntry[]): FlatSection[]`</strong></p>\n<p>Flatten nested manifest into searchable sections.</p>\n<pre><code class=\"language-javascript\">const flat = flattenManifest(MANIFEST);\n// Returns all navigable sections with group info</code></pre>\n<p><strong>`buildSearchIndex(manifest: SectionEntry[]): Promise&lt;IndexedSection[]&gt;`</strong></p>\n<p>Build search index by loading all section modules. Cached after first call.</p>\n<pre><code class=\"language-javascript\">const index = await buildSearchIndex(MANIFEST);\n// Each entry has searchContent: lowercase text for matching</code></pre>\n<p><strong>`filterSections(manifest: SectionEntry[], query: string): FlatSection[]`</strong></p>\n<p>Synchronous title/summary search (no content).</p>\n<pre><code class=\"language-javascript\">const results = filterSections(MANIFEST, &#39;setup&#39;);</code></pre>\n<p><strong>`searchContent(manifest: SectionEntry[], query: string): Promise&lt;IndexedSection[]&gt;`</strong></p>\n<p>Full-text search across all content.</p>\n<pre><code class=\"language-javascript\">const results = await searchContent(MANIFEST, &#39;authentication&#39;);\n// Searches titles, summaries, and full content</code></pre>\n<p><strong>`findPreferredIndex(entries: Section[], currentId: string): number`</strong></p>\n<p>Find index of current section in filtered results.</p>\n<hr>\n<h3 id=\"librouterjs-hash-routing\">lib/router.js - Hash Routing</h3>\n<p>URL hash parsing and resolution.</p>\n<h4 id=\"functions-4\">Functions</h4>\n<p><strong>`resolveTarget(hash: string): string`</strong></p>\n<p>Extract section ID from URL hash.</p>\n<pre><code class=\"language-javascript\">resolveTarget(&#39;#/guides/setup&#39;); // &#39;guides/setup&#39;\nresolveTarget(&#39;#&#39;); // &#39;&#39; (empty)</code></pre>\n<p><strong>`resolveEntry(entry: SectionEntry): SectionEntry`</strong></p>\n<p>Resolve a section entry, following redirects if needed.</p>\n<hr>\n<h3 id=\"libexportjs-document-export\">lib/export.js - Document Export</h3>\n<p>Compose sections into exportable HTML documents.</p>\n<h4 id=\"functions-5\">Functions</h4>\n<p><strong>`composeExportDocument(chapters: Chapter[]): string`</strong></p>\n<p>Generate complete HTML document from chapters.</p>\n<pre><code class=\"language-javascript\">const html = composeExportDocument([\n { section: { title: &#39;Welcome&#39; }, html: &#39;&lt;p&gt;Hello&lt;/p&gt;&#39; },\n { section: { title: &#39;Setup&#39; }, html: &#39;&lt;p&gt;Install...&lt;/p&gt;&#39; }\n]);\n// Returns complete HTML with TOC, styles, syntax highlighting</code></pre>\n<p><strong>`collectExportableSections(manifest: SectionEntry[]): SectionEntry[]`</strong></p>\n<p>Get all sections that can be exported (have module paths).</p>\n<pre><code class=\"language-javascript\">const sections = collectExportableSections(MANIFEST);</code></pre>\n<hr>\n<h2 id=\"enhancement-modules\">Enhancement Modules</h2>\n<h3 id=\"mermaid-initjs-diagram-rendering\">mermaid-init.js - Diagram Rendering</h3>\n<p>Lazy-load and render Mermaid diagrams.</p>\n<h4 id=\"functions-6\">Functions</h4>\n<p><strong>`renderMermaidBlocks(container: Element): Promise&lt;void&gt;`</strong></p>\n<p>Find and render all Mermaid code blocks in a container.</p>\n<pre><code class=\"language-javascript\">await renderMermaidBlocks(document.querySelector(&#39;.canvas&#39;));\n// Replaces ```mermaid blocks with rendered SVGs</code></pre>\n<hr>\n<h3 id=\"syntax-highlightjs-code-highlighting\">syntax-highlight.js - Code Highlighting</h3>\n<p>Lazy-load and apply Prism.js syntax highlighting.</p>\n<h4 id=\"functions-7\">Functions</h4>\n<p><strong>`highlightCodeBlocks(container: Element): Promise&lt;void&gt;`</strong></p>\n<p>Highlight all code blocks in a container.</p>\n<pre><code class=\"language-javascript\">await highlightCodeBlocks(document.querySelector(&#39;.canvas&#39;));\n// Applies syntax highlighting to all &lt;code&gt; elements</code></pre>\n<p>Supported languages: JavaScript, TypeScript, Python, Rust, Go, C, JSON, YAML, Bash, SQL, Solidity.</p>\n<hr>\n<h2 id=\"section-module-contract\">Section Module Contract</h2>\n<p>All section modules must export a `load` function:</p>\n<pre><code class=\"language-javascript\">/**\n * Load section content.\n * @returns {Promise&lt;{ html: string, afterRender?: (container: Element) =&gt; void }&gt;}\n */\nexport async function load() {\n return {\n html: &#39;&lt;section class=&quot;section doc&quot;&gt;...&lt;/section&gt;&#39;,\n\n // Optional: called after HTML is inserted into DOM\n afterRender(container) {\n // DOM manipulation, event listeners, etc.\n }\n };\n}</code></pre>\n<h3 id=\"examples\">Examples</h3>\n<p><strong>Static Content:</strong></p>\n<pre><code class=\"language-javascript\">export async function load() {\n return {\n html: `\n &lt;section class=&quot;section doc markdown&quot;&gt;\n &lt;div class=&quot;doc-content&quot;&gt;\n &lt;h1&gt;Welcome&lt;/h1&gt;\n &lt;p&gt;Hello, world!&lt;/p&gt;\n &lt;/div&gt;\n &lt;/section&gt;\n `\n };\n}</code></pre>\n<p><strong>Dynamic Content:</strong></p>\n<pre><code class=\"language-javascript\">export async function load() {\n const data = await fetch(&#39;/api/stats.json&#39;).then(r =&gt; r.json());\n\n return {\n html: `\n &lt;section class=&quot;section doc&quot;&gt;\n &lt;h1&gt;Stats&lt;/h1&gt;\n &lt;p&gt;Count: ${data.count}&lt;/p&gt;\n &lt;/section&gt;\n `,\n afterRender(container) {\n container.querySelector(&#39;button&#39;)?.addEventListener(&#39;click&#39;, refresh);\n }\n };\n}</code></pre>\n<hr>\n<h2 id=\"type-definitions\">Type Definitions</h2>\n<pre><code class=\"language-typescript\">interface SectionEntry {\n id: string;\n title: string;\n summary?: string;\n module?: string;\n subsections?: SectionEntry[];\n exclude?: boolean;\n}\n\ninterface FlatSection extends SectionEntry {\n group?: string; // Parent group title\n}\n\ninterface IndexedSection extends FlatSection {\n searchContent: string; // Lowercase text for searching\n}\n\ninterface SiteConfig {\n title: string;\n description?: string;\n brandMark?: string;\n brandSub?: string;\n tagline?: string;\n copyright?: string;\n}\n\ninterface Chapter {\n section: { title: string; summary?: string };\n html: string;\n}</code></pre>\n<hr>\n<h2 id=\"build-scripts\">Build Scripts</h2>\n<h3 id=\"scriptsbuildjs\">scripts/build.js</h3>\n<p>Core build script for copying and minifying assets.</p>\n<pre><code class=\"language-bash\">node scripts/build.js [--dev]</code></pre>\n<p>Options:</p>\n<ul>\n<li>`--dev` - Skip minification</li>\n</ul>\n<h3 id=\"scriptsbuild-tenantsjs\">scripts/build-tenants.js</h3>\n<p>Multi-tenant build orchestrator.</p>\n<pre><code class=\"language-bash\">node scripts/build-tenants.js [tenant-id] [--incremental]</code></pre>\n<p>Arguments:</p>\n<ul>\n<li>`tenant-id` - Build specific tenant (omit for all)</li>\n<li>`--incremental` - Only rebuild changed files</li>\n</ul>\n<h3 id=\"scriptsservejs\">scripts/serve.js</h3>\n<p>Development server.</p>\n<pre><code class=\"language-bash\">node scripts/serve.js [--port=5173]</code></pre>\n<h3 id=\"scriptssync-docsjs\">scripts/sync-docs.js</h3>\n<p>Regenerate section template modules.</p>\n<pre><code class=\"language-bash\">node scripts/sync-docs.js</code></pre>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"api-reference\">API Reference</h1>\n<p>Complete reference for Pagenary Publisher modules and functions.</p>\n<h2 id=\"core-modules\">Core Modules</h2>\n<h3 id=\"appjs-shell-controller\">app.js - Shell Controller</h3>\n<p>Main application controller handling routing, navigation, and UI state.</p>\n<h4 id=\"functions\">Functions</h4>\n<p><strong>`navigate(id: string): void`</strong></p>\n<p>Navigate to a section by ID.</p>\n<pre><code class=\"language-javascript\">navigate(&#39;guides/getting-started&#39;);\n// Updates hash to #/guides/getting-started and renders section</code></pre>\n<p><strong>`handleRoute(): Promise&lt;void&gt;`</strong></p>\n<p>Process current URL hash and render corresponding section.</p>\n<p><strong>`loadSection(entry: SectionEntry): Promise&lt;void&gt;`</strong></p>\n<p>Load and render a section from the manifest.</p>\n<pre><code class=\"language-javascript\">const section = findSection(&#39;welcome&#39;);\nawait loadSection(section);</code></pre>\n<p><strong>`updateNavState(activeId: string): void`</strong></p>\n<p>Update navigation UI to reflect active section.</p>\n<p><strong>`openCommandPalette(): void`</strong></p>\n<p>Open the command palette (search interface).</p>\n<p><strong>`closeCommandPalette(): void`</strong></p>\n<p>Close the command palette.</p>\n<hr>\n<h3 id=\"manifestjs-navigation-registry\">manifest.js - Navigation Registry</h3>\n<p>Defines navigation structure and section metadata.</p>\n<h4 id=\"exports\">Exports</h4>\n<p><strong>`MANIFEST: SectionEntry[]`</strong></p>\n<p>Ordered array of sections defining navigation.</p>\n<pre><code class=\"language-javascript\">export const MANIFEST = [\n {\n id: &#39;welcome&#39;,\n title: &#39;Welcome&#39;,\n summary: &#39;Introduction&#39;,\n module: &#39;./sections/welcome.js&#39;\n },\n {\n id: &#39;guides&#39;,\n title: &#39;Guides&#39;,\n subsections: [\n { id: &#39;guides/setup&#39;, title: &#39;Setup&#39;, module: &#39;./sections/guides--setup.js&#39; }\n ]\n }\n];</code></pre>\n<p><strong>`DEFAULT_SECTION: string`</strong></p>\n<p>ID of the default section (first in manifest).</p>\n<p><strong>`SITE_CONFIG: SiteConfig`</strong></p>\n<p>Site configuration from build.</p>\n<pre><code class=\"language-javascript\">export const SITE_CONFIG = {\n title: &#39;My Docs&#39;,\n description: &#39;Documentation for My Product&#39;,\n brandMark: &#39;MY&#39;,\n brandSub: &#39;DOCS&#39;\n};</code></pre>\n<p><strong>`findSection(id: string): SectionEntry | undefined`</strong></p>\n<p>Look up a section by ID.</p>\n<pre><code class=\"language-javascript\">const section = findSection(&#39;guides/setup&#39;);\n// Returns { id: &#39;guides/setup&#39;, title: &#39;Setup&#39;, ... }</code></pre>\n<p><strong>`getAdjacentSections(id: string): { prev?: SectionEntry, next?: SectionEntry }`</strong></p>\n<p>Get previous and next sections for navigation.</p>\n<hr>\n<h3 id=\"seojs-metadata-helper\">seo.js - Metadata Helper</h3>\n<p>Manages document metadata for SEO.</p>\n<h4 id=\"functions-2\">Functions</h4>\n<p><strong>`updateMetaTags({ title: string, description?: string }): void`</strong></p>\n<p>Update document title and meta description.</p>\n<pre><code class=\"language-javascript\">updateMetaTags({\n title: &#39;Getting Started - My Docs&#39;,\n description: &#39;Learn how to get started with My Product&#39;\n});</code></pre>\n<hr>\n<h2 id=\"library-modules\">Library Modules</h2>\n<h3 id=\"libsearchjs-full-text-search\">lib/search.js - Full-Text Search</h3>\n<p>Search functionality with lazy content indexing.</p>\n<h4 id=\"functions-3\">Functions</h4>\n<p><strong>`escapeRegExp(value: string): string`</strong></p>\n<p>Escape special regex characters.</p>\n<pre><code class=\"language-javascript\">escapeRegExp(&#39;foo.bar&#39;); // &#39;foo\\\\.bar&#39;</code></pre>\n<p><strong>`flattenManifest(manifest: SectionEntry[]): FlatSection[]`</strong></p>\n<p>Flatten nested manifest into searchable sections.</p>\n<pre><code class=\"language-javascript\">const flat = flattenManifest(MANIFEST);\n// Returns all navigable sections with group info</code></pre>\n<p><strong>`buildSearchIndex(manifest: SectionEntry[]): Promise&lt;IndexedSection[]&gt;`</strong></p>\n<p>Build search index by loading all section modules. Cached after first call.</p>\n<pre><code class=\"language-javascript\">const index = await buildSearchIndex(MANIFEST);\n// Each entry has searchContent: lowercase text for matching</code></pre>\n<p><strong>`filterSections(manifest: SectionEntry[], query: string): FlatSection[]`</strong></p>\n<p>Synchronous title/summary search (no content).</p>\n<pre><code class=\"language-javascript\">const results = filterSections(MANIFEST, &#39;setup&#39;);</code></pre>\n<p><strong>`searchContent(manifest: SectionEntry[], query: string): Promise&lt;IndexedSection[]&gt;`</strong></p>\n<p>Full-text search across all content.</p>\n<pre><code class=\"language-javascript\">const results = await searchContent(MANIFEST, &#39;authentication&#39;);\n// Searches titles, summaries, and full content</code></pre>\n<p><strong>`findPreferredIndex(entries: Section[], currentId: string): number`</strong></p>\n<p>Find index of current section in filtered results.</p>\n<hr>\n<h3 id=\"librouterjs-hash-routing\">lib/router.js - Hash Routing</h3>\n<p>URL hash parsing and resolution.</p>\n<h4 id=\"functions-4\">Functions</h4>\n<p><strong>`resolveTarget(hash: string): string`</strong></p>\n<p>Extract section ID from URL hash.</p>\n<pre><code class=\"language-javascript\">resolveTarget(&#39;#/guides/setup&#39;); // &#39;guides/setup&#39;\nresolveTarget(&#39;#&#39;); // &#39;&#39; (empty)</code></pre>\n<p><strong>`resolveEntry(entry: SectionEntry): SectionEntry`</strong></p>\n<p>Resolve a section entry, following redirects if needed.</p>\n<hr>\n<h3 id=\"libexportjs-document-export\">lib/export.js - Document Export</h3>\n<p>Compose sections into exportable HTML documents.</p>\n<h4 id=\"functions-5\">Functions</h4>\n<p><strong>`composeExportDocument(chapters: Chapter[]): string`</strong></p>\n<p>Generate complete HTML document from chapters.</p>\n<pre><code class=\"language-javascript\">const html = composeExportDocument([\n { section: { title: &#39;Welcome&#39; }, html: &#39;&lt;p&gt;Hello&lt;/p&gt;&#39; },\n { section: { title: &#39;Setup&#39; }, html: &#39;&lt;p&gt;Install...&lt;/p&gt;&#39; }\n]);\n// Returns complete HTML with TOC, styles, syntax highlighting</code></pre>\n<p><strong>`collectExportableSections(manifest: SectionEntry[]): SectionEntry[]`</strong></p>\n<p>Get all sections that can be exported (have module paths).</p>\n<pre><code class=\"language-javascript\">const sections = collectExportableSections(MANIFEST);</code></pre>\n<hr>\n<h2 id=\"enhancement-modules\">Enhancement Modules</h2>\n<h3 id=\"mermaid-initjs-diagram-rendering\">mermaid-init.js - Diagram Rendering</h3>\n<p>Lazy-load and render Mermaid diagrams.</p>\n<h4 id=\"functions-6\">Functions</h4>\n<p><strong>`renderMermaidBlocks(container: Element): Promise&lt;void&gt;`</strong></p>\n<p>Find and render all Mermaid code blocks in a container.</p>\n<pre><code class=\"language-javascript\">await renderMermaidBlocks(document.querySelector(&#39;.canvas&#39;));\n// Replaces ```mermaid blocks with rendered SVGs</code></pre>\n<hr>\n<h3 id=\"syntax-highlightjs-code-highlighting\">syntax-highlight.js - Code Highlighting</h3>\n<p>Lazy-load and apply Prism.js syntax highlighting.</p>\n<h4 id=\"functions-7\">Functions</h4>\n<p><strong>`highlightCodeBlocks(container: Element): Promise&lt;void&gt;`</strong></p>\n<p>Highlight all code blocks in a container.</p>\n<pre><code class=\"language-javascript\">await highlightCodeBlocks(document.querySelector(&#39;.canvas&#39;));\n// Applies syntax highlighting to all &lt;code&gt; elements</code></pre>\n<p>Supported languages: JavaScript, TypeScript, Python, Rust, Go, C, JSON, YAML, Bash, SQL, Solidity.</p>\n<hr>\n<h2 id=\"section-module-contract\">Section Module Contract</h2>\n<p>All section modules must export a `load` function:</p>\n<pre><code class=\"language-javascript\">/**\n * Load section content.\n * @returns {Promise&lt;{ html: string, afterRender?: (container: Element) =&gt; void }&gt;}\n */\nexport async function load() {\n return {\n html: &#39;&lt;section class=&quot;section doc&quot;&gt;...&lt;/section&gt;&#39;,\n\n // Optional: called after HTML is inserted into DOM\n afterRender(container) {\n // DOM manipulation, event listeners, etc.\n }\n };\n}</code></pre>\n<h3 id=\"examples\">Examples</h3>\n<p><strong>Static Content:</strong></p>\n<pre><code class=\"language-javascript\">export async function load() {\n return {\n html: `\n &lt;section class=&quot;section doc markdown&quot;&gt;\n &lt;div class=&quot;doc-content&quot;&gt;\n &lt;h1&gt;Welcome&lt;/h1&gt;\n &lt;p&gt;Hello, world!&lt;/p&gt;\n &lt;/div&gt;\n &lt;/section&gt;\n `\n };\n}</code></pre>\n<p><strong>Dynamic Content:</strong></p>\n<pre><code class=\"language-javascript\">export async function load() {\n const data = await fetch(&#39;/api/stats.json&#39;).then(r =&gt; r.json());\n\n return {\n html: `\n &lt;section class=&quot;section doc&quot;&gt;\n &lt;h1&gt;Stats&lt;/h1&gt;\n &lt;p&gt;Count: ${data.count}&lt;/p&gt;\n &lt;/section&gt;\n `,\n afterRender(container) {\n container.querySelector(&#39;button&#39;)?.addEventListener(&#39;click&#39;, refresh);\n }\n };\n}</code></pre>\n<hr>\n<h2 id=\"type-definitions\">Type Definitions</h2>\n<pre><code class=\"language-typescript\">interface SectionEntry {\n id: string;\n title: string;\n summary?: string;\n module?: string;\n subsections?: SectionEntry[];\n exclude?: boolean;\n}\n\ninterface FlatSection extends SectionEntry {\n group?: string; // Parent group title\n}\n\ninterface IndexedSection extends FlatSection {\n searchContent: string; // Lowercase text for searching\n}\n\ninterface SiteConfig {\n title: string;\n description?: string;\n brandMark?: string;\n brandSub?: string;\n tagline?: string;\n copyright?: string;\n}\n\ninterface Chapter {\n section: { title: string; summary?: string };\n html: string;\n}</code></pre>\n<hr>\n<h2 id=\"build-scripts\">Build Scripts</h2>\n<h3 id=\"scriptsbuildjs\">scripts/build.js</h3>\n<p>Core build script for copying and minifying assets.</p>\n<pre><code class=\"language-bash\">node scripts/build.js [--dev]</code></pre>\n<p>Options:</p>\n<ul>\n<li>`--dev` - Skip minification</li>\n</ul>\n<h3 id=\"scriptsbuild-tenantsjs\">scripts/build-tenants.js</h3>\n<p>Multi-tenant build orchestrator. It processes tenant content, applies branding</p>\n<p>and overrides, copies public assets, then calls the build library modules for</p>\n<p>SEO artifacts and collections.</p>\n<pre><code class=\"language-bash\">node scripts/build-tenants.js [tenant-id] [--incremental]</code></pre>\n<p>Arguments:</p>\n<ul>\n<li>`tenant-id` - Build specific tenant (omit for all)</li>\n<li>`--incremental` - Only rebuild changed files</li>\n</ul>\n<p>See <a href=\"#build-library-modules\">Build Library Modules</a> for the helper modules used</p>\n<p>by this orchestrator.</p>\n<h3 id=\"scriptsservejs\">scripts/serve.js</h3>\n<p>Development server.</p>\n<pre><code class=\"language-bash\">node scripts/serve.js [--port=5173]</code></pre>\n<h3 id=\"scriptssync-docsjs\">scripts/sync-docs.js</h3>\n<p>Regenerate section template modules.</p>\n<pre><code class=\"language-bash\">node scripts/sync-docs.js</code></pre>\n<h2 id=\"build-library-modules\">Build Library Modules</h2>\n<p>These modules are called by `scripts/build-tenants.js` during tenant builds.</p>\n<p>They generate files that ship in each tenant output, so they are part of the</p>\n<p>build-time API surface even though they do not run in the browser.</p>\n<h3 id=\"scriptslibseo-generatorjs\">scripts/lib/seo-generator.js</h3>\n<p>Generates crawler-facing SEO artifacts after tenant content, branding, theme,</p>\n<p>welcome, and public assets have been written.</p>\n<h4 id=\"exports-2\">Exports</h4>\n<p><strong>`resolveBaseUrl(config?: object): string`</strong></p>\n<p>Resolve the tenant absolute base URL. `seo.siteUrl` takes precedence over</p>\n<p>`domain`; domains without a scheme are treated as HTTPS. Returns an empty</p>\n<p>string when neither value is configured.</p>\n<pre><code class=\"language-javascript\">const baseUrl = resolveBaseUrl({ domain: &#39;docs.example.com&#39; });\n// &#39;https://docs.example.com&#39;</code></pre>\n<p><strong>`resolveOgImage(config?: object, baseUrl?: string): string`</strong></p>\n<p>Resolve `seo.ogImage` for Open Graph and Twitter metadata. Absolute image URLs</p>\n<p>pass through; site-relative paths are joined to `baseUrl` when available.</p>\n<p><strong>`generateSeoArtifacts(distDir: string, config: object): Promise&lt;void&gt;`</strong></p>\n<p>Generate all enabled SEO artifacts for the tenant output directory:</p>\n<ul>\n<li>`sitemap.xml`</li>\n<li>`robots.txt`</li>\n<li>`llms.txt`</li>\n<li>static crawler snapshots under `pages/`</li>\n<li>JSON-LD embedded in generated static pages</li>\n</ul>\n<p>Called from `scripts/build-tenants.js` after `.public/` assets are copied and</p>\n<p>before collection manifests are generated.</p>\n<pre><code class=\"language-javascript\">await generateSeoArtifacts(distDir, config);</code></pre>\n<p><strong>`generateSitemap(distDir: string, manifest: SectionEntry[], config: object): Promise&lt;void&gt;`</strong></p>\n<p>Write `sitemap.xml` from the generated navigation manifest.</p>\n<p><strong>`generateRobotsTxt(distDir: string, config: object): Promise&lt;void&gt;`</strong></p>\n<p>Write `robots.txt`, including a sitemap pointer when a base URL is configured.</p>\n<p><strong>`generateStaticSnapshots(distDir: string, manifest: SectionEntry[], config: object): Promise&lt;void&gt;`</strong></p>\n<p>Write static HTML snapshots for each navigable section so crawlers can consume</p>\n<p>content without executing the SPA.</p>\n<p><strong>`generateLlmsTxt(distDir: string, manifest: SectionEntry[], config: object): Promise&lt;void&gt;`</strong></p>\n<p>Write `llms.txt` with tenant-level metadata and links to generated static pages.</p>\n<h3 id=\"scriptslibcollections-generatorjs\">scripts/lib/collections-generator.js</h3>\n<p>Generates per-collection manifests and optional RSS feeds from Markdown posts&#39;</p>\n<p>front matter. Collections are opt-in through `config.collections`.</p>\n<h4 id=\"exports-3\">Exports</h4>\n<p><strong>`generateCollections(distDir: string, config: object, contentBasePath: string): Promise&lt;void&gt;`</strong></p>\n<p>For each collection config, read posts under `contentBasePath/&lt;collection.path&gt;`</p>\n<p>and emit artifacts under the configured route:</p>\n<ul>\n<li>`&lt;route&gt;/index.json` when `manifest !== false`</li>\n<li>`&lt;route&gt;/feed.xml` when `feed === true`</li>\n</ul>\n<p>The `index.json` entry shape is:</p>\n<pre><code class=\"language-typescript\">interface CollectionEntry {\n slug: string;\n title: string;\n date: string | null;\n summary: string;\n hero: string | null;\n tags: string[];\n reading_time: number;\n canonical: string;\n path: string;\n}</code></pre>\n<p>Called from `scripts/build-tenants.js` after SEO artifacts are generated:</p>\n<pre><code class=\"language-javascript\">const collectionRoot = await findContentRoot(sourceDir);\nawait generateCollections(distDir, config, collectionRoot.basePath);</code></pre>\n<h3 id=\"scriptslibfrontmatterjs\">scripts/lib/frontmatter.js</h3>\n<p>Parses the Markdown front-matter subset used by collection posts and tenant</p>\n<p>content metadata.</p>\n<h4 id=\"exports-4\">Exports</h4>\n<p><strong>`parseFrontmatter(raw: string): { data: Record&lt;string, any&gt;, body: string }`</strong></p>\n<p>Parse a leading `---` fenced block of `key: value` pairs. Values are coerced to</p>\n<p>booleans, numbers, `null`, quoted strings, or inline lists such as</p>\n<p>`[docs, release]`. Nested maps are not supported; unsupported values remain</p>\n<p>strings.</p>\n<pre><code class=\"language-javascript\">const { data, body } = parseFrontmatter(markdown);</code></pre>\n<p><strong>`estimateReadingTime(body: string): number`</strong></p>\n<p>Estimate reading time in minutes at roughly 200 words per minute, with a</p>\n<p>minimum of `1`.</p>\n<p><strong>`firstHeading(body: string): string | null`</strong></p>\n<p>Return the first Markdown H1 (`# Title`) in the body, or `null` when none is present.</p>\n </div>\n</section>" };
3
3
  }
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"pagenary-architecture\">Pagenary Architecture</h1>\n<p>A minimalist multi-tenant documentation platform built around static assets and client-side rendering.</p>\n<h2 id=\"design-principles\">Design Principles</h2>\n<ul>\n<li><strong>Zero Runtime Dependencies</strong> - Vanilla HTML, CSS, and ES modules keep the footprint tiny</li>\n<li><strong>Static-First</strong> - Hash-based routing (`#/page-id`) works on any static host</li>\n<li><strong>Multi-Tenant Isolation</strong> - Each tenant gets isolated content, branding, and configuration</li>\n<li><strong>Progressive Enhancement</strong> - Core content works without JavaScript; features enhance with it</li>\n</ul>\n<h2 id=\"system-overview\">System Overview</h2>\n<pre><code>┌─────────────────────────────────────────────────────────────┐\n│ Build System │\n│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ │\n│ │ Tenant │ │ Content │ │ Asset Pipeline │ │\n│ │ Registry │──│ Processor │──│ (Minify, Copy, Brand) │ │\n│ └──────────┘ └──────────────┘ └───────────────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Static Bundle (dist/) │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │\n│ │index.html│ │ app.js │ │styles.css│ │ sections/ │ │\n│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Runtime (Browser) │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │\n│ │ Router │ │ Search │ │ Renderer │ │ Export │ │\n│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │\n└─────────────────────────────────────────────────────────────┘</code></pre>\n<h2 id=\"build-system\">Build System</h2>\n<h3 id=\"tenant-registry\">Tenant Registry</h3>\n<p>`tenants.json` maps tenant IDs to content sources:</p>\n<pre><code class=\"language-json\">{\n &quot;tenant-id&quot;: {\n &quot;source&quot;: &quot;/path/or/git:url#branch&quot;,\n &quot;domain&quot;: &quot;docs.example.com&quot;\n }\n}</code></pre>\n<p>Supports local paths and git repositories. Git sources are cloned to a cache directory.</p>\n<h3 id=\"content-processor\">Content Processor</h3>\n<p>Transforms source content into section modules:</p>\n<table><thead><tr><th style=\"text-align: left\">Input</th><th style=\"text-align: left\">Processing</th><th style=\"text-align: left\">Output</th></tr></thead><tbody><tr><td style=\"text-align: left\">`.md`</td><td style=\"text-align: left\">Parse Markdown → HTML</td><td style=\"text-align: left\">ES module with `load()`</td></tr><tr><td style=\"text-align: left\">`.html`</td><td style=\"text-align: left\">Wrap in loader</td><td style=\"text-align: left\">ES module with `load()`</td></tr><tr><td style=\"text-align: left\">`.js`</td><td style=\"text-align: left\">Copy unchanged</td><td style=\"text-align: left\">ES module with `load()`</td></tr></tbody></table>\n<h3 id=\"asset-pipeline\">Asset Pipeline</h3>\n<p>1. <strong>Copy</strong> - Static assets from `src/` to `dist/`</p>\n<p>2. <strong>Minify</strong> - JavaScript via Terser (production)</p>\n<p>3. <strong>Brand</strong> - Apply tenant config (colors, text)</p>\n<p>4. <strong>Override</strong> - Replace files from `overrides/`</p>\n<h2 id=\"runtime-architecture\">Runtime Architecture</h2>\n<h3 id=\"shell-layout\">Shell Layout</h3>\n<pre><code>┌─────────────────────────────────────────────────────────┐\n│ Top Bar: Menu Toggle │ Brand │ Command Palette │ Export │\n├───────────────┬─────────────────────────────────────────┤\n│ │ │\n│ Sidebar │ Canvas │\n│ (Nav) │ (Content) │\n│ │ │\n├───────────────┴─────────────────────────────────────────┤\n│ Footer │\n└─────────────────────────────────────────────────────────┘</code></pre>\n<h3 id=\"module-structure\">Module Structure</h3>\n<pre><code>src/\n├── index.html # Shell template\n├── app.js # Core controller\n├── styles.css # All styling\n├── manifest.js # Navigation registry\n├── seo.js # Meta tag management\n├── mermaid-init.js # Diagram rendering\n├── syntax-highlight.js # Code highlighting\n└── lib/\n ├── search.js # Full-text search\n ├── router.js # Hash routing utilities\n └── export.js # Document export</code></pre>\n<h3 id=\"core-flow\">Core Flow</h3>\n<pre><code>Hash Change → Router → Manifest Lookup → Module Import → Render → Post-Process\n │\n ├── Mermaid Diagrams\n ├── Syntax Highlighting\n └── SEO Meta Tags</code></pre>\n<h2 id=\"key-components\">Key Components</h2>\n<h3 id=\"router-appjs\">Router (app.js)</h3>\n<p>Hash-based navigation with history support:</p>\n<pre><code class=\"language-javascript\">// URL: https://docs.example.com/#/guides/setup\n// Resolves to section ID: &quot;guides/setup&quot;\n\nwindow.addEventListener(&#39;hashchange&#39;, handleRoute);\n\nfunction handleRoute() {\n const id = resolveTarget(location.hash);\n const section = findSection(id);\n await loadSection(section);\n}</code></pre>\n<h3 id=\"search-libsearchjs\">Search (lib/search.js)</h3>\n<p>Full-text search with lazy indexing:</p>\n<pre><code class=\"language-javascript\">// First search: load all modules, extract text, build index\n// Subsequent: search cached index\n\nasync function buildSearchIndex(manifest) {\n const sections = flattenManifest(manifest);\n return Promise.all(sections.map(async (section) =&gt; {\n const mod = await import(section.module);\n const { html } = await mod.load();\n return { ...section, searchContent: extractText(html) };\n }));\n}</code></pre>\n<h3 id=\"mermaid-integration-mermaid-initjs\">Mermaid Integration (mermaid-init.js)</h3>\n<p>Lazy-loaded diagram rendering:</p>\n<pre><code class=\"language-javascript\">export async function renderMermaidBlocks(container) {\n const blocks = container.querySelectorAll(&#39;pre &gt; code.language-mermaid&#39;);\n if (!blocks.length) return;\n\n const mermaid = await import(&#39;https://esm.sh/mermaid@11&#39;);\n mermaid.default.initialize({ startOnLoad: false });\n\n for (const block of blocks) {\n const { svg } = await mermaid.default.render(id, block.textContent);\n // Replace code block with rendered SVG\n }\n}</code></pre>\n<h3 id=\"syntax-highlighting-syntax-highlightjs\">Syntax Highlighting (syntax-highlight.js)</h3>\n<p>Prism.js integration with language auto-detection:</p>\n<pre><code class=\"language-javascript\">export async function highlightCodeBlocks(container) {\n const blocks = container.querySelectorAll(&#39;pre &gt; code[class*=&quot;language-&quot;]&#39;);\n if (!blocks.length) return;\n\n const Prism = await import(&#39;https://esm.sh/prismjs@1.29.0&#39;);\n // Load language modules dynamically\n Prism.highlightAllUnder(container);\n}</code></pre>\n<h3 id=\"export-libexportjs\">Export (lib/export.js)</h3>\n<p>Document composition for print/PDF:</p>\n<pre><code class=\"language-javascript\">export function composeExportDocument(chapters) {\n // Generate TOC\n const toc = chapters.map((ch, i) =&gt; `&lt;li&gt;${i+1}. ${ch.title}&lt;/li&gt;`);\n\n // Compose sections\n const body = chapters.map((ch, i) =&gt; `\n &lt;section&gt;\n &lt;h2&gt;${i+1}. ${ch.title}&lt;/h2&gt;\n ${ch.html}\n &lt;/section&gt;\n `);\n\n return `&lt;!doctype html&gt;...${toc}...${body}...`;\n}</code></pre>\n<h2 id=\"multi-tenant-architecture\">Multi-Tenant Architecture</h2>\n<h3 id=\"build-time-isolation\">Build-Time Isolation</h3>\n<p>Each tenant build produces an isolated bundle:</p>\n<pre><code>dist/\n├── tenant-a/\n│ ├── index.html # Branded shell\n│ ├── manifest.js # Tenant navigation\n│ ├── styles.css # Themed styles\n│ └── sections/ # Tenant content\n└── tenant-b/\n └── ... # Completely separate</code></pre>\n<h3 id=\"runtime-isolation\">Runtime Isolation</h3>\n<ul>\n<li>No shared state between tenants</li>\n<li>Each tenant loads its own manifest</li>\n<li>Theming via CSS variables replaced at build time</li>\n</ul>\n<h3 id=\"caddy-routing\">Caddy Routing</h3>\n<p>Multi-tenant domain routing via Caddy:</p>\n<pre><code>tenant-a.example.com → dist/tenant-a/\ntenant-b.example.com → dist/tenant-b/</code></pre>\n<h2 id=\"performance-characteristics\">Performance Characteristics</h2>\n<h3 id=\"bundle-size\">Bundle Size</h3>\n<table><thead><tr><th style=\"text-align: left\">Component</th><th style=\"text-align: left\">Size (minified)</th></tr></thead><tbody><tr><td style=\"text-align: left\">Shell (HTML/CSS/JS)</td><td style=\"text-align: left\">~50 KB</td></tr><tr><td style=\"text-align: left\">Per section</td><td style=\"text-align: left\">~1-5 KB</td></tr><tr><td style=\"text-align: left\">Mermaid (lazy)</td><td style=\"text-align: left\">~800 KB</td></tr><tr><td style=\"text-align: left\">Prism (lazy)</td><td style=\"text-align: left\">~30 KB</td></tr></tbody></table>\n<h3 id=\"loading-strategy\">Loading Strategy</h3>\n<p>1. <strong>Critical Path</strong> - Shell + manifest + first section</p>\n<p>2. <strong>Lazy Load</strong> - Other sections on navigation</p>\n<p>3. <strong>On-Demand</strong> - Mermaid/Prism when needed</p>\n<p>4. <strong>Cached</strong> - Search index after first search</p>\n<h2 id=\"extensibility-points\">Extensibility Points</h2>\n<h3 id=\"custom-page-types\">Custom Page Types</h3>\n<p>Add to `section-templates.js`:</p>\n<pre><code class=\"language-javascript\">export const templates = {\n &#39;custom-type&#39;: {\n render: (data) =&gt; `&lt;section class=&quot;custom&quot;&gt;...&lt;/section&gt;`\n }\n};</code></pre>\n<h3 id=\"custom-components\">Custom Components</h3>\n<p>Use HTML classes in content:</p>\n<div class=\"html-block\"><div class=\"my-component\">...</div></div>\n<p>Add styles to tenant&#39;s `overrides/styles.css`.</p>\n<h3 id=\"dynamic-data\">Dynamic Data</h3>\n<p>JavaScript modules can fetch external data:</p>\n<pre><code class=\"language-javascript\">export async function load() {\n const data = await fetch(&#39;/api/data.json&#39;).then(r =&gt; r.json());\n return { html: renderWithData(data) };\n}</code></pre>\n<h2 id=\"security-considerations\">Security Considerations</h2>\n<ul>\n<li><strong>No Server-Side Code</strong> - Pure static assets</li>\n<li><strong>CSP Compatible</strong> - No inline scripts in content</li>\n<li><strong>Sandboxed Content</strong> - Each tenant in separate directory</li>\n<li><strong>No User Data</strong> - Only localStorage for UI state</li>\n</ul>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"pagenary-architecture\">Pagenary Architecture</h1>\n<p>A minimalist multi-tenant documentation platform built around static assets and client-side rendering.</p>\n<h2 id=\"design-principles\">Design Principles</h2>\n<ul>\n<li><strong>Zero Runtime Dependencies</strong> - Vanilla HTML, CSS, and ES modules keep the footprint tiny</li>\n<li><strong>Static-First</strong> - Hash-based routing (`#/page-id`) works on any static host</li>\n<li><strong>Multi-Tenant Isolation</strong> - Each tenant gets isolated content, branding, and configuration</li>\n<li><strong>Progressive Enhancement</strong> - Core content works without JavaScript; features enhance with it</li>\n</ul>\n<h2 id=\"system-overview\">System Overview</h2>\n<pre><code>┌─────────────────────────────────────────────────────────────┐\n│ Build System │\n│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ │\n│ │ Tenant │ │ Content │ │ Asset Pipeline │ │\n│ │ Registry │──│ Processor │──│ (Minify, Copy, Brand) │ │\n│ └──────────┘ └──────────────┘ └───────────────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Static Bundle (dist/) │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │\n│ │index.html│ │ app.js │ │styles.css│ │ sections/ │ │\n│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ Runtime (Browser) │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │\n│ │ Router │ │ Search │ │ Renderer │ │ Export │ │\n│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │\n└─────────────────────────────────────────────────────────────┘</code></pre>\n<h2 id=\"build-system\">Build System</h2>\n<h3 id=\"tenant-registry\">Tenant Registry</h3>\n<p>`tenants.json` maps tenant IDs to content sources:</p>\n<pre><code class=\"language-json\">{\n &quot;tenant-id&quot;: {\n &quot;source&quot;: &quot;/path/or/git:url#branch&quot;,\n &quot;domain&quot;: &quot;docs.example.com&quot;\n }\n}</code></pre>\n<p>Supports local paths and git repositories. Git sources are cloned to a cache directory.</p>\n<h3 id=\"content-processor\">Content Processor</h3>\n<p>Transforms source content into section modules:</p>\n<table><thead><tr><th style=\"text-align: left\">Input</th><th style=\"text-align: left\">Processing</th><th style=\"text-align: left\">Output</th></tr></thead><tbody><tr><td style=\"text-align: left\">`.md`</td><td style=\"text-align: left\">Parse Markdown → HTML</td><td style=\"text-align: left\">ES module with `load()`</td></tr><tr><td style=\"text-align: left\">`.html`</td><td style=\"text-align: left\">Wrap in loader</td><td style=\"text-align: left\">ES module with `load()`</td></tr><tr><td style=\"text-align: left\">`.js`</td><td style=\"text-align: left\">Copy unchanged</td><td style=\"text-align: left\">ES module with `load()`</td></tr></tbody></table>\n<h3 id=\"asset-pipeline\">Asset Pipeline</h3>\n<p>1. <strong>Copy</strong> - Static assets from `src/` to `dist/`</p>\n<p>2. <strong>Minify</strong> - JavaScript via Terser (production)</p>\n<p>3. <strong>Brand</strong> - Apply tenant config (colors, text)</p>\n<p>4. <strong>Override</strong> - Replace files from `overrides/`</p>\n<h3 id=\"seo-artifact-generation\">SEO Artifact Generation</h3>\n<p>After content processing, branding, tenant overrides, and `.public/` assets are</p>\n<p>in place, `scripts/build-tenants.js` calls `scripts/lib/seo-generator.js` to emit</p>\n<p>crawler-facing files into the tenant output directory:</p>\n<ul>\n<li>`sitemap.xml` from the generated manifest</li>\n<li>`robots.txt` with a sitemap pointer</li>\n<li>`llms.txt` for LLM-friendly site discovery</li>\n<li>static snapshots under `pages/` for each navigable section</li>\n<li>JSON-LD metadata embedded in the generated snapshots</li>\n</ul>\n<p>The generator resolves absolute URLs from `seo.siteUrl` or `domain`. Tenants can</p>\n<p>disable the whole stage with `seo.enabled: false` or individual artifact switches.</p>\n<h3 id=\"collection-manifests\">Collection Manifests</h3>\n<p>If `config.collections` is configured, `buildTenant()` resolves the tenant content</p>\n<p>root and calls `scripts/lib/collections-generator.js` after SEO artifacts are</p>\n<p>written. Each collection reads Markdown posts from its configured `path`, parses</p>\n<p>front matter with `scripts/lib/frontmatter.js`, sorts entries by the configured</p>\n<p>field/order, and writes machine-readable output under the collection route:</p>\n<ul>\n<li>`index.json` with `slug`, `title`, `date`, `summary`, `hero`, `tags`,</li>\n</ul>\n<p>`reading_time`, `canonical`, and `path`</p>\n<ul>\n<li>optional `feed.xml` when `feed: true`</li>\n</ul>\n<h3 id=\"build-flow\">Build Flow</h3>\n<pre><code class=\"language-text\">resolve source\n -&gt; run base build\n -&gt; process tenant content and manifest\n -&gt; apply overrides\n -&gt; apply branding/theme/navigation/welcome\n -&gt; copy .public assets\n -&gt; generate SEO artifacts\n -&gt; generate collection manifests/feeds\n -&gt; copy or sync to target</code></pre>\n<h2 id=\"runtime-architecture\">Runtime Architecture</h2>\n<h3 id=\"shell-layout\">Shell Layout</h3>\n<pre><code>┌─────────────────────────────────────────────────────────┐\n│ Top Bar: Menu Toggle │ Brand │ Command Palette │ Export │\n├───────────────┬─────────────────────────────────────────┤\n│ │ │\n│ Sidebar │ Canvas │\n│ (Nav) │ (Content) │\n│ │ │\n├───────────────┴─────────────────────────────────────────┤\n│ Footer │\n└─────────────────────────────────────────────────────────┘</code></pre>\n<h3 id=\"module-structure\">Module Structure</h3>\n<pre><code>src/\n├── index.html # Shell template\n├── app.js # Core controller\n├── styles.css # All styling\n├── manifest.js # Navigation registry\n├── seo.js # Meta tag management\n├── mermaid-init.js # Diagram rendering\n├── syntax-highlight.js # Code highlighting\n└── lib/\n ├── search.js # Full-text search\n ├── router.js # Hash routing utilities\n └── export.js # Document export\n\nscripts/lib/\n├── seo-generator.js # Build-time SEO artifacts\n├── collections-generator.js # Collection manifests and feeds\n└── frontmatter.js # Markdown front-matter parsing</code></pre>\n<h3 id=\"core-flow\">Core Flow</h3>\n<pre><code>Build: Content → Manifest → Branding → Public Assets → SEO Artifacts → Collections\n\nRuntime: Hash Change → Router → Manifest Lookup → Module Import → Render → Post-Process\n │\n ├── Mermaid Diagrams\n ├── Syntax Highlighting\n └── SEO Meta Tags</code></pre>\n<h2 id=\"key-components\">Key Components</h2>\n<h3 id=\"router-appjs\">Router (app.js)</h3>\n<p>Hash-based navigation with history support:</p>\n<pre><code class=\"language-javascript\">// URL: https://docs.example.com/#/guides/setup\n// Resolves to section ID: &quot;guides/setup&quot;\n\nwindow.addEventListener(&#39;hashchange&#39;, handleRoute);\n\nfunction handleRoute() {\n const id = resolveTarget(location.hash);\n const section = findSection(id);\n await loadSection(section);\n}</code></pre>\n<h3 id=\"search-libsearchjs\">Search (lib/search.js)</h3>\n<p>Full-text search with lazy indexing:</p>\n<pre><code class=\"language-javascript\">// First search: load all modules, extract text, build index\n// Subsequent: search cached index\n\nasync function buildSearchIndex(manifest) {\n const sections = flattenManifest(manifest);\n return Promise.all(sections.map(async (section) =&gt; {\n const mod = await import(section.module);\n const { html } = await mod.load();\n return { ...section, searchContent: extractText(html) };\n }));\n}</code></pre>\n<h3 id=\"mermaid-integration-mermaid-initjs\">Mermaid Integration (mermaid-init.js)</h3>\n<p>Lazy-loaded diagram rendering:</p>\n<pre><code class=\"language-javascript\">export async function renderMermaidBlocks(container) {\n const blocks = container.querySelectorAll(&#39;pre &gt; code.language-mermaid&#39;);\n if (!blocks.length) return;\n\n const mermaid = await import(&#39;https://esm.sh/mermaid@11&#39;);\n mermaid.default.initialize({ startOnLoad: false });\n\n for (const block of blocks) {\n const { svg } = await mermaid.default.render(id, block.textContent);\n // Replace code block with rendered SVG\n }\n}</code></pre>\n<h3 id=\"syntax-highlighting-syntax-highlightjs\">Syntax Highlighting (syntax-highlight.js)</h3>\n<p>Prism.js integration with language auto-detection:</p>\n<pre><code class=\"language-javascript\">export async function highlightCodeBlocks(container) {\n const blocks = container.querySelectorAll(&#39;pre &gt; code[class*=&quot;language-&quot;]&#39;);\n if (!blocks.length) return;\n\n const Prism = await import(&#39;https://esm.sh/prismjs@1.29.0&#39;);\n // Load language modules dynamically\n Prism.highlightAllUnder(container);\n}</code></pre>\n<h3 id=\"export-libexportjs\">Export (lib/export.js)</h3>\n<p>Document composition for print/PDF:</p>\n<pre><code class=\"language-javascript\">export function composeExportDocument(chapters) {\n // Generate TOC\n const toc = chapters.map((ch, i) =&gt; `&lt;li&gt;${i+1}. ${ch.title}&lt;/li&gt;`);\n\n // Compose sections\n const body = chapters.map((ch, i) =&gt; `\n &lt;section&gt;\n &lt;h2&gt;${i+1}. ${ch.title}&lt;/h2&gt;\n ${ch.html}\n &lt;/section&gt;\n `);\n\n return `&lt;!doctype html&gt;...${toc}...${body}...`;\n}</code></pre>\n<h2 id=\"multi-tenant-architecture\">Multi-Tenant Architecture</h2>\n<h3 id=\"build-time-isolation\">Build-Time Isolation</h3>\n<p>Each tenant build produces an isolated bundle:</p>\n<pre><code>dist/\n├── tenant-a/\n│ ├── index.html # Branded shell\n│ ├── manifest.js # Tenant navigation\n│ ├── styles.css # Themed styles\n│ └── sections/ # Tenant content\n└── tenant-b/\n └── ... # Completely separate</code></pre>\n<h3 id=\"runtime-isolation\">Runtime Isolation</h3>\n<ul>\n<li>No shared state between tenants</li>\n<li>Each tenant loads its own manifest</li>\n<li>Theming via CSS variables replaced at build time</li>\n</ul>\n<h3 id=\"caddy-routing\">Caddy Routing</h3>\n<p>Multi-tenant domain routing via Caddy:</p>\n<pre><code>tenant-a.example.com → dist/tenant-a/\ntenant-b.example.com → dist/tenant-b/</code></pre>\n<h2 id=\"performance-characteristics\">Performance Characteristics</h2>\n<h3 id=\"bundle-size\">Bundle Size</h3>\n<table><thead><tr><th style=\"text-align: left\">Component</th><th style=\"text-align: left\">Size (minified)</th></tr></thead><tbody><tr><td style=\"text-align: left\">Shell (HTML/CSS/JS)</td><td style=\"text-align: left\">~50 KB</td></tr><tr><td style=\"text-align: left\">Per section</td><td style=\"text-align: left\">~1-5 KB</td></tr><tr><td style=\"text-align: left\">Mermaid (lazy)</td><td style=\"text-align: left\">~800 KB</td></tr><tr><td style=\"text-align: left\">Prism (lazy)</td><td style=\"text-align: left\">~30 KB</td></tr></tbody></table>\n<h3 id=\"loading-strategy\">Loading Strategy</h3>\n<p>1. <strong>Critical Path</strong> - Shell + manifest + first section</p>\n<p>2. <strong>Lazy Load</strong> - Other sections on navigation</p>\n<p>3. <strong>On-Demand</strong> - Mermaid/Prism when needed</p>\n<p>4. <strong>Cached</strong> - Search index after first search</p>\n<h2 id=\"extensibility-points\">Extensibility Points</h2>\n<h3 id=\"custom-page-types\">Custom Page Types</h3>\n<p>Add to `section-templates.js`:</p>\n<pre><code class=\"language-javascript\">export const templates = {\n &#39;custom-type&#39;: {\n render: (data) =&gt; `&lt;section class=&quot;custom&quot;&gt;...&lt;/section&gt;`\n }\n};</code></pre>\n<h3 id=\"custom-components\">Custom Components</h3>\n<p>Use HTML classes in content:</p>\n<div class=\"html-block\"><div class=\"my-component\">...</div></div>\n<p>Add styles to tenant&#39;s `overrides/styles.css`.</p>\n<h3 id=\"dynamic-data\">Dynamic Data</h3>\n<p>JavaScript modules can fetch external data:</p>\n<pre><code class=\"language-javascript\">export async function load() {\n const data = await fetch(&#39;/api/data.json&#39;).then(r =&gt; r.json());\n return { html: renderWithData(data) };\n}</code></pre>\n<h2 id=\"security-considerations\">Security Considerations</h2>\n<ul>\n<li><strong>No Server-Side Code</strong> - Pure static assets</li>\n<li><strong>CSP Compatible</strong> - No inline scripts in content</li>\n<li><strong>Sandboxed Content</strong> - Each tenant in separate directory</li>\n<li><strong>No User Data</strong> - Only localStorage for UI state</li>\n</ul>\n </div>\n</section>" };
3
3
  }
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"developer-guide\">Developer Guide</h1>\n<p>Welcome to Pagenary Publisher. This workspace lives at `apps/publisher/` inside the Pagenary platform monorepo. Use the repo-level commands (e.g. `npm run publisher:*`) when you prefer not to `cd` into the workspace. This guide keeps onboarding fast so you can focus on tailoring tenant-specific copy rather than plumbing.</p>\n<h2 id=\"prerequisites\">Prerequisites</h2>\n<ul>\n<li>Node.js 16+</li>\n<li>npm (ships with Node)</li>\n<li>Optional: a modern browser for local testing, and a static host account for deployment</li>\n</ul>\n<h2 id=\"install-run\">Install &amp; Run</h2>\n<pre><code class=\"language-bash\">npm install\nnpm run dev</code></pre>\n<p>The dev command builds to `dist/` and serves the bundle with live reload. Open the printed URL and start exploring.</p>\n<h2 id=\"project-tour\">Project Tour</h2>\n<ul>\n<li>`src/index.html` – HTML entry point with top-level shell structure</li>\n<li>`src/app.js` – navigation, routing, command palette, export logic</li>\n<li>`src/sections/section-templates.js` – template catalogue for every page type</li>\n<li>`src/manifest.js` – default navigation structure (overridden per tenant via `tenants/&lt;id&gt;/manifest.json`)</li>\n<li>`scripts/` – small Node utilities for building, serving, syncing sections, linting content, and checking SEO metadata</li>\n</ul>\n<h2 id=\"key-features\">Key Features</h2>\n<p>1. <strong>Mermaid Diagrams</strong> - Use fenced code blocks with `mermaid` language. Renders with zoom/pan controls and pan-on-drag functionality.</p>\n<p>2. <strong>External Links</strong> - Links to external URLs (http/https) automatically open in new tab with security attributes. Use `url` property in manifest for external nav links.</p>\n<p>3. <strong>Internal Linking</strong> - Link between sections with `#section-id` syntax. Build validates that all internal links reference existing sections.</p>\n<p>4. <strong>Bottom Navigation</strong> - Configurable via `bottomNav` in root manifest. Options: &quot;mobile&quot; (default), &quot;always&quot;, or &quot;never&quot;.</p>\n<p>5. <strong>Command Palette</strong> - Press Ctrl+K (or Cmd+K) to search and navigate sections. Supports fuzzy search across section titles and summaries.</p>\n<p>See TENANT-CONFIG.md for full configuration details and examples.</p>\n<h2 id=\"common-tasks\">Common Tasks</h2>\n<ul>\n<li><strong>Add a Section</strong> – create `src/sections/my-section.js`, set the `SECTION_ID`, update `manifest.js`.</li>\n<li><strong>Regenerate Templates</strong> – run `npm run sync:docs` to reset section boilerplate if you need a clean slate.</li>\n<li><strong>Branding</strong> – tweak the logo text and footer copy in `src/index.html`; adjust colors in `src/styles.css`.</li>\n<li><strong>Export Testing</strong> – open the app locally and use the Export button to confirm the combined PDF-ready document looks correct.</li>\n</ul>\n<h2 id=\"tenant-content-bundles\">Tenant Content Bundles</h2>\n<ul>\n<li>Each tenant folder supports a `manifest.json` describing nav groupings, titles, and the content file backing each section. Use nested `sections` arrays to create expandable groups.</li>\n<li>Supported content types live in `tenants/&lt;id&gt;/content/`:</li>\n<li>`.md` → converted to structured HTML (headings, lists, blockquotes supported by the lightweight parser).</li>\n<li>`.html` → shipped as-is, wrapped in a loader module.</li>\n<li>`.js` → copied unchanged; export a `load()` function that returns `{ html, afterRender? }` to drive rich experiences.</li>\n<li>Run `npm run build:tenants` after editing content. The script regenerates `dist/&lt;id&gt;/manifest.js` plus section modules so the navigation immediately reflects the manifest.</li>\n<li>Provide per-tenant overrides (styles, assets, app shell tweaks) inside `tenants/&lt;id&gt;/overrides/`; they copy into the tenant bundle after content generation, so you can replace generated files if needed.</li>\n</ul>\n<h2 id=\"tooling-philosophy\">Tooling Philosophy</h2>\n<ul>\n<li>Zero framework lock-in; replace `app.js` with your own router if you outgrow it.</li>\n<li>Scripts avoid third-party dependencies so they run in restricted environments.</li>\n<li>Everything is ASCII-only by default to ease diffs and downstream localization.</li>\n</ul>\n<h2 id=\"support-checklist\">Support Checklist</h2>\n<p>Before shipping to a tenant:</p>\n<p>1. Replace placeholder copy with real content.</p>\n<p>2. Verify navigation order and summaries in `manifest.js`.</p>\n<p>3. Run `npm run check` to ensure lint and SEO checks pass.</p>\n<p>4. Test export output and section highlighting.</p>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"developer-guide\">Developer Guide</h1>\n<p>Welcome to Pagenary Publisher. This workspace lives at `apps/publisher/` inside the Pagenary platform monorepo. Use the repo-level commands (e.g. `npm run publisher:*`) when you prefer not to `cd` into the workspace. This guide keeps onboarding fast so you can focus on tailoring tenant-specific copy rather than plumbing.</p>\n<h2 id=\"prerequisites\">Prerequisites</h2>\n<ul>\n<li>Node.js 16+</li>\n<li>npm (ships with Node)</li>\n<li>Optional: a modern browser for local testing, and a static host account for deployment</li>\n</ul>\n<h2 id=\"install-run\">Install &amp; Run</h2>\n<pre><code class=\"language-bash\">npm install\nnpm run dev</code></pre>\n<p>The dev command builds to `dist/` and serves the bundle with live reload. Open the printed URL and start exploring.</p>\n<h2 id=\"project-tour\">Project Tour</h2>\n<ul>\n<li>`src/index.html` – HTML entry point with top-level shell structure</li>\n<li>`src/app.js` – navigation, routing, command palette, export logic</li>\n<li>`src/sections/section-templates.js` – template catalogue for every page type</li>\n<li>`src/manifest.js` – default navigation structure (overridden per tenant via `tenants/&lt;id&gt;/manifest.json`)</li>\n<li>`scripts/` – small Node utilities for building, serving, syncing sections, linting content, and checking SEO metadata</li>\n</ul>\n<h2 id=\"key-features\">Key Features</h2>\n<p>1. <strong>Mermaid Diagrams</strong> - Use fenced code blocks with `mermaid` language. Renders with zoom/pan controls and pan-on-drag functionality.</p>\n<p>2. <strong>External Links</strong> - Links to external URLs (http/https) automatically open in new tab with security attributes. Use `url` property in manifest for external nav links.</p>\n<p>3. <strong>Internal Linking</strong> - Link between sections with `#section-id` syntax. Build validates that all internal links reference existing sections.</p>\n<p>4. <strong>Bottom Navigation</strong> - Configurable via `bottomNav` in root manifest. Options: &quot;mobile&quot; (default), &quot;always&quot;, or &quot;never&quot;.</p>\n<p>5. <strong>Command Palette</strong> - Press Ctrl+K (or Cmd+K) to search and navigate sections. Supports fuzzy search across section titles and summaries.</p>\n<p>See TENANT-CONFIG.md for full configuration details and examples.</p>\n<h2 id=\"common-tasks\">Common Tasks</h2>\n<ul>\n<li><strong>Add a Section</strong> – create `src/sections/my-section.js`, set the `SECTION_ID`, update `manifest.js`.</li>\n<li><strong>Regenerate Templates</strong> – run `npm run sync:docs` to reset section boilerplate if you need a clean slate.</li>\n<li><strong>Branding</strong> – tweak the logo text and footer copy in `src/index.html`; adjust colors in `src/styles.css`.</li>\n<li><strong>Export Testing</strong> – open the app locally and use the Export button to confirm the combined PDF-ready document looks correct.</li>\n</ul>\n<h2 id=\"tenant-content-bundles\">Tenant Content Bundles</h2>\n<ul>\n<li>Each tenant folder supports a `manifest.json` describing nav groupings, titles, and the content file backing each section. Use nested `sections` arrays to create expandable groups.</li>\n<li>Supported content types live in `tenants/&lt;id&gt;/content/`:</li>\n<li>`.md` → converted to structured HTML (headings, lists, blockquotes supported by the lightweight parser).</li>\n<li>`.html` → shipped as-is, wrapped in a loader module.</li>\n<li>`.js` → copied unchanged; export a `load()` function that returns `{ html, afterRender? }` to drive rich experiences.</li>\n<li>Run `npm run build:tenants` after editing content. The script regenerates `dist/&lt;id&gt;/manifest.js` plus section modules so the navigation immediately reflects the manifest.</li>\n<li>Provide per-tenant overrides (styles, assets, app shell tweaks) inside `tenants/&lt;id&gt;/overrides/`; they copy into the tenant bundle after content generation, so you can replace generated files if needed.</li>\n</ul>\n<h2 id=\"authoring-collection-posts\">Authoring Collection Posts</h2>\n<p>Use collections when a tenant needs a feed-like content group such as a blog,</p>\n<p>release notes, news, or changelog. Configure the collection in</p>\n<p>`TENANT-CONFIG.md` under `collections`, then add Markdown posts under the</p>\n<p>configured content path.</p>\n<p>Each post can start with front matter parsed by `scripts/lib/frontmatter.js`:</p>\n<pre><code class=\"language-markdown\">---\ntitle: Launch Notes\ndate: 2026-05-28\nsummary: What changed in this release\ntags: [release, platform]\nhero: /assets/blog/launch.png\n---\n\n# Launch Notes\n\nPost content starts here.</code></pre>\n<p>During `npm run build:tenants`, `scripts/lib/collections-generator.js` reads those</p>\n<p>posts and writes `index.json` plus optional `feed.xml` under the collection</p>\n<p>route. See `TENANT-CONFIG.md` for the collection config schema and `API.md` for</p>\n<p>the generated entry shape.</p>\n<h2 id=\"extending-seo-output\">Extending SEO Output</h2>\n<p>Tenant SEO settings live in `TENANT-CONFIG.md` under `seo`. The runtime</p>\n<p>`src/seo.js` module updates browser metadata after navigation, while the build-time</p>\n<p>`scripts/lib/seo-generator.js` module emits crawler-facing artifacts:</p>\n<ul>\n<li>`sitemap.xml`</li>\n<li>`robots.txt`</li>\n<li>`llms.txt`</li>\n<li>static snapshots under `pages/`</li>\n<li>JSON-LD metadata for generated pages</li>\n</ul>\n<p>Extend `scripts/lib/seo-generator.js` when the output artifact set changes, and update</p>\n<p>`API.md` when adding or changing exported helpers.</p>\n<h2 id=\"tooling-philosophy\">Tooling Philosophy</h2>\n<ul>\n<li>Zero framework lock-in; replace `app.js` with your own router if you outgrow it.</li>\n<li>Scripts avoid third-party dependencies so they run in restricted environments.</li>\n<li>Everything is ASCII-only by default to ease diffs and downstream localization.</li>\n</ul>\n<h2 id=\"support-checklist\">Support Checklist</h2>\n<p>Before shipping to a tenant:</p>\n<p>1. Replace placeholder copy with real content.</p>\n<p>2. Verify navigation order and summaries in `manifest.js`.</p>\n<p>3. Run `npm run check` to ensure lint and SEO checks pass.</p>\n<p>4. Test export output and section highlighting.</p>\n </div>\n</section>" };
3
3
  }
package/site/sitemap.xml CHANGED
@@ -2,61 +2,61 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://docs.pagenary.com/</loc>
5
- <lastmod>2026-05-27</lastmod>
5
+ <lastmod>2026-05-28</lastmod>
6
6
  <changefreq>weekly</changefreq>
7
7
  <priority>1.0</priority>
8
8
  </url>
9
9
  <url>
10
10
  <loc>https://docs.pagenary.com/pages/welcome.html</loc>
11
- <lastmod>2026-05-27</lastmod>
11
+ <lastmod>2026-05-28</lastmod>
12
12
  <changefreq>monthly</changefreq>
13
13
  <priority>0.8</priority>
14
14
  </url>
15
15
  <url>
16
16
  <loc>https://docs.pagenary.com/pages/quickstart.html</loc>
17
- <lastmod>2026-05-27</lastmod>
17
+ <lastmod>2026-05-28</lastmod>
18
18
  <changefreq>monthly</changefreq>
19
19
  <priority>0.6</priority>
20
20
  </url>
21
21
  <url>
22
22
  <loc>https://docs.pagenary.com/pages/developer-guide.html</loc>
23
- <lastmod>2026-05-27</lastmod>
23
+ <lastmod>2026-05-28</lastmod>
24
24
  <changefreq>monthly</changefreq>
25
25
  <priority>0.6</priority>
26
26
  </url>
27
27
  <url>
28
28
  <loc>https://docs.pagenary.com/pages/tenant-config.html</loc>
29
- <lastmod>2026-05-27</lastmod>
29
+ <lastmod>2026-05-28</lastmod>
30
30
  <changefreq>monthly</changefreq>
31
31
  <priority>0.6</priority>
32
32
  </url>
33
33
  <url>
34
34
  <loc>https://docs.pagenary.com/pages/extending.html</loc>
35
- <lastmod>2026-05-27</lastmod>
35
+ <lastmod>2026-05-28</lastmod>
36
36
  <changefreq>monthly</changefreq>
37
37
  <priority>0.6</priority>
38
38
  </url>
39
39
  <url>
40
40
  <loc>https://docs.pagenary.com/pages/architecture.html</loc>
41
- <lastmod>2026-05-27</lastmod>
41
+ <lastmod>2026-05-28</lastmod>
42
42
  <changefreq>monthly</changefreq>
43
43
  <priority>0.6</priority>
44
44
  </url>
45
45
  <url>
46
46
  <loc>https://docs.pagenary.com/pages/api.html</loc>
47
- <lastmod>2026-05-27</lastmod>
47
+ <lastmod>2026-05-28</lastmod>
48
48
  <changefreq>monthly</changefreq>
49
49
  <priority>0.6</priority>
50
50
  </url>
51
51
  <url>
52
52
  <loc>https://docs.pagenary.com/pages/deployment.html</loc>
53
- <lastmod>2026-05-27</lastmod>
53
+ <lastmod>2026-05-28</lastmod>
54
54
  <changefreq>monthly</changefreq>
55
55
  <priority>0.6</priority>
56
56
  </url>
57
57
  <url>
58
58
  <loc>https://docs.pagenary.com/pages/seo-strategy.html</loc>
59
- <lastmod>2026-05-27</lastmod>
59
+ <lastmod>2026-05-28</lastmod>
60
60
  <changefreq>monthly</changefreq>
61
61
  <priority>0.6</priority>
62
62
  </url>
package/site/styles.css CHANGED
@@ -207,9 +207,12 @@ a:focus {
207
207
 
208
208
  .nav-parent > .nav-summary {
209
209
  grid-column: 1 / -1;
210
+ grid-row: 2;
210
211
  }
211
212
 
212
213
  .nav-parent .nav-title {
214
+ grid-column: 1;
215
+ grid-row: 1;
213
216
  font-size: 0.95rem;
214
217
  font-weight: 600;
215
218
  letter-spacing: 0.2em;
@@ -217,6 +220,13 @@ a:focus {
217
220
 
218
221
  .nav-parent::after {
219
222
  content: '\25BE';
223
+ /* Explicit placement: keep the disclosure arrow in row 1 / column 2 beside
224
+ the title. Auto-placement here diverges in Firefox (the arrow lands on its
225
+ own centered row). */
226
+ grid-column: 2;
227
+ grid-row: 1;
228
+ justify-self: end;
229
+ align-self: center;
220
230
  font-size: 0.75rem;
221
231
  color: var(--muted);
222
232
  transform: rotate(-90deg);
@@ -231,6 +241,7 @@ a:focus {
231
241
 
232
242
  .nav-parent-with-content .nav-summary {
233
243
  grid-column: 1 / -1;
244
+ grid-row: 2;
234
245
  }
235
246
 
236
247
  .nav-parent-with-content::after {
@@ -238,6 +249,8 @@ a:focus {
238
249
  }
239
250
 
240
251
  .nav-parent-with-content .nav-title-link {
252
+ grid-column: 1;
253
+ grid-row: 1;
241
254
  font-size: 0.95rem;
242
255
  font-weight: 600;
243
256
  letter-spacing: 0.2em;
@@ -250,6 +263,10 @@ a:focus {
250
263
  }
251
264
 
252
265
  .nav-parent-with-content .nav-expand-toggle {
266
+ grid-column: 2;
267
+ grid-row: 1;
268
+ justify-self: end;
269
+ align-self: center;
253
270
  display: flex;
254
271
  align-items: center;
255
272
  justify-content: center;
package/src/styles.css CHANGED
@@ -207,9 +207,12 @@ a:focus {
207
207
 
208
208
  .nav-parent > .nav-summary {
209
209
  grid-column: 1 / -1;
210
+ grid-row: 2;
210
211
  }
211
212
 
212
213
  .nav-parent .nav-title {
214
+ grid-column: 1;
215
+ grid-row: 1;
213
216
  font-size: 0.95rem;
214
217
  font-weight: 600;
215
218
  letter-spacing: 0.2em;
@@ -217,6 +220,13 @@ a:focus {
217
220
 
218
221
  .nav-parent::after {
219
222
  content: '\25BE';
223
+ /* Explicit placement: keep the disclosure arrow in row 1 / column 2 beside
224
+ the title. Auto-placement here diverges in Firefox (the arrow lands on its
225
+ own centered row). */
226
+ grid-column: 2;
227
+ grid-row: 1;
228
+ justify-self: end;
229
+ align-self: center;
220
230
  font-size: 0.75rem;
221
231
  color: var(--muted);
222
232
  transform: rotate(-90deg);
@@ -231,6 +241,7 @@ a:focus {
231
241
 
232
242
  .nav-parent-with-content .nav-summary {
233
243
  grid-column: 1 / -1;
244
+ grid-row: 2;
234
245
  }
235
246
 
236
247
  .nav-parent-with-content::after {
@@ -238,6 +249,8 @@ a:focus {
238
249
  }
239
250
 
240
251
  .nav-parent-with-content .nav-title-link {
252
+ grid-column: 1;
253
+ grid-row: 1;
241
254
  font-size: 0.95rem;
242
255
  font-weight: 600;
243
256
  letter-spacing: 0.2em;
@@ -250,6 +263,10 @@ a:focus {
250
263
  }
251
264
 
252
265
  .nav-parent-with-content .nav-expand-toggle {
266
+ grid-column: 2;
267
+ grid-row: 1;
268
+ justify-self: end;
269
+ align-self: center;
253
270
  display: flex;
254
271
  align-items: center;
255
272
  justify-content: center;