import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const pagePath = join(repoRoot, "content", "pages", "home.json");
const knowledgePath = join(repoRoot, "content", "knowledge.json");
const robotsPath = join(repoRoot, "robots.txt");
const sitemapPath = join(repoRoot, "sitemap.xml");
const templateRoot = join(repoRoot, "templates", "pages", "home");
const shellPath = join(templateRoot, "shell.html");
const blockRoot = join(templateRoot, "blocks");
const REGION_TOKEN = {
beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
cViz: "{{NODEDC_BLOCKS_CVIZ}}",
sections: "{{NODEDC_BLOCKS_SECTIONS}}",
};
const PAGE_SEO_DEFAULTS = {
title: "NODE.DC - платформа для процессов, данных и ИИ-агентов",
description:
"NODE.DC объединяет Hub, Engine, Ops, прикладные модули и сквозного ассистента для задач, workflow, данных, ролей, интеграций и инженерных моделей.",
canonicalUrl: "https://nodedc.dctouch.ru/",
robots: "index,follow,max-image-preview:large",
siteName: "NODE.DC",
locale: "ru_RU",
ogImage: "https://nodedc.dctouch.ru/assets/site/images/69b6acdde3123ef078e2b7f3_og-img.jpg",
imageAlt: "NODE.DC - платформа для процессов, данных и ИИ-агентов",
organizationName: "DCTOUCH",
organizationUrl: "https://dctouch.ru/",
applicationCategory: "BusinessApplication",
};
const page = JSON.parse(await readFile(pagePath, "utf8"));
const shell = await readFile(shellPath, "utf8");
const templateCache = new Map();
async function readTemplate(templateName) {
if (!templateName || templateName.includes("..") || templateName.includes("/")) {
throw new Error(`Unsafe template name: ${templateName}`);
}
if (!templateCache.has(templateName)) {
templateCache.set(templateName, await readFile(join(blockRoot, templateName), "utf8"));
}
return templateCache.get(templateName);
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getByPath(source, path) {
return path.split(".").reduce((value, key) => value?.[key], source);
}
function isTruthy(value) {
if (Array.isArray(value)) return value.length > 0;
return Boolean(value);
}
function cloneValue(value) {
return value == null ? value : JSON.parse(JSON.stringify(value));
}
function allBlocks(model) {
return (model.regions ?? []).flatMap((region) => region.blocks ?? []);
}
function applySeoDefaults(model) {
model.seo = {
...PAGE_SEO_DEFAULTS,
...(model.seo || {}),
};
}
function applySharedDemoRequest(model) {
const blocks = allBlocks(model);
const source = blocks.find((block) => block.id === "hero-waitlist");
if (!source?.content) return;
for (const block of blocks) {
if (block.id === source.id || block.type !== "waitlistFiles") continue;
block.content ??= {};
block.content.accessCard = cloneValue(source.content.accessCard);
block.content.form = cloneValue(source.content.form);
block.content.legal ??= { enabled: true, html: "" };
block.content.legal.html = source.content.legalHtml ?? block.content.legal.html ?? "";
}
}
const LINK_WINDOW_TARGETS = new Set(["_blank", "_self", "_parent", "_top"]);
function isWindowTarget(value) {
return LINK_WINDOW_TARGETS.has(String(value || "").trim());
}
function isExternalHref(value) {
return /^(https?:)?\/\//i.test(String(value || "")) || /^(mailto:|tel:)/i.test(String(value || ""));
}
function isInertHref(value) {
const href = String(value || "").trim();
return !href || href === "#" || href === "#hero" || href.toLowerCase().startsWith("javascript:");
}
function normalizeLinkForRender(item) {
if (!item || typeof item !== "object") return;
let href = item.href ?? "";
let target = item.target ?? "";
let linkTarget = item.linkTarget ?? "";
if (isWindowTarget(target)) {
linkTarget ||= target;
target = "";
}
const mode = item.linkMode === "href" || item.linkMode === "target" ? item.linkMode : isExternalHref(href) ? "href" : "target";
if (mode === "target" && !target && href && !isExternalHref(href)) {
target = href;
href = "";
}
const primaryHref = mode === "href" ? href : target;
const fallbackHref = mode === "href" ? target : href;
item.resolvedHref = primaryHref || fallbackHref || "#";
item.resolvedTarget = mode === "href" ? linkTarget : "";
}
function normalizeDockItemForRender(item) {
if (!item || typeof item !== "object") return;
const href = String(item.href || "").trim();
const opensWindow = !isInertHref(href);
item.resolvedDockHref = href || "#";
item.resolvedDockTarget = opensWindow ? "_blank" : "";
item.resolvedDockRel = opensWindow ? "noopener noreferrer" : "";
}
function applyRenderableLinks(model) {
const staticLinks = model.staticElements?.content?.navigation?.links;
if (Array.isArray(staticLinks)) {
staticLinks.forEach(normalizeLinkForRender);
}
const dockItems = model.staticElements?.content?.dock?.items;
if (Array.isArray(dockItems)) {
dockItems.forEach(normalizeDockItemForRender);
}
for (const block of allBlocks(model)) {
const footer = block.content?.footer;
if (!footer) continue;
if (Array.isArray(footer.navLinks)) footer.navLinks.forEach(normalizeLinkForRender);
if (Array.isArray(footer.contactLinks)) footer.contactLinks.forEach(normalizeLinkForRender);
}
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
}
function escapeAttr(value) {
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
}
function escapeJsString(value) {
return String(value)
.replaceAll("\\", "\\\\")
.replaceAll('"', '\\"')
.replaceAll("\n", "\\n")
.replaceAll("\r", "\\r")
.replaceAll("\u2028", "\\u2028")
.replaceAll("\u2029", "\\u2029")
.replaceAll("", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function stripHtml(value) {
return String(value ?? "")
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function normalizeLandingTarget(value) {
const raw = String(value || "").trim();
if (!raw || /^javascript:/i.test(raw) || isExternalHref(raw)) return "";
const hashValue = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw;
return hashValue
.replace(/^\/+/, "")
.replace(/^index\.html#?/i, "")
.replace(/^#+/, "")
.trim();
}
function validateLandingTargets(model) {
const targets = new Map();
for (const block of allBlocks(model)) {
if (block.enabled === false) continue;
const target = normalizeLandingTarget(block.anchor);
if (!target) continue;
if (targets.has(target)) {
const first = targets.get(target);
throw new Error(
`Duplicate landing target "${target}" in blocks "${first.adminLabel || first.id}" and "${block.adminLabel || block.id}"`,
);
}
targets.set(target, block);
}
}
function slugify(value, fallback = "section") {
const map = {
а: "a",
б: "b",
в: "v",
г: "g",
д: "d",
е: "e",
ё: "e",
ж: "zh",
з: "z",
и: "i",
й: "y",
к: "k",
л: "l",
м: "m",
н: "n",
о: "o",
п: "p",
р: "r",
с: "s",
т: "t",
у: "u",
ф: "f",
х: "h",
ц: "c",
ч: "ch",
ш: "sh",
щ: "sch",
ъ: "",
ы: "y",
ь: "",
э: "e",
ю: "yu",
я: "ya",
};
const safe = String(value ?? "")
.toLowerCase()
.replace(/[а-яё]/g, (char) => map[char] ?? "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return safe || fallback;
}
function collectKnowledgeUrls(source, canonical) {
let model;
try {
model = JSON.parse(source);
} catch {
return [];
}
const basePath = String(model.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge";
const urls = [new URL(`${basePath}/`, canonical).href];
function walk(nodes = [], parentSegments = []) {
nodes.forEach((node, index) => {
if (node.enabled === false) return;
const segment = slugify(node.slug || node.title || node.id, `page-${index + 1}`);
const segments = [...parentSegments, segment];
if (node.useContent !== false && stripHtml(node.bodyHtml).length > 0) {
urls.push(new URL(`${basePath}/${segments.join("/")}/`, canonical).href);
}
if (Array.isArray(node.children)) walk(node.children, segments);
});
}
walk(model.tree || []);
return urls;
}
function canonicalUrl(model) {
const fallback = "https://nodedc.dctouch.ru/";
const value = String(model.seo?.canonicalUrl || fallback).trim() || fallback;
try {
const url = new URL(value);
return url.href;
} catch {
return fallback;
}
}
async function writeSeoFiles(model) {
const canonical = canonicalUrl(model);
const sitemapUrl = new URL("sitemap.xml", canonical).href;
const knowledgeSource = await readFile(knowledgePath, "utf8").catch(() => "");
const knowledgeUrls = collectKnowledgeUrls(knowledgeSource, canonical);
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");
const sitemap = `