@pagenary/publisher 2026.5.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.
Files changed (147) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +337 -0
  3. package/bin/pagenary.mjs +116 -0
  4. package/build.config.json +5 -0
  5. package/package.json +66 -0
  6. package/scripts/build-site.js +87 -0
  7. package/scripts/build-tenants.js +3569 -0
  8. package/scripts/build.js +99 -0
  9. package/scripts/generate-sections.js +41 -0
  10. package/scripts/lib/seo-generator.js +558 -0
  11. package/scripts/lint-content.js +62 -0
  12. package/scripts/seo-smoke.js +94 -0
  13. package/scripts/serve.js +142 -0
  14. package/site/app.js +1 -0
  15. package/site/index.html +57 -0
  16. package/site/lib/categories.js +1 -0
  17. package/site/lib/export.js +1 -0
  18. package/site/lib/manifest-utils.js +1 -0
  19. package/site/lib/router.js +1 -0
  20. package/site/lib/search.js +1 -0
  21. package/site/llms.txt +22 -0
  22. package/site/manifest.js +132 -0
  23. package/site/mermaid-init.js +1 -0
  24. package/site/pages/api.html +339 -0
  25. package/site/pages/architecture.html +303 -0
  26. package/site/pages/deployment.html +282 -0
  27. package/site/pages/developer-guide.html +157 -0
  28. package/site/pages/extending.html +135 -0
  29. package/site/pages/quickstart.html +318 -0
  30. package/site/pages/seo-strategy.html +121 -0
  31. package/site/pages/tenant-config.html +519 -0
  32. package/site/pages/welcome.html +116 -0
  33. package/site/robots.txt +10 -0
  34. package/site/sections/api.js +3 -0
  35. package/site/sections/architecture.js +3 -0
  36. package/site/sections/deployment.js +3 -0
  37. package/site/sections/developer-guide.js +3 -0
  38. package/site/sections/extending.js +3 -0
  39. package/site/sections/quickstart.js +3 -0
  40. package/site/sections/section-templates.js +1 -0
  41. package/site/sections/seo-strategy.js +3 -0
  42. package/site/sections/tenant-config.js +3 -0
  43. package/site/sections/welcome.js +3 -0
  44. package/site/seo.js +1 -0
  45. package/site/sitemap.xml +63 -0
  46. package/site/styles.css +1982 -0
  47. package/site/syntax-highlight.js +1 -0
  48. package/src/app.js +988 -0
  49. package/src/index.html +56 -0
  50. package/src/lib/categories.js +55 -0
  51. package/src/lib/export.js +195 -0
  52. package/src/lib/manifest-utils.js +69 -0
  53. package/src/lib/router.js +44 -0
  54. package/src/lib/search.js +151 -0
  55. package/src/manifest.js +246 -0
  56. package/src/mermaid-init.js +207 -0
  57. package/src/sections/archive-future-roadmap.js +7 -0
  58. package/src/sections/archive-initiative-alpha.js +7 -0
  59. package/src/sections/archive-milestone-records.js +7 -0
  60. package/src/sections/archive-timeline-overview.js +7 -0
  61. package/src/sections/core-technology-compliance-frameworks.js +7 -0
  62. package/src/sections/core-technology-coordination-model.js +7 -0
  63. package/src/sections/core-technology-data-definitions.js +7 -0
  64. package/src/sections/core-technology-hardware-integration.js +7 -0
  65. package/src/sections/core-technology-integrity-controls.js +7 -0
  66. package/src/sections/core-technology-network-topology.js +7 -0
  67. package/src/sections/core-technology-operator-requirements.js +7 -0
  68. package/src/sections/core-technology-overview.js +7 -0
  69. package/src/sections/core-technology-service-interfaces.js +7 -0
  70. package/src/sections/core-technology-synchronization-strategy.js +7 -0
  71. package/src/sections/core-technology-system-foundation.js +7 -0
  72. package/src/sections/developers-api-credentials.js +7 -0
  73. package/src/sections/developers-api-operations.js +7 -0
  74. package/src/sections/developers-api-reference.js +7 -0
  75. package/src/sections/developers-api-websocket.js +7 -0
  76. package/src/sections/developers-automation-blueprints.js +7 -0
  77. package/src/sections/developers-automation-modules.js +7 -0
  78. package/src/sections/developers-automation-patterns.js +7 -0
  79. package/src/sections/developers-deployment-playbook.js +7 -0
  80. package/src/sections/developers-overview.js +7 -0
  81. package/src/sections/developers-scheduling-patterns.js +7 -0
  82. package/src/sections/developers-sdk-go.js +7 -0
  83. package/src/sections/developers-sdk-javascript.js +7 -0
  84. package/src/sections/developers-sdk-python.js +7 -0
  85. package/src/sections/developers-sdk-rust.js +7 -0
  86. package/src/sections/developers-sdks.js +7 -0
  87. package/src/sections/developers-solution-examples.js +7 -0
  88. package/src/sections/developers-testing-framework.js +7 -0
  89. package/src/sections/getting-started-architecture-basics.js +7 -0
  90. package/src/sections/getting-started-introduction.js +7 -0
  91. package/src/sections/getting-started-performance-overview.js +7 -0
  92. package/src/sections/governance-community-initiatives.js +7 -0
  93. package/src/sections/governance-dao-overview.js +7 -0
  94. package/src/sections/governance-multi-token.js +7 -0
  95. package/src/sections/governance-overview.js +7 -0
  96. package/src/sections/governance-proposal-process.js +7 -0
  97. package/src/sections/governance-proposals.js +7 -0
  98. package/src/sections/governance-structure.js +7 -0
  99. package/src/sections/governance-token-distribution.js +7 -0
  100. package/src/sections/governance-treasury.js +7 -0
  101. package/src/sections/operations-environment-prep.js +7 -0
  102. package/src/sections/operations-getting-started.js +7 -0
  103. package/src/sections/operations-incentives-guide.js +7 -0
  104. package/src/sections/operations-incentives-strategies.js +7 -0
  105. package/src/sections/operations-incentives.js +7 -0
  106. package/src/sections/operations-infrastructure.js +7 -0
  107. package/src/sections/operations-monitoring.js +7 -0
  108. package/src/sections/operations-overview.js +7 -0
  109. package/src/sections/operations-performance.js +7 -0
  110. package/src/sections/operations-power-infrastructure.js +7 -0
  111. package/src/sections/operations-setup-guide.js +7 -0
  112. package/src/sections/operations-sync-setup.js +7 -0
  113. package/src/sections/products-flagship-solution.js +7 -0
  114. package/src/sections/products-solution-library.js +7 -0
  115. package/src/sections/resources-brand-assets.js +7 -0
  116. package/src/sections/resources-faq.js +7 -0
  117. package/src/sections/resources-glossary.js +7 -0
  118. package/src/sections/resources-research-papers.js +7 -0
  119. package/src/sections/section-templates.js +873 -0
  120. package/src/sections/security-audits.js +7 -0
  121. package/src/sections/security-best-practices.js +7 -0
  122. package/src/sections/security-bug-bounty.js +7 -0
  123. package/src/sections/security-incident-response.js +7 -0
  124. package/src/sections/security-overview.js +7 -0
  125. package/src/sections/technical-architecture.js +7 -0
  126. package/src/sections/technical-whitepaper.js +7 -0
  127. package/src/sections/tutorial-automation-bot.js +7 -0
  128. package/src/sections/tutorial-build-first-integration.js +7 -0
  129. package/src/sections/tutorial-deploy-automation.js +7 -0
  130. package/src/sections/tutorial-event-driven-experience.js +7 -0
  131. package/src/sections/tutorial-operations-onboarding.js +7 -0
  132. package/src/sections/tutorial-systems-integration.js +7 -0
  133. package/src/sections/tutorials-overview.js +7 -0
  134. package/src/sections/use-case-connected-devices.js +7 -0
  135. package/src/sections/use-case-digital-auctions.js +7 -0
  136. package/src/sections/use-case-financial-automation.js +7 -0
  137. package/src/sections/use-case-interactive-media.js +7 -0
  138. package/src/sections/use-case-realtime-execution.js +7 -0
  139. package/src/sections/use-case-research-analytics.js +7 -0
  140. package/src/sections/use-case-supply-operations.js +7 -0
  141. package/src/sections/use-cases-overview.js +7 -0
  142. package/src/sections/welcome-overview.js +7 -0
  143. package/src/seo.js +90 -0
  144. package/src/styles.css +1982 -0
  145. package/src/syntax-highlight.js +90 -0
  146. package/tenants.json.example +68 -0
  147. package/tenants.schema.json +231 -0
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /* Minimal build: copy src -> dist with optional terser minification */
3
+ import fs from 'fs';
4
+ import fsp from 'fs/promises';
5
+ import path from 'path';
6
+ import { createRequire } from 'module';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const root = process.cwd();
10
+ const srcDir = path.join(root, 'src');
11
+ const outputDirEnv = process.env.BUILD_OUTPUT || process.env.DIST_DIR;
12
+ const distDir = outputDirEnv ? path.resolve(root, outputDirEnv) : path.join(root, 'dist');
13
+
14
+ const argv = process.argv.slice(2);
15
+ const DEV = argv.includes('--dev') || process.env.NODE_ENV === 'development';
16
+ const FORCE_MINIFY = argv.includes('--force-minify') || /^(1|true)$/i.test(process.env.FORCE_MINIFY || '');
17
+ let buildConfig = {};
18
+ const cfgPath = path.join(root, 'build.config.json');
19
+ if (fs.existsSync(cfgPath)) {
20
+ try { buildConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {}; }
21
+ catch (err) { console.warn('Could not read build.config.json:', err.message); }
22
+ }
23
+ const minifyDefault = buildConfig.minify !== false;
24
+ const MINIFY = FORCE_MINIFY ? true : (DEV ? false : minifyDefault);
25
+ const baseReserved = Array.isArray(buildConfig.mangleReserved) ? buildConfig.mangleReserved : [];
26
+ const sectionReserved = Array.isArray(buildConfig.sectionsMangleReserved) ? buildConfig.sectionsMangleReserved : ['load'];
27
+ let terser = null;
28
+ let attemptedTerser = false;
29
+
30
+ async function rimraf(dir) {
31
+ if (!fs.existsSync(dir)) return;
32
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
33
+ await Promise.all(entries.map((entry) => entry.isDirectory()
34
+ ? rimraf(path.join(dir, entry.name))
35
+ : fsp.unlink(path.join(dir, entry.name))));
36
+ await fsp.rmdir(dir).catch(() => {});
37
+ }
38
+
39
+ async function ensureDir(dir) {
40
+ await fsp.mkdir(dir, { recursive: true });
41
+ }
42
+
43
+ async function copyEntry(from, to) {
44
+ const stat = await fsp.lstat(from);
45
+ if (stat.isDirectory()) {
46
+ await ensureDir(to);
47
+ const entries = await fsp.readdir(from);
48
+ for (const name of entries) {
49
+ await copyEntry(path.join(from, name), path.join(to, name));
50
+ }
51
+ return;
52
+ }
53
+
54
+ let content = await fsp.readFile(from, 'utf8');
55
+ if (path.basename(from) === 'index.html') {
56
+ content = content.replace('</head>', ` <meta name="x-build" content="${new Date().toISOString()}" />\n </head>`);
57
+ }
58
+
59
+ if (MINIFY && from.endsWith('.js')) {
60
+ if (!attemptedTerser) {
61
+ attemptedTerser = true;
62
+ try { terser = require('terser'); }
63
+ catch { console.warn('Terser not installed; skipping minification.'); }
64
+ }
65
+ if (terser) {
66
+ try {
67
+ const isSection = from.includes(`${path.sep}sections${path.sep}`);
68
+ const reserved = Array.from(new Set([...(isSection ? sectionReserved : baseReserved)]));
69
+ const result = await terser.minify(content, {
70
+ module: true,
71
+ compress: { passes: 2 },
72
+ mangle: { toplevel: true, reserved }
73
+ });
74
+ if (result.code) content = result.code;
75
+ } catch (err) {
76
+ console.warn('Minification error in', path.relative(root, from), '-', err.message);
77
+ }
78
+ }
79
+ }
80
+
81
+ await ensureDir(path.dirname(to));
82
+ await fsp.writeFile(to, content, 'utf8');
83
+ }
84
+
85
+ async function main() {
86
+ if (!fs.existsSync(srcDir)) {
87
+ console.error('src directory missing.');
88
+ process.exit(1);
89
+ }
90
+ await rimraf(distDir);
91
+ await ensureDir(distDir);
92
+ await copyEntry(srcDir, distDir);
93
+ console.log(`Built docs-toolkit -> ${path.relative(root, distDir)}/ ${MINIFY ? '(minified)' : '(dev copy)'}`);
94
+ }
95
+
96
+ main().catch((err) => {
97
+ console.error(err);
98
+ process.exit(1);
99
+ });
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const ROOT = process.cwd();
6
+ const SECTIONS_DIR = path.join(ROOT, 'src', 'sections');
7
+ const TEMPLATE_FILENAME = 'section-templates.js';
8
+
9
+ function buildModule(id) {
10
+ return `import { renderSectionTemplate } from './section-templates.js';
11
+
12
+ const SECTION_ID = '${id}';
13
+
14
+ export async function load() {
15
+ return { html: renderSectionTemplate({ id: SECTION_ID }) };
16
+ }
17
+ `;
18
+ }
19
+
20
+ function synchronizeSections() {
21
+ if (!fs.existsSync(SECTIONS_DIR)) {
22
+ console.error('Sections directory not found:', SECTIONS_DIR);
23
+ process.exit(1);
24
+ }
25
+
26
+ const entries = fs.readdirSync(SECTIONS_DIR).filter((file) => file.endsWith('.js') && file !== TEMPLATE_FILENAME);
27
+ if (!entries.length) {
28
+ console.warn('No sections found to synchronize.');
29
+ return;
30
+ }
31
+
32
+ entries.forEach((file) => {
33
+ const sectionId = file.replace(/\.js$/, '');
34
+ const modulePath = path.join(SECTIONS_DIR, file);
35
+ const nextContent = buildModule(sectionId);
36
+ fs.writeFileSync(modulePath, nextContent, 'utf8');
37
+ console.log(`Updated ${sectionId}`);
38
+ });
39
+ }
40
+
41
+ synchronizeSections();
@@ -0,0 +1,558 @@
1
+ /**
2
+ * SEO Generator Utilities
3
+ * Build-time generation of sitemap.xml, robots.txt, static HTML snapshots, and JSON-LD
4
+ */
5
+
6
+ import * as fsp from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+
9
+ /**
10
+ * Encode section ID for use as filename (replace / with --)
11
+ * @param {string} sectionId - Section ID like "guides/getting-started"
12
+ * @returns {string} Filename-safe string like "guides--getting-started"
13
+ */
14
+ export function encodePathForFilename(sectionId) {
15
+ return sectionId.replace(/\//g, '--');
16
+ }
17
+
18
+ /**
19
+ * Collect all sections with modules from manifest (recursive)
20
+ * @param {Array} manifest - Navigation manifest
21
+ * @param {string|null} parentTitle - Parent group title for breadcrumbs
22
+ * @returns {Array} Flat array of sections with metadata
23
+ */
24
+ export function collectAllSections(manifest, parentTitle = null) {
25
+ const sections = [];
26
+ for (const entry of manifest) {
27
+ if (entry.module) {
28
+ sections.push({
29
+ id: entry.id,
30
+ title: entry.title,
31
+ summary: entry.summary || '',
32
+ module: entry.module,
33
+ parent: parentTitle
34
+ });
35
+ }
36
+ if (entry.subsections) {
37
+ sections.push(...collectAllSections(entry.subsections, entry.title));
38
+ }
39
+ }
40
+ return sections;
41
+ }
42
+
43
+ /**
44
+ * Generate XML sitemap content
45
+ * @param {Array} urls - Array of URL objects with loc, priority, changefreq
46
+ * @returns {string} XML sitemap content
47
+ */
48
+ export function buildSitemapXml(urls) {
49
+ const urlEntries = urls.map(url => {
50
+ const lastmod = url.lastmod || new Date().toISOString().split('T')[0];
51
+ return ` <url>
52
+ <loc>${escapeXml(url.loc)}</loc>
53
+ <lastmod>${lastmod}</lastmod>
54
+ <changefreq>${url.changefreq || 'monthly'}</changefreq>
55
+ <priority>${url.priority || '0.5'}</priority>
56
+ </url>`;
57
+ }).join('\n');
58
+
59
+ return `<?xml version="1.0" encoding="UTF-8"?>
60
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
61
+ ${urlEntries}
62
+ </urlset>
63
+ `;
64
+ }
65
+
66
+ /**
67
+ * Escape special characters for XML
68
+ */
69
+ function escapeXml(str) {
70
+ return str
71
+ .replace(/&/g, '&amp;')
72
+ .replace(/</g, '&lt;')
73
+ .replace(/>/g, '&gt;')
74
+ .replace(/"/g, '&quot;')
75
+ .replace(/'/g, '&apos;');
76
+ }
77
+
78
+ /**
79
+ * Generate sitemap.xml for a tenant
80
+ * @param {string} distDir - Tenant dist directory
81
+ * @param {Array} manifest - Navigation manifest
82
+ * @param {object} config - Tenant configuration
83
+ */
84
+ export async function generateSitemap(distDir, manifest, config) {
85
+ const seoConfig = config.seo || {};
86
+ if (seoConfig.generateSitemap === false) return;
87
+
88
+ const baseUrl = seoConfig.siteUrl || '';
89
+ const defaultChangeFreq = seoConfig.defaultChangeFreq || 'weekly';
90
+ const urls = [];
91
+
92
+ // Add root URL
93
+ urls.push({
94
+ loc: baseUrl ? `${baseUrl}/` : '/',
95
+ priority: '1.0',
96
+ changefreq: defaultChangeFreq
97
+ });
98
+
99
+ // Collect all sections (deduplicate by ID to prevent double entries)
100
+ const sections = collectAllSections(manifest);
101
+ const seen = new Set();
102
+
103
+ for (const section of sections) {
104
+ if (seen.has(section.id)) continue;
105
+ seen.add(section.id);
106
+ const filename = encodePathForFilename(section.id);
107
+ urls.push({
108
+ loc: baseUrl ? `${baseUrl}/pages/${filename}.html` : `/pages/${filename}.html`,
109
+ priority: section.parent ? '0.6' : '0.8',
110
+ changefreq: 'monthly'
111
+ });
112
+ }
113
+
114
+ const xml = buildSitemapXml(urls);
115
+ await fsp.writeFile(path.join(distDir, 'sitemap.xml'), xml, 'utf8');
116
+ console.log(` ↳ generated sitemap.xml (${urls.length} unique URLs)`);
117
+ }
118
+
119
+ /**
120
+ * Generate robots.txt for a tenant
121
+ * @param {string} distDir - Tenant dist directory
122
+ * @param {object} config - Tenant configuration
123
+ */
124
+ export async function generateRobotsTxt(distDir, config) {
125
+ const seoConfig = config.seo || {};
126
+ if (seoConfig.generateRobotsTxt === false) return;
127
+
128
+ const baseUrl = seoConfig.siteUrl || '';
129
+ const sitemapUrl = baseUrl ? `${baseUrl}/sitemap.xml` : '/sitemap.xml';
130
+ const buildDate = new Date().toISOString();
131
+
132
+ const content = `# ${config.title || 'Documentation'}
133
+ # Generated: ${buildDate}
134
+
135
+ User-agent: *
136
+ Allow: /
137
+ Allow: /pages/
138
+ Disallow: /sections/
139
+ Disallow: /lib/
140
+
141
+ Sitemap: ${sitemapUrl}
142
+ `;
143
+
144
+ await fsp.writeFile(path.join(distDir, 'robots.txt'), content, 'utf8');
145
+ console.log(` ↳ generated robots.txt`);
146
+ }
147
+
148
+ /**
149
+ * Generate JSON-LD for a documentation page
150
+ * @param {object} section - Section metadata
151
+ * @param {object} config - Tenant configuration
152
+ * @returns {string} JSON-LD script content
153
+ */
154
+ export function buildPageJsonLd(section, config) {
155
+ const seoConfig = config.seo || {};
156
+ const baseUrl = seoConfig.siteUrl || '';
157
+ const canonicalUrl = `${baseUrl}/#${section.id}`;
158
+ const buildDate = new Date().toISOString().split('T')[0];
159
+
160
+ const articleSchema = {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'TechArticle',
163
+ headline: section.title,
164
+ description: section.summary || config.description || '',
165
+ url: canonicalUrl,
166
+ dateModified: buildDate,
167
+ mainEntityOfPage: {
168
+ '@type': 'WebPage',
169
+ '@id': canonicalUrl
170
+ },
171
+ isPartOf: {
172
+ '@type': 'WebSite',
173
+ name: config.title || 'Documentation',
174
+ url: baseUrl || '/'
175
+ }
176
+ };
177
+
178
+ // Add publisher if organization info provided
179
+ if (seoConfig.structuredData?.organizationName) {
180
+ articleSchema.publisher = {
181
+ '@type': 'Organization',
182
+ name: seoConfig.structuredData.organizationName
183
+ };
184
+ if (seoConfig.structuredData.logoUrl) {
185
+ articleSchema.publisher.logo = seoConfig.structuredData.logoUrl;
186
+ }
187
+ }
188
+
189
+ // Build breadcrumb schema
190
+ const breadcrumbItems = [
191
+ { '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl || '/' }
192
+ ];
193
+
194
+ if (section.parent) {
195
+ breadcrumbItems.push({
196
+ '@type': 'ListItem',
197
+ position: 2,
198
+ name: section.parent,
199
+ item: `${baseUrl}/#${section.parent.toLowerCase().replace(/\s+/g, '-')}`
200
+ });
201
+ breadcrumbItems.push({
202
+ '@type': 'ListItem',
203
+ position: 3,
204
+ name: section.title,
205
+ item: canonicalUrl
206
+ });
207
+ } else {
208
+ breadcrumbItems.push({
209
+ '@type': 'ListItem',
210
+ position: 2,
211
+ name: section.title,
212
+ item: canonicalUrl
213
+ });
214
+ }
215
+
216
+ const breadcrumbSchema = {
217
+ '@context': 'https://schema.org',
218
+ '@type': 'BreadcrumbList',
219
+ itemListElement: breadcrumbItems
220
+ };
221
+
222
+ return JSON.stringify([articleSchema, breadcrumbSchema], null, 2);
223
+ }
224
+
225
+ /**
226
+ * Generate JSON-LD for the home page (WebSite schema)
227
+ * @param {object} config - Tenant configuration
228
+ * @returns {string} JSON-LD content
229
+ */
230
+ export function buildHomePageJsonLd(config) {
231
+ const seoConfig = config.seo || {};
232
+ const baseUrl = seoConfig.siteUrl || '';
233
+
234
+ const schema = {
235
+ '@context': 'https://schema.org',
236
+ '@type': 'WebSite',
237
+ name: config.title || 'Documentation',
238
+ description: config.description || '',
239
+ url: baseUrl || '/'
240
+ };
241
+
242
+ // Add search action if search is available
243
+ if (baseUrl) {
244
+ schema.potentialAction = {
245
+ '@type': 'SearchAction',
246
+ target: `${baseUrl}/#?q={search_term_string}`,
247
+ 'query-input': 'required name=search_term_string'
248
+ };
249
+ }
250
+
251
+ return JSON.stringify(schema, null, 2);
252
+ }
253
+
254
+ /**
255
+ * Build static HTML page for a section
256
+ * @param {object} options - Page options
257
+ * @returns {string} Complete HTML document
258
+ */
259
+ export function buildStaticPage(options) {
260
+ const {
261
+ sectionId,
262
+ sectionTitle,
263
+ sectionSummary,
264
+ sectionParent,
265
+ contentHtml,
266
+ siteTitle,
267
+ baseUrl,
268
+ config
269
+ } = options;
270
+
271
+ const canonicalUrl = `${baseUrl}/#${sectionId}`;
272
+ const jsonLd = buildPageJsonLd({
273
+ id: sectionId,
274
+ title: sectionTitle,
275
+ summary: sectionSummary,
276
+ parent: sectionParent
277
+ }, config);
278
+
279
+ // Escape content for HTML
280
+ const safeTitle = escapeHtml(sectionTitle);
281
+ const safeSiteTitle = escapeHtml(siteTitle);
282
+ const safeSummary = escapeHtml(sectionSummary || '');
283
+
284
+ return `<!doctype html>
285
+ <html lang="en">
286
+ <head>
287
+ <meta charset="utf-8" />
288
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
289
+ <title>${safeTitle} | ${safeSiteTitle}</title>
290
+ <meta name="description" content="${safeSummary}" />
291
+ <link rel="canonical" href="${canonicalUrl}" />
292
+
293
+ <!-- Open Graph -->
294
+ <meta property="og:title" content="${safeTitle}" />
295
+ <meta property="og:description" content="${safeSummary}" />
296
+ <meta property="og:type" content="article" />
297
+ <meta property="og:url" content="${canonicalUrl}" />
298
+
299
+ <!-- Twitter Card -->
300
+ <meta name="twitter:card" content="summary" />
301
+ <meta name="twitter:title" content="${safeTitle}" />
302
+ <meta name="twitter:description" content="${safeSummary}" />
303
+
304
+ <!-- Structured Data -->
305
+ <script type="application/ld+json">
306
+ ${jsonLd}
307
+ </script>
308
+
309
+ <!-- Redirect to SPA for JavaScript-enabled browsers -->
310
+ <script>
311
+ if (typeof window !== 'undefined') {
312
+ window.location.replace('${baseUrl}/#${sectionId}');
313
+ }
314
+ </script>
315
+ <noscript>
316
+ <meta http-equiv="refresh" content="0; url=${baseUrl}/#${sectionId}" />
317
+ </noscript>
318
+
319
+ <link rel="stylesheet" href="../styles.css" />
320
+ <style>
321
+ .static-content { max-width: 800px; margin: 0 auto; padding: 2rem; }
322
+ .static-footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #eee; font-size: 0.9rem; color: #666; }
323
+ </style>
324
+ </head>
325
+ <body>
326
+ <main class="static-content">
327
+ <article>
328
+ <h1>${safeTitle}</h1>
329
+ ${sectionSummary ? `<p class="lead">${safeSummary}</p>` : ''}
330
+ <div class="section-body">
331
+ ${contentHtml}
332
+ </div>
333
+ </article>
334
+ <footer class="static-footer">
335
+ <p>View interactive version: <a href="${canonicalUrl}">${safeTitle}</a></p>
336
+ </footer>
337
+ </main>
338
+ </body>
339
+ </html>
340
+ `;
341
+ }
342
+
343
+ /**
344
+ * Escape HTML special characters
345
+ */
346
+ function escapeHtml(str) {
347
+ if (!str) return '';
348
+ return str
349
+ .replace(/&/g, '&amp;')
350
+ .replace(/</g, '&lt;')
351
+ .replace(/>/g, '&gt;')
352
+ .replace(/"/g, '&quot;');
353
+ }
354
+
355
+ /**
356
+ * Extract HTML content from a compiled section module
357
+ * @param {string} moduleContent - The JS module source
358
+ * @returns {string|null} The HTML content or null
359
+ */
360
+ export function extractHtmlFromModule(moduleContent) {
361
+ // Look for the html property in the module
362
+ // Pattern: html: "..." or html: `...`
363
+ const patterns = [
364
+ /html:\s*"((?:[^"\\]|\\.)*)"/s,
365
+ /html:\s*`((?:[^`\\]|\\.)*)`/s,
366
+ /html:\s*'((?:[^'\\]|\\.)*)'/s
367
+ ];
368
+
369
+ for (const pattern of patterns) {
370
+ const match = moduleContent.match(pattern);
371
+ if (match) {
372
+ // Unescape the string
373
+ try {
374
+ return JSON.parse(`"${match[1].replace(/"/g, '\\"')}"`);
375
+ } catch {
376
+ // Try direct return if JSON parse fails
377
+ return match[1]
378
+ .replace(/\\n/g, '\n')
379
+ .replace(/\\t/g, '\t')
380
+ .replace(/\\"/g, '"')
381
+ .replace(/\\\\/g, '\\');
382
+ }
383
+ }
384
+ }
385
+ return null;
386
+ }
387
+
388
+ /**
389
+ * Generate static HTML snapshots for all sections
390
+ * @param {string} distDir - Tenant dist directory
391
+ * @param {Array} manifest - Navigation manifest
392
+ * @param {object} config - Tenant configuration
393
+ */
394
+ export async function generateStaticSnapshots(distDir, manifest, config) {
395
+ const seoConfig = config.seo || {};
396
+ if (seoConfig.generateStaticPages === false) return;
397
+
398
+ const pagesDir = path.join(distDir, 'pages');
399
+ await fsp.mkdir(pagesDir, { recursive: true });
400
+
401
+ const baseUrl = seoConfig.siteUrl || '';
402
+ const sections = collectAllSections(manifest);
403
+ let generated = 0;
404
+
405
+ for (const section of sections) {
406
+ try {
407
+ // Read the compiled module
408
+ const modulePath = path.join(distDir, section.module);
409
+ const moduleContent = await fsp.readFile(modulePath, 'utf8');
410
+
411
+ // Extract HTML content
412
+ const contentHtml = extractHtmlFromModule(moduleContent);
413
+ if (!contentHtml) {
414
+ console.warn(` ⚠ Could not extract HTML from ${section.id}`);
415
+ continue;
416
+ }
417
+
418
+ // Generate static page
419
+ const pageHtml = buildStaticPage({
420
+ sectionId: section.id,
421
+ sectionTitle: section.title,
422
+ sectionSummary: section.summary,
423
+ sectionParent: section.parent,
424
+ contentHtml,
425
+ siteTitle: config.title || 'Documentation',
426
+ baseUrl,
427
+ config
428
+ });
429
+
430
+ // Write the file
431
+ const filename = `${encodePathForFilename(section.id)}.html`;
432
+ await fsp.writeFile(path.join(pagesDir, filename), pageHtml, 'utf8');
433
+ generated++;
434
+ } catch (err) {
435
+ console.warn(` ⚠ Failed to generate static page for ${section.id}: ${err.message}`);
436
+ }
437
+ }
438
+
439
+ console.log(` ↳ generated ${generated} static HTML snapshots in /pages/`);
440
+ }
441
+
442
+ /**
443
+ * Read and parse the MANIFEST from a generated manifest.js file
444
+ * @param {string} distDir - Tenant dist directory
445
+ * @returns {Array|null} The manifest array or null if not found
446
+ */
447
+ export async function readManifestFromDist(distDir) {
448
+ const manifestPath = path.join(distDir, 'manifest.js');
449
+ try {
450
+ const content = await fsp.readFile(manifestPath, 'utf8');
451
+ // Extract the MANIFEST array from the module
452
+ const match = content.match(/export\s+const\s+MANIFEST\s*=\s*(\[[\s\S]*?\]);/);
453
+ if (match) {
454
+ // Use Function constructor to safely evaluate the JSON-like array
455
+ // eslint-disable-next-line no-new-func
456
+ const manifest = new Function(`return ${match[1]}`)();
457
+ return manifest;
458
+ }
459
+ } catch (err) {
460
+ console.warn(` ⚠ Could not read manifest: ${err.message}`);
461
+ }
462
+ return null;
463
+ }
464
+
465
+ /**
466
+ * Generate llms.txt for LLM-friendly site discovery
467
+ * Follows the llms.txt specification (https://llmstxt.org/)
468
+ * @param {string} distDir - Tenant dist directory
469
+ * @param {Array} manifest - Navigation manifest
470
+ * @param {object} config - Tenant configuration
471
+ */
472
+ export async function generateLlmsTxt(distDir, manifest, config) {
473
+ const seoConfig = config.seo || {};
474
+ const baseUrl = seoConfig.siteUrl || '';
475
+ const title = config.title || 'Documentation';
476
+ const description = config.description || '';
477
+
478
+ const lines = [];
479
+ lines.push(`# ${title}`);
480
+ if (description) {
481
+ lines.push('');
482
+ lines.push(`> ${description}`);
483
+ }
484
+
485
+ // Group sections by top-level entries
486
+ for (const entry of manifest) {
487
+ if (entry.url) continue; // Skip external links
488
+
489
+ if (entry.subsections && entry.subsections.length > 0) {
490
+ lines.push('');
491
+ lines.push(`## ${entry.title}`);
492
+ if (entry.summary) lines.push('');
493
+
494
+ // Add group's own page if it has one
495
+ if (entry.module) {
496
+ const filename = encodePathForFilename(entry.id);
497
+ const url = baseUrl ? `${baseUrl}/pages/${filename}.html` : `/pages/${filename}.html`;
498
+ lines.push(`- [${entry.title}](${url}): ${entry.summary || ''}`);
499
+ }
500
+
501
+ // Add subsection pages
502
+ for (const sub of entry.subsections) {
503
+ if (!sub.module && !sub.subsections) continue;
504
+ if (sub.module) {
505
+ const filename = encodePathForFilename(sub.id);
506
+ const url = baseUrl ? `${baseUrl}/pages/${filename}.html` : `/pages/${filename}.html`;
507
+ lines.push(`- [${sub.title}](${url}): ${sub.summary || ''}`);
508
+ }
509
+ // Recurse one more level for deeply nested sections
510
+ if (sub.subsections) {
511
+ for (const nested of sub.subsections) {
512
+ if (!nested.module) continue;
513
+ const filename = encodePathForFilename(nested.id);
514
+ const url = baseUrl ? `${baseUrl}/pages/${filename}.html` : `/pages/${filename}.html`;
515
+ lines.push(`- [${nested.title}](${url}): ${nested.summary || ''}`);
516
+ }
517
+ }
518
+ }
519
+ } else if (entry.module) {
520
+ // Top-level page without subsections
521
+ const filename = encodePathForFilename(entry.id);
522
+ const url = baseUrl ? `${baseUrl}/pages/${filename}.html` : `/pages/${filename}.html`;
523
+ lines.push('');
524
+ lines.push(`- [${entry.title}](${url}): ${entry.summary || ''}`);
525
+ }
526
+ }
527
+
528
+ const content = lines.join('\n') + '\n';
529
+ await fsp.writeFile(path.join(distDir, 'llms.txt'), content, 'utf8');
530
+ console.log(` ↳ generated llms.txt`);
531
+ }
532
+
533
+ /**
534
+ * Generate all SEO artifacts for a tenant
535
+ * @param {string} distDir - Tenant dist directory
536
+ * @param {object} config - Tenant configuration
537
+ */
538
+ export async function generateSeoArtifacts(distDir, config) {
539
+ const seoConfig = config.seo || {};
540
+
541
+ // Skip if SEO is explicitly disabled
542
+ if (seoConfig.enabled === false) {
543
+ return;
544
+ }
545
+
546
+ // Read manifest from generated file
547
+ const manifest = await readManifestFromDist(distDir);
548
+ if (!manifest) {
549
+ console.warn(` ⚠ Could not generate SEO artifacts: manifest not found`);
550
+ return;
551
+ }
552
+
553
+ // Generate each artifact
554
+ await generateSitemap(distDir, manifest, config);
555
+ await generateRobotsTxt(distDir, config);
556
+ await generateLlmsTxt(distDir, manifest, config);
557
+ await generateStaticSnapshots(distDir, manifest, config);
558
+ }