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"); const html = await readFile(sourcePath, "utf8"); const marker = { hero: '
', features: '
', cViz: '
', pricingIntro: '
', waitlistFiles: '
', demoVideoFolder: '
', 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("
", footerStart) + "".length; const 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 field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text") { return { path, label, kind, renderAs }; } 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: от Designer до кастомного кода.', cards: [ { muted: "Полный", main: "агентный контроль", bodyHtml: 'Интеграция с Webflow MCP, мультиагентный режим позволяет работать с несколькими экземплярами параллельно.', }, { muted: "Компонентная", main: "библиотека", bodyHtml: 'Собирайте с учетом ИИ-агентов разные компонентные заготовки дают основу, а стиль и сложную функциональность можно дорабатывать промптами.', }, { muted: "Деплой ", main: "пайплайн", bodyHtml: 'Деплой в один клик, непрерывный пайплайн, мгновенный откат и быстрая доставка через CDN.', }, ], app: { iconAlt: "Иконка NODE.DC", logoAlt: "Логотип NODE.DC", release: "Бета-релиз: июль 2026", heading: "Ранний доступ", description: "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре.", }, form: { messagePlaceholder: "Сообщение (необязательно)", namePlaceholder: "Ваше имя*", emailPlaceholder: "Email для регистрации*", submitLabel: "Встать в лист ожидания", successHeading: "Заявка принята. Спасибо.", successTextHtml: "Мы скоро свяжемся.
", betaCtaLabel: "Подать заявку в бету?", founderIntroHtml: "Или оформить ранний Founder-доступ
", founderCtaHtml: 'Оформить Founder-доступ 350€', spotsLabel: "До 200 мест", failureHeading: "Не получилось", failureTextHtml: "Что-то пошло не так.
Попробуйте ещё раз чуть позже.
", }, legalHtml: 'Политика конфиденциальностиПолитика cookie
© — Все права защищены
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: 'Универсальный инструмент для ИИ-агентов в вашем Webflow-проекте.', features: [ { eyebrow: "Контекст проекта", bodyHtml: 'Подключитесь к MCP чтобы агенты получили полный контекст вашей сборки.', }, { eyebrow: "Группы агентов", bodyHtml: 'Используйте несколько агентов параллельно чтобы добавлять кастомный код, анимации и функциональность.', }, { eyebrow: "Среда разработки", bodyHtml: 'Используйте быстрые Bun сборки и автообновление для вашего кастомного кода.', }, { eyebrow: "Деплой в один клик", bodyHtml: 'Нажмите и публикуйте кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта.', }, ], }, 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 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", 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}`);