@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.
- package/LICENSE +661 -0
- package/README.md +337 -0
- package/bin/pagenary.mjs +116 -0
- package/build.config.json +5 -0
- package/package.json +66 -0
- package/scripts/build-site.js +87 -0
- package/scripts/build-tenants.js +3569 -0
- package/scripts/build.js +99 -0
- package/scripts/generate-sections.js +41 -0
- package/scripts/lib/seo-generator.js +558 -0
- package/scripts/lint-content.js +62 -0
- package/scripts/seo-smoke.js +94 -0
- package/scripts/serve.js +142 -0
- package/site/app.js +1 -0
- package/site/index.html +57 -0
- package/site/lib/categories.js +1 -0
- package/site/lib/export.js +1 -0
- package/site/lib/manifest-utils.js +1 -0
- package/site/lib/router.js +1 -0
- package/site/lib/search.js +1 -0
- package/site/llms.txt +22 -0
- package/site/manifest.js +132 -0
- package/site/mermaid-init.js +1 -0
- package/site/pages/api.html +339 -0
- package/site/pages/architecture.html +303 -0
- package/site/pages/deployment.html +282 -0
- package/site/pages/developer-guide.html +157 -0
- package/site/pages/extending.html +135 -0
- package/site/pages/quickstart.html +318 -0
- package/site/pages/seo-strategy.html +121 -0
- package/site/pages/tenant-config.html +519 -0
- package/site/pages/welcome.html +116 -0
- package/site/robots.txt +10 -0
- package/site/sections/api.js +3 -0
- package/site/sections/architecture.js +3 -0
- package/site/sections/deployment.js +3 -0
- package/site/sections/developer-guide.js +3 -0
- package/site/sections/extending.js +3 -0
- package/site/sections/quickstart.js +3 -0
- package/site/sections/section-templates.js +1 -0
- package/site/sections/seo-strategy.js +3 -0
- package/site/sections/tenant-config.js +3 -0
- package/site/sections/welcome.js +3 -0
- package/site/seo.js +1 -0
- package/site/sitemap.xml +63 -0
- package/site/styles.css +1982 -0
- package/site/syntax-highlight.js +1 -0
- package/src/app.js +988 -0
- package/src/index.html +56 -0
- package/src/lib/categories.js +55 -0
- package/src/lib/export.js +195 -0
- package/src/lib/manifest-utils.js +69 -0
- package/src/lib/router.js +44 -0
- package/src/lib/search.js +151 -0
- package/src/manifest.js +246 -0
- package/src/mermaid-init.js +207 -0
- package/src/sections/archive-future-roadmap.js +7 -0
- package/src/sections/archive-initiative-alpha.js +7 -0
- package/src/sections/archive-milestone-records.js +7 -0
- package/src/sections/archive-timeline-overview.js +7 -0
- package/src/sections/core-technology-compliance-frameworks.js +7 -0
- package/src/sections/core-technology-coordination-model.js +7 -0
- package/src/sections/core-technology-data-definitions.js +7 -0
- package/src/sections/core-technology-hardware-integration.js +7 -0
- package/src/sections/core-technology-integrity-controls.js +7 -0
- package/src/sections/core-technology-network-topology.js +7 -0
- package/src/sections/core-technology-operator-requirements.js +7 -0
- package/src/sections/core-technology-overview.js +7 -0
- package/src/sections/core-technology-service-interfaces.js +7 -0
- package/src/sections/core-technology-synchronization-strategy.js +7 -0
- package/src/sections/core-technology-system-foundation.js +7 -0
- package/src/sections/developers-api-credentials.js +7 -0
- package/src/sections/developers-api-operations.js +7 -0
- package/src/sections/developers-api-reference.js +7 -0
- package/src/sections/developers-api-websocket.js +7 -0
- package/src/sections/developers-automation-blueprints.js +7 -0
- package/src/sections/developers-automation-modules.js +7 -0
- package/src/sections/developers-automation-patterns.js +7 -0
- package/src/sections/developers-deployment-playbook.js +7 -0
- package/src/sections/developers-overview.js +7 -0
- package/src/sections/developers-scheduling-patterns.js +7 -0
- package/src/sections/developers-sdk-go.js +7 -0
- package/src/sections/developers-sdk-javascript.js +7 -0
- package/src/sections/developers-sdk-python.js +7 -0
- package/src/sections/developers-sdk-rust.js +7 -0
- package/src/sections/developers-sdks.js +7 -0
- package/src/sections/developers-solution-examples.js +7 -0
- package/src/sections/developers-testing-framework.js +7 -0
- package/src/sections/getting-started-architecture-basics.js +7 -0
- package/src/sections/getting-started-introduction.js +7 -0
- package/src/sections/getting-started-performance-overview.js +7 -0
- package/src/sections/governance-community-initiatives.js +7 -0
- package/src/sections/governance-dao-overview.js +7 -0
- package/src/sections/governance-multi-token.js +7 -0
- package/src/sections/governance-overview.js +7 -0
- package/src/sections/governance-proposal-process.js +7 -0
- package/src/sections/governance-proposals.js +7 -0
- package/src/sections/governance-structure.js +7 -0
- package/src/sections/governance-token-distribution.js +7 -0
- package/src/sections/governance-treasury.js +7 -0
- package/src/sections/operations-environment-prep.js +7 -0
- package/src/sections/operations-getting-started.js +7 -0
- package/src/sections/operations-incentives-guide.js +7 -0
- package/src/sections/operations-incentives-strategies.js +7 -0
- package/src/sections/operations-incentives.js +7 -0
- package/src/sections/operations-infrastructure.js +7 -0
- package/src/sections/operations-monitoring.js +7 -0
- package/src/sections/operations-overview.js +7 -0
- package/src/sections/operations-performance.js +7 -0
- package/src/sections/operations-power-infrastructure.js +7 -0
- package/src/sections/operations-setup-guide.js +7 -0
- package/src/sections/operations-sync-setup.js +7 -0
- package/src/sections/products-flagship-solution.js +7 -0
- package/src/sections/products-solution-library.js +7 -0
- package/src/sections/resources-brand-assets.js +7 -0
- package/src/sections/resources-faq.js +7 -0
- package/src/sections/resources-glossary.js +7 -0
- package/src/sections/resources-research-papers.js +7 -0
- package/src/sections/section-templates.js +873 -0
- package/src/sections/security-audits.js +7 -0
- package/src/sections/security-best-practices.js +7 -0
- package/src/sections/security-bug-bounty.js +7 -0
- package/src/sections/security-incident-response.js +7 -0
- package/src/sections/security-overview.js +7 -0
- package/src/sections/technical-architecture.js +7 -0
- package/src/sections/technical-whitepaper.js +7 -0
- package/src/sections/tutorial-automation-bot.js +7 -0
- package/src/sections/tutorial-build-first-integration.js +7 -0
- package/src/sections/tutorial-deploy-automation.js +7 -0
- package/src/sections/tutorial-event-driven-experience.js +7 -0
- package/src/sections/tutorial-operations-onboarding.js +7 -0
- package/src/sections/tutorial-systems-integration.js +7 -0
- package/src/sections/tutorials-overview.js +7 -0
- package/src/sections/use-case-connected-devices.js +7 -0
- package/src/sections/use-case-digital-auctions.js +7 -0
- package/src/sections/use-case-financial-automation.js +7 -0
- package/src/sections/use-case-interactive-media.js +7 -0
- package/src/sections/use-case-realtime-execution.js +7 -0
- package/src/sections/use-case-research-analytics.js +7 -0
- package/src/sections/use-case-supply-operations.js +7 -0
- package/src/sections/use-cases-overview.js +7 -0
- package/src/sections/welcome-overview.js +7 -0
- package/src/seo.js +90 -0
- package/src/styles.css +1982 -0
- package/src/syntax-highlight.js +90 -0
- package/tenants.json.example +68 -0
- package/tenants.schema.json +231 -0
package/scripts/build.js
ADDED
|
@@ -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, '&')
|
|
72
|
+
.replace(/</g, '<')
|
|
73
|
+
.replace(/>/g, '>')
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
.replace(/'/g, ''');
|
|
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, '&')
|
|
350
|
+
.replace(/</g, '<')
|
|
351
|
+
.replace(/>/g, '>')
|
|
352
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|