Import expanded Knowledge SEO content

This commit is contained in:
dcconstructions 2026-07-10 01:51:58 +03:00
parent e08ad18253
commit 2c07dc2df8
27 changed files with 3114 additions and 311 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
<url>
<loc>https://nodedc.dctouch.ru/knowledge/</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/</loc>
@ -18,23 +18,43 @@
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/hub/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/engine/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/ops/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/integration-layer/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/platform/artifact-event-layer/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/agent-context/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/agent-context/document-agent/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/agent-context/agent-catalog/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/</loc>
<changefreq>weekly</changefreq>
@ -43,41 +63,61 @@
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/tender-agent/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/1c-integration/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/drone-management/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/city-digital-twins/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/industrial-contour/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/enterprise-management/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/construction-contour/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/seo-module/</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/procurement-contour/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/project-office/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/video-analytics/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nodedc.dctouch.ru/knowledge/modules/engineering-contour/</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

View File

@ -0,0 +1,262 @@
import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const knowledgePath = resolve(repoRoot, "content", "knowledge.json");
const args = process.argv.slice(2);
const inputPath = args.find((value) => !value.startsWith("--"));
const dryRun = args.includes("--dry-run");
if (!inputPath) {
throw new Error("Укажите путь к NODEDC_Knowledge_SEO_replacement_pack.md");
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function cleanMetaValue(value) {
const trimmed = String(value || "").trim();
return trimmed.startsWith("`") && trimmed.endsWith("`") ? trimmed.slice(1, -1).trim() : trimmed;
}
function metadataValue(section, label) {
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = section.match(new RegExp(`^\\*\\*${escapedLabel}:\\*\\*\\s*(.+?)\\s*$`, "m"));
return cleanMetaValue(match?.[1] || "");
}
function inlineMarkdown(value) {
return escapeHtml(String(value || "").trim())
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2">$1</a>');
}
function buttonShortcode(label, href) {
const name = String(label || "Открыть").replaceAll('"', "&quot;");
const link = String(href || "/").replaceAll('"', "&quot;");
return `<p>[button name="${name}" link="${link}"]</p>`;
}
function markdownToHtml(markdown) {
const lines = String(markdown || "").replaceAll("\r\n", "\n").split("\n");
const blocks = [];
let index = 0;
while (index < lines.length) {
const line = lines[index].trim();
if (!line || line === "---") {
index += 1;
continue;
}
if (/^#\s+/.test(line)) {
index += 1;
continue;
}
const heading = /^(#{2,3})\s+(.+)$/.exec(line);
if (heading) {
const level = heading[1].length;
blocks.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
index += 1;
continue;
}
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(line);
if (link) {
blocks.push(buttonShortcode(link[1], link[2]));
index += 1;
continue;
}
const bullet = /^[-*]\s+(.+)$/.exec(line);
if (bullet) {
const items = [];
while (index < lines.length) {
const item = /^[-*]\s+(.+)$/.exec(lines[index].trim());
if (!item) break;
items.push(`<li>${inlineMarkdown(item[1])}</li>`);
index += 1;
}
blocks.push(`<ul>${items.join("")}</ul>`);
continue;
}
const ordered = /^\d+\.\s+(.+)$/.exec(line);
if (ordered) {
const items = [];
while (index < lines.length) {
const item = /^\d+\.\s+(.+)$/.exec(lines[index].trim());
if (!item) break;
items.push(`<li>${inlineMarkdown(item[1])}</li>`);
index += 1;
}
blocks.push(`<ol>${items.join("")}</ol>`);
continue;
}
const paragraph = [];
while (index < lines.length) {
const next = lines[index].trim();
if (!next || next === "---" || /^#{1,3}\s+/.test(next) || /^[-*]\s+/.test(next) || /^\d+\.\s+/.test(next) || /^\[[^\]]+\]\([^)\s]+\)$/.test(next)) {
break;
}
paragraph.push(next);
index += 1;
}
if (paragraph.length) {
blocks.push(`<p>${inlineMarkdown(paragraph.join(" "))}</p>`);
} else {
index += 1;
}
}
return `${blocks.join("\n")}\n`;
}
function parsePack(markdown) {
const matches = [...markdown.matchAll(/^## (\d+)\. (.+)$/gm)];
const pages = new Map();
for (let index = 0; index < matches.length; index += 1) {
const match = matches[index];
const number = Number(match[1]);
if (!Number.isInteger(number) || number < 1 || number > 23) continue;
const section = markdown.slice(match.index + match[0].length, matches[index + 1]?.index || markdown.length);
const h1 = /^# (.+)$/m.exec(section);
if (!h1) throw new Error(`В SEO-паке не найден H1 для раздела ${number}`);
const bodyStart = (h1.index || 0) + h1[0].length;
const page = {
number,
title: h1[1].trim(),
id: metadataValue(section, "ID"),
slug: metadataValue(section, "Slug"),
seoTitle: metadataValue(section, "SEO title"),
seoDescription: metadataValue(section, "SEO description"),
bodyHtml: markdownToHtml(section.slice(bodyStart)),
};
if (number > 1 && (!page.id || !page.slug || !page.seoTitle || !page.seoDescription)) {
throw new Error(`В SEO-паке неполные метаданные для «${page.title}»`);
}
pages.set(number, page);
}
if (pages.size !== 23 || !pages.has(1)) {
throw new Error(`Ожидалось 23 страницы SEO-пака, найдено ${pages.size}`);
}
return pages;
}
function walk(nodes, callback) {
for (const node of nodes || []) {
callback(node);
walk(node.children, callback);
}
}
function leadingMediaParagraph(html) {
return String(html || "").match(/^\s*(<p>\s*<(?:img|video)\b[\s\S]*?<\/p>)/i)?.[1] || "";
}
function materialFromPage(page, existing) {
const media = leadingMediaParagraph(existing?.bodyHtml);
return {
...(existing || {}),
id: page.id,
title: page.title,
slug: page.slug,
enabled: true,
useContent: true,
seo: {
title: page.seoTitle,
description: page.seoDescription,
},
bodyHtml: `${media ? `${media}\n` : ""}${page.bodyHtml}`,
children: [],
};
}
const treeOrder = [
{
id: "platform-overview",
children: ["hub", "engine", "ops", "integration-layer", "artifact-event-layer"],
},
{
id: "agent-context",
children: ["document-agent", "agent-catalog"],
},
{
id: "applied-modules",
children: [
"tender-agent",
"1c-integration",
"drone-management",
"city-digital-twins",
"industrial-contour",
"enterprise-management",
"construction-contour",
"seo-module",
"procurement-contour",
"project-office",
"video-analytics",
"engineering-contour",
],
},
];
const [markdown, rawKnowledge] = await Promise.all([
readFile(resolve(inputPath), "utf8"),
readFile(knowledgePath, "utf8"),
]);
const pages = parsePack(markdown);
const knowledge = JSON.parse(rawKnowledge);
const existingNodes = new Map();
walk(knowledge.tree, (node) => existingNodes.set(node.id, node));
const rootPage = pages.get(1);
const pageById = new Map([...pages.values()].filter((page) => page.number > 1).map((page) => [page.id, page]));
const materials = new Map([...pageById].map(([id, page]) => [id, materialFromPage(page, existingNodes.get(id))]));
for (const { id, children } of treeOrder) {
const node = materials.get(id);
if (!node) throw new Error(`Не найден раздел «${id}» в SEO-паке`);
node.children = children.map((childId) => {
const child = materials.get(childId);
if (!child) throw new Error(`Не найден дочерний раздел «${childId}» в SEO-паке`);
return child;
});
}
knowledge.title = rootPage.title;
knowledge.seo = {
...(knowledge.seo || {}),
title: rootPage.seoTitle,
description: rootPage.seoDescription,
};
knowledge.settings ||= {};
knowledge.settings.description = rootPage.seoDescription;
knowledge.introHtml = rootPage.bodyHtml;
knowledge.tree = treeOrder.map(({ id }) => materials.get(id));
const summary = {
pages: pages.size,
updated: [...pageById.keys()].filter((id) => existingNodes.has(id)).length,
added: [...pageById.keys()].filter((id) => !existingNodes.has(id)).length,
tree: knowledge.tree.map((node) => ({ id: node.id, children: node.children.map((child) => child.id) })),
};
if (!dryRun) {
await writeFile(knowledgePath, `${JSON.stringify(knowledge, null, 2)}\n`);
}
console.log(`${dryRun ? "Checked" : "Imported"} Knowledge SEO pack: ${JSON.stringify(summary)}`);

View File

@ -548,7 +548,7 @@ function renderHeader(searchPlaceholder) {
}
function pageTitle(record = null) {
if (!record) return knowledge.title || "База знаний";
if (!record) return knowledge.seo?.title || knowledge.title || "База знаний";
return record.node.seo?.title || record.node.title || "Материал";
}
@ -558,7 +558,7 @@ function visibleTitle(record = null) {
}
function pageDescription(record = null) {
if (!record) return knowledge.settings?.description || "";
if (!record) return knowledge.seo?.description || knowledge.settings?.description || "";
return record.node.seo?.description || stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml)).slice(0, 180);
}
@ -723,8 +723,8 @@ async function writePage(urlPath, html) {
async function writeSeoFiles(records) {
const sitemapUrls = [
{ loc: canonicalHome(), priority: "1.0" },
{ loc: canonicalForPath(`/${knowledgeBasePath()}/`), priority: "0.8" },
...records.map((record) => ({ loc: canonicalForPath(record.urlPath), priority: record.parents.length ? "0.6" : "0.7" })),
{ loc: canonicalForPath(`/${knowledgeBasePath()}/`), priority: "0.7" },
...records.map((record) => ({ loc: canonicalForPath(record.urlPath), priority: "0.7" })),
];
const sitemapUrl = new URL("sitemap.xml", canonicalHome()).href;
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");