541 lines
24 KiB
JavaScript
541 lines
24 KiB
JavaScript
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 sourcePath = join(repoRoot, "index.html");
|
||
const templateRoot = join(repoRoot, "templates", "pages", "home");
|
||
const blockRoot = join(templateRoot, "blocks");
|
||
const contentPath = join(repoRoot, "content", "pages", "home.json");
|
||
|
||
let html = await readFile(sourcePath, "utf8");
|
||
html = html.replace(/\n*<script id="node-dc-static-config"(?: type="application\/json")?>[\s\S]*?<\/script>\n*/g, "\n");
|
||
|
||
const marker = {
|
||
hero: '<header id="hero" class="s h-screen">',
|
||
features: '<section id="features" class="s h-screen py mobile-flip">',
|
||
cViz: '<div class="c-viz">',
|
||
pricingIntro: '<section id="waitlist" class="s">',
|
||
waitlistFiles: '<section id="waitlist" class="s h-screen">',
|
||
demoVideoFolder: '<div data-module="folder-wrap" class="folder-w video-w">',
|
||
footer: '<footer class="s footer">',
|
||
};
|
||
|
||
function findRequired(needle, from = 0) {
|
||
const index = html.indexOf(needle, from);
|
||
if (index === -1) {
|
||
throw new Error(`Could not find marker: ${needle}`);
|
||
}
|
||
return index;
|
||
}
|
||
|
||
const heroStart = findRequired(marker.hero);
|
||
const featuresStart = findRequired(marker.features, heroStart);
|
||
const cVizStart = findRequired(marker.cViz, featuresStart);
|
||
const cVizOpenEnd = cVizStart + marker.cViz.length;
|
||
const pricingIntroStart = findRequired(marker.pricingIntro, cVizOpenEnd);
|
||
const waitlistFilesStart = findRequired(marker.waitlistFiles, pricingIntroStart);
|
||
const demoVideoFolderStart = findRequired(marker.demoVideoFolder, waitlistFilesStart);
|
||
const footerStart = findRequired(marker.footer, demoVideoFolderStart);
|
||
const footerEnd = findRequired("</footer>", footerStart) + "</footer>".length;
|
||
|
||
let shell = [
|
||
html.slice(0, heroStart),
|
||
"{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
|
||
html.slice(cVizStart, cVizOpenEnd),
|
||
"{{NODEDC_BLOCKS_CVIZ}}",
|
||
html.slice(footerEnd),
|
||
].join("");
|
||
|
||
const blocks = [
|
||
{
|
||
region: "beforeCViz",
|
||
id: "hero-waitlist",
|
||
type: "staticHtml",
|
||
template: "hero-waitlist.html",
|
||
adminLabel: "Первый экран и заявка",
|
||
anchor: "hero",
|
||
html: html.slice(heroStart, featuresStart),
|
||
},
|
||
{
|
||
region: "beforeCViz",
|
||
id: "feature-video",
|
||
type: "staticHtml",
|
||
template: "feature-video.html",
|
||
adminLabel: "Демо и базовая логика",
|
||
anchor: "features",
|
||
html: html.slice(featuresStart, cVizStart),
|
||
},
|
||
{
|
||
region: "cViz",
|
||
id: "product-system",
|
||
type: "staticHtml",
|
||
template: "product-system.html",
|
||
adminLabel: "Компоненты, режимы, FAQ и тарифная таблица",
|
||
anchor: null,
|
||
html: html.slice(cVizOpenEnd, pricingIntroStart),
|
||
},
|
||
{
|
||
region: "cViz",
|
||
id: "pricing-intro",
|
||
type: "staticHtml",
|
||
template: "pricing-intro.html",
|
||
adminLabel: "Интро тарифов",
|
||
anchor: "waitlist",
|
||
html: html.slice(pricingIntroStart, waitlistFilesStart),
|
||
},
|
||
{
|
||
region: "cViz",
|
||
id: "waitlist-files",
|
||
type: "staticHtml",
|
||
template: "waitlist-files.html",
|
||
adminLabel: "Форма ожидания и txt-файлы",
|
||
anchor: "waitlist",
|
||
html: html.slice(waitlistFilesStart, demoVideoFolderStart),
|
||
},
|
||
{
|
||
region: "cViz",
|
||
id: "demo-video-folder",
|
||
type: "staticHtml",
|
||
template: "demo-video-folder.html",
|
||
adminLabel: "Иконка демо-видео в доке",
|
||
anchor: null,
|
||
html: html.slice(demoVideoFolderStart, footerStart),
|
||
},
|
||
{
|
||
region: "cViz",
|
||
id: "footer",
|
||
type: "staticHtml",
|
||
template: "footer.html",
|
||
adminLabel: "Футер",
|
||
anchor: null,
|
||
html: html.slice(footerStart, footerEnd),
|
||
},
|
||
];
|
||
|
||
function getByPath(source, path) {
|
||
return path.split(".").reduce((value, key) => value?.[key], source);
|
||
}
|
||
|
||
function inferFieldGroup(path) {
|
||
if (path.startsWith("content.navigation.")) return "Шапка";
|
||
if (path.startsWith("content.notifications.")) return "Уведомления";
|
||
if (path.startsWith("content.dock.")) return "Dock";
|
||
if (path.startsWith("content.assets.")) return "Модель и логотипы";
|
||
if (path.startsWith("content.cards.")) return "Тезисы первого экрана";
|
||
if (path.startsWith("content.app.")) return "Карточка раннего доступа";
|
||
if (path.startsWith("content.form.")) return "Форма";
|
||
if (path === "content.legalHtml") return "Юридический блок";
|
||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||
if (path.startsWith("content.features.")) return "Пункты";
|
||
if (path.startsWith("content.heading.") || path === "content.introHtml") return "Заголовок";
|
||
return "Контент";
|
||
}
|
||
|
||
function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null) {
|
||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path) };
|
||
}
|
||
|
||
function tokenizeBlock(block, definition) {
|
||
let html = block.html;
|
||
|
||
for (const editableField of definition.editableFields) {
|
||
const value = getByPath({ content: definition.content }, editableField.path);
|
||
const token = `{{${editableField.renderAs}:${editableField.path}}}`;
|
||
|
||
if (typeof value !== "string") {
|
||
throw new Error(`Token field must be a string: ${block.id}.${editableField.path}`);
|
||
}
|
||
|
||
if (!html.includes(value)) {
|
||
throw new Error(`Could not find "${value}" while tokenizing ${block.id}`);
|
||
}
|
||
|
||
html = html.replace(value, token);
|
||
}
|
||
|
||
return {
|
||
...block,
|
||
type: definition.type,
|
||
html,
|
||
content: definition.content,
|
||
editableFields: definition.editableFields,
|
||
};
|
||
}
|
||
|
||
const typedBlockDefinitions = {
|
||
"hero-waitlist": {
|
||
type: "heroWaitlist",
|
||
content: {
|
||
heading: {
|
||
muted: "Ваш Webflow ",
|
||
main: "ИИ-инструменты",
|
||
},
|
||
introHtml:
|
||
'Несколько агентов для проекта Webflow: от <span class="darker-70">Designer</span> до <span class="darker-70">кастомного кода.</span>',
|
||
cards: [
|
||
{
|
||
muted: "Полный",
|
||
main: "агентный контроль",
|
||
bodyHtml:
|
||
'Интеграция с <span class="darker-70">Webflow MCP,</span> мультиагентный режим позволяет работать с <span class="darker-70">несколькими экземплярами</span> параллельно.',
|
||
},
|
||
{
|
||
muted: "Компонентная",
|
||
main: "библиотека",
|
||
bodyHtml:
|
||
'Собирайте с учетом <span class="darker-70">ИИ-агентов</span> разные компонентные заготовки дают основу, а стиль и сложную функциональность можно дорабатывать промптами.',
|
||
},
|
||
{
|
||
muted: "Деплой ",
|
||
main: "пайплайн",
|
||
bodyHtml:
|
||
'<span class="darker-70">Деплой в один клик,</span> непрерывный пайплайн, мгновенный откат и быстрая <span class="darker-70">доставка через CDN.</span>',
|
||
},
|
||
],
|
||
app: {
|
||
iconAlt: "Иконка NODE.DC",
|
||
logoAlt: "Логотип NODE.DC",
|
||
release: "Бета-релиз: июль 2026",
|
||
heading: "Ранний доступ",
|
||
description: "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре.",
|
||
},
|
||
form: {
|
||
messagePlaceholder: "Сообщение (необязательно)",
|
||
namePlaceholder: "Ваше имя*",
|
||
emailPlaceholder: "Email для регистрации*",
|
||
submitLabel: "Встать в лист ожидания",
|
||
successHeading: "Заявка принята. Спасибо.",
|
||
successTextHtml: "Мы скоро свяжемся. <br/>",
|
||
betaCtaLabel: "Подать заявку в бету?",
|
||
founderIntroHtml: "Или оформить ранний Founder-доступ<br/>",
|
||
founderCtaHtml: 'Оформить Founder-доступ <span class="darker-70">350€</span>',
|
||
spotsLabel: "До 200 мест",
|
||
failureHeading: "Не получилось",
|
||
failureTextHtml: "Что-то пошло не так. <br/>Попробуйте ещё раз чуть позже.<br/>",
|
||
},
|
||
legalHtml:
|
||
'<a href="https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing" target="_blank">Политика конфиденциальности</a> • <a href="https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing" target="_blank">Политика cookie</a><br/>© — Все права защищены<br/>2026',
|
||
},
|
||
editableFields: [
|
||
field("content.heading.muted", "Hero: приглушённая строка"),
|
||
field("content.heading.main", "Hero: основная строка"),
|
||
field("content.introHtml", "Hero: вводный текст", "html"),
|
||
field("content.cards.0.muted", "Карточка 1: приглушённая строка"),
|
||
field("content.cards.0.main", "Карточка 1: основная строка"),
|
||
field("content.cards.0.bodyHtml", "Карточка 1: текст", "html"),
|
||
field("content.cards.1.muted", "Карточка 2: приглушённая строка"),
|
||
field("content.cards.1.main", "Карточка 2: основная строка"),
|
||
field("content.cards.1.bodyHtml", "Карточка 2: текст", "html"),
|
||
field("content.cards.2.muted", "Карточка 3: приглушённая строка"),
|
||
field("content.cards.2.main", "Карточка 3: основная строка"),
|
||
field("content.cards.2.bodyHtml", "Карточка 3: текст", "html"),
|
||
field("content.app.iconAlt", "App: alt иконки", "text", "attr"),
|
||
field("content.app.logoAlt", "App: alt логотипа", "text", "attr"),
|
||
field("content.app.release", "App: релиз"),
|
||
field("content.app.heading", "App: заголовок"),
|
||
field("content.app.description", "App: описание"),
|
||
field("content.form.messagePlaceholder", "Форма: placeholder сообщения", "text", "attr"),
|
||
field("content.form.namePlaceholder", "Форма: placeholder имени", "text", "attr"),
|
||
field("content.form.emailPlaceholder", "Форма: placeholder email", "text", "attr"),
|
||
field("content.form.submitLabel", "Форма: кнопка"),
|
||
field("content.form.successHeading", "Форма: успех, заголовок"),
|
||
field("content.form.successTextHtml", "Форма: успех, текст", "html"),
|
||
field("content.form.betaCtaLabel", "Форма: CTA беты"),
|
||
field("content.form.founderIntroHtml", "Форма: Founder intro", "html"),
|
||
field("content.form.founderCtaHtml", "Форма: Founder CTA", "html"),
|
||
field("content.form.spotsLabel", "Форма: лимит мест"),
|
||
field("content.form.failureHeading", "Форма: ошибка, заголовок"),
|
||
field("content.form.failureTextHtml", "Форма: ошибка, текст", "html"),
|
||
field("content.legalHtml", "Юридический текст", "html"),
|
||
],
|
||
},
|
||
"feature-video": {
|
||
type: "featureVideo",
|
||
content: {
|
||
windowTitle: "демо-видео.mp4",
|
||
videoSrc: "./assets/media/publish-flow.mp4",
|
||
heading: {
|
||
muted: "Всё в одном",
|
||
main: "приложение.",
|
||
},
|
||
introHtml:
|
||
'Универсальный инструмент для <span class="darker-70">ИИ-агентов</span> в вашем Webflow-проекте.',
|
||
features: [
|
||
{
|
||
eyebrow: "Контекст проекта",
|
||
bodyHtml:
|
||
'Подключитесь к <span class="darker-70">MCP</span> чтобы агенты получили полный контекст вашей сборки.',
|
||
},
|
||
{
|
||
eyebrow: "Группы агентов",
|
||
bodyHtml:
|
||
'Используйте <span class="darker-70">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность.',
|
||
},
|
||
{
|
||
eyebrow: "Среда разработки",
|
||
bodyHtml:
|
||
'Используйте быстрые <span class="darker-70">Bun</span> сборки и автообновление для вашего <span class="darker-70">кастомного кода.</span>',
|
||
},
|
||
{
|
||
eyebrow: "Деплой в один клик",
|
||
bodyHtml:
|
||
'<span class="darker-70">Нажмите и публикуйте</span> кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта.',
|
||
},
|
||
],
|
||
},
|
||
editableFields: [
|
||
field("content.windowTitle", "Окно: название файла"),
|
||
field("content.videoSrc", "Окно: видео", "media", "attr"),
|
||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||
field("content.heading.main", "Заголовок: основная строка"),
|
||
field("content.introHtml", "Вводный текст", "html"),
|
||
field("content.features.0.eyebrow", "Пункт 1: подпись"),
|
||
field("content.features.0.bodyHtml", "Пункт 1: текст", "html"),
|
||
field("content.features.1.eyebrow", "Пункт 2: подпись"),
|
||
field("content.features.1.bodyHtml", "Пункт 2: текст", "html"),
|
||
field("content.features.2.eyebrow", "Пункт 3: подпись"),
|
||
field("content.features.2.bodyHtml", "Пункт 3: текст", "html"),
|
||
field("content.features.3.eyebrow", "Пункт 4: подпись"),
|
||
field("content.features.3.bodyHtml", "Пункт 4: текст", "html"),
|
||
],
|
||
},
|
||
};
|
||
|
||
const staticElements = {
|
||
id: "static-elements",
|
||
type: "staticElements",
|
||
adminLabel: "Статичные элементы",
|
||
enabled: true,
|
||
content: {
|
||
navigation: {
|
||
logoAlt: "главная",
|
||
brandLabel: "NODE.DC",
|
||
links: [
|
||
{
|
||
label: "О продукте",
|
||
href: "index.html#features",
|
||
target: "",
|
||
},
|
||
{
|
||
label: "Лист ожидания",
|
||
href: "index.html#hero",
|
||
target: "",
|
||
},
|
||
{
|
||
label: "Документация",
|
||
href: "https://docs.ssscript.app/",
|
||
target: "_blank",
|
||
},
|
||
{
|
||
label: "Тарифы",
|
||
href: "index.html#pricing-wrap",
|
||
target: "",
|
||
},
|
||
],
|
||
},
|
||
notifications: [
|
||
{
|
||
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> — ИЮЛЬ 2026',
|
||
title: "Открытая бета",
|
||
body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
|
||
},
|
||
{
|
||
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
|
||
title: "Альфа для Founder-доступа",
|
||
body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
|
||
},
|
||
{
|
||
eyebrowHtml: "СЕЙЧАС",
|
||
title: "Инвайты в закрытую бету уже отправлены.",
|
||
body: "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
|
||
},
|
||
],
|
||
dock: {
|
||
appTarget: "app-data",
|
||
trashAlt: "Белая иконка корзины.",
|
||
demoFileLabel: "демо-видео.txt",
|
||
},
|
||
assets: {
|
||
webglModelSrc: "./assets/custom/icon.glb",
|
||
logoPng: "./assets/custom/logo.png",
|
||
logoPngAlt: "Логотип NODE.DC",
|
||
appIconSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
|
||
appIconAlt: "Иконка NODE.DC",
|
||
appWordmarkSrc: "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
|
||
appWordmarkAlt: "Логотип NODE.DC",
|
||
},
|
||
},
|
||
editableFields: [
|
||
field("content.navigation.logoAlt", "Логотип в шапке: alt"),
|
||
field("content.navigation.brandLabel", "Бренд в шапке"),
|
||
field("content.navigation.links.0.label", "Ссылка 1: текст"),
|
||
field("content.navigation.links.0.href", "Ссылка 1: адрес", "url", "attr"),
|
||
field("content.navigation.links.1.label", "Ссылка 2: текст"),
|
||
field("content.navigation.links.1.href", "Ссылка 2: адрес", "url", "attr"),
|
||
field("content.navigation.links.2.label", "Ссылка 3: текст"),
|
||
field("content.navigation.links.2.href", "Ссылка 3: адрес", "url", "attr"),
|
||
field("content.navigation.links.2.target", "Ссылка 3: target", "text", "attr"),
|
||
field("content.navigation.links.3.label", "Ссылка 4: текст"),
|
||
field("content.navigation.links.3.href", "Ссылка 4: адрес", "url", "attr"),
|
||
field("content.notifications.0.eyebrowHtml", "Уведомление 1: дата/статус", "html"),
|
||
field("content.notifications.0.title", "Уведомление 1: заголовок"),
|
||
field("content.notifications.0.body", "Уведомление 1: текст", "textarea"),
|
||
field("content.notifications.1.eyebrowHtml", "Уведомление 2: дата/статус", "html"),
|
||
field("content.notifications.1.title", "Уведомление 2: заголовок"),
|
||
field("content.notifications.1.body", "Уведомление 2: текст", "textarea"),
|
||
field("content.notifications.2.eyebrowHtml", "Уведомление 3: дата/статус", "html"),
|
||
field("content.notifications.2.title", "Уведомление 3: заголовок"),
|
||
field("content.notifications.2.body", "Уведомление 3: текст", "textarea"),
|
||
field("content.dock.appTarget", "Dock: ID приложения"),
|
||
field("content.dock.trashAlt", "Dock: alt корзины"),
|
||
field("content.dock.demoFileLabel", "Dock: подпись демо-файла"),
|
||
field("content.assets.webglModelSrc", "WebGL-модель логотипа", "media", "attr"),
|
||
field("content.assets.logoPng", "PNG-логотип", "media", "attr"),
|
||
field("content.assets.logoPngAlt", "PNG-логотип: alt"),
|
||
field("content.assets.appIconSrc", "Иконка приложения", "media", "attr"),
|
||
field("content.assets.appIconAlt", "Иконка приложения: alt"),
|
||
field("content.assets.appWordmarkSrc", "Wordmark приложения", "media", "attr"),
|
||
field("content.assets.appWordmarkAlt", "Wordmark приложения: alt"),
|
||
],
|
||
};
|
||
|
||
function shellToken(mode, path) {
|
||
return `{{${mode}:staticElements.${path}}}`;
|
||
}
|
||
|
||
function replaceShellLiteral(source, literal, replacement, label) {
|
||
if (!source.includes(literal)) {
|
||
throw new Error(`Could not find shell fragment while tokenizing static elements: ${label}`);
|
||
}
|
||
|
||
return source.replace(literal, replacement);
|
||
}
|
||
|
||
function tokenizeStaticShell(source, definition) {
|
||
const { navigation, notifications, dock } = definition.content;
|
||
|
||
let tokenized = source;
|
||
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`aria-label="${navigation.logoAlt}" aria-current`,
|
||
`aria-label="${shellToken("attr", "content.navigation.logoAlt")}" aria-current`,
|
||
"navigation.logoAlt",
|
||
);
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`class="bar-txt medium w--current">${navigation.brandLabel}</a>`,
|
||
`class="bar-txt medium w--current">${shellToken("text", "content.navigation.brandLabel")}</a>`,
|
||
"navigation.brandLabel",
|
||
);
|
||
|
||
for (const [index, link] of navigation.links.entries()) {
|
||
const targetPart = link.target ? ` target="${link.target}"` : "";
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`<a href="${link.href}"${targetPart} class="bar-txt">${link.label}</a>`,
|
||
`<a href="${shellToken("attr", `content.navigation.links.${index}.href`)}"${
|
||
link.target ? ` target="${shellToken("attr", `content.navigation.links.${index}.target`)}"` : ""
|
||
} class="bar-txt">${shellToken("text", `content.navigation.links.${index}.label`)}</a>`,
|
||
`navigation.links.${index}`,
|
||
);
|
||
}
|
||
|
||
notifications.forEach((notification, index) => {
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
notification.eyebrowHtml,
|
||
shellToken("html", `content.notifications.${index}.eyebrowHtml`),
|
||
`notifications.${index}.eyebrowHtml`,
|
||
);
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`<div>${notification.title}</div>`,
|
||
`<div>${shellToken("text", `content.notifications.${index}.title`)}</div>`,
|
||
`notifications.${index}.title`,
|
||
);
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`<div>${notification.body}</div>`,
|
||
`<div>${shellToken("text", `content.notifications.${index}.body`)}</div>`,
|
||
`notifications.${index}.body`,
|
||
);
|
||
});
|
||
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`data-target="${dock.appTarget}" class="dock-icon ssscript"`,
|
||
`data-target="${shellToken("attr", "content.dock.appTarget")}" class="dock-icon ssscript"`,
|
||
"dock.appTarget",
|
||
);
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
`alt="${dock.trashAlt}" class="dock-app-img trash"`,
|
||
`alt="${shellToken("attr", "content.dock.trashAlt")}" class="dock-app-img trash"`,
|
||
"dock.trashAlt",
|
||
);
|
||
|
||
tokenized = replaceShellLiteral(
|
||
tokenized,
|
||
"<script>\nwindow.__API_ORIGIN__=",
|
||
`<script id="node-dc-static-config" type="application/json">{"assets":{"icon":"${shellToken(
|
||
"js",
|
||
"content.assets.webglModelSrc",
|
||
)}","logo":"${shellToken("js", "content.assets.logoPng")}"}}</script>\n<script>\nwindow.__API_ORIGIN__=`,
|
||
"static asset config",
|
||
);
|
||
|
||
return tokenized;
|
||
}
|
||
|
||
shell = tokenizeStaticShell(shell, staticElements);
|
||
|
||
const preparedBlocks = blocks.map((block) => {
|
||
const definition = typedBlockDefinitions[block.id];
|
||
return definition ? tokenizeBlock(block, definition) : block;
|
||
});
|
||
|
||
const page = {
|
||
schemaVersion: 1,
|
||
slug: "home",
|
||
output: "index.html",
|
||
title: "NODE.DC",
|
||
staticElements,
|
||
seo: {
|
||
title: "NODE.DC — ИИ-платформа для Webflow",
|
||
description:
|
||
"Единая платформа для кастомного кода, MCP и ИИ-агентов в Webflow: контекст проекта, автоматизация, деплой и управляемая разработка.",
|
||
},
|
||
regions: [
|
||
{
|
||
id: "beforeCViz",
|
||
label: "До c-viz wrapper",
|
||
description: "Hero and first product section. These blocks live before the visual wrapper.",
|
||
blocks: preparedBlocks
|
||
.filter((block) => block.region === "beforeCViz")
|
||
.map(({ html: _html, ...block }) => ({ ...block, enabled: true })),
|
||
},
|
||
{
|
||
id: "cViz",
|
||
label: "Inside c-viz wrapper",
|
||
description: "Sections coupled to WebGL/color-flip shell. Keep these in this region for now.",
|
||
blocks: preparedBlocks
|
||
.filter((block) => block.region === "cViz")
|
||
.map(({ html: _html, ...block }) => ({ ...block, enabled: true })),
|
||
},
|
||
],
|
||
};
|
||
|
||
await mkdir(blockRoot, { recursive: true });
|
||
await mkdir(dirname(contentPath), { recursive: true });
|
||
await writeFile(join(templateRoot, "shell.html"), shell);
|
||
for (const block of preparedBlocks) {
|
||
await writeFile(join(blockRoot, block.template), block.html);
|
||
}
|
||
await writeFile(contentPath, `${JSON.stringify(page, null, 2)}\n`);
|
||
|
||
console.log(`Extracted ${blocks.length} blocks from ${sourcePath}`);
|
||
console.log(`Wrote ${join(templateRoot, "shell.html")}`);
|
||
console.log(`Wrote ${contentPath}`);
|