diff --git a/README.md b/README.md index b74e05b..71c0d4c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,37 @@ python3 -m http.server 8081 --bind 127.0.0.1 Open http://127.0.0.1:8081/ +## Admin And Render + +The first refactor step keeps the Webflow markup intact, but moves the home page into a block-rendered structure: + +- `content/pages/home.json` - ordered page regions and block instances. +- `templates/pages/home/shell.html` - original page shell with block slots. +- `templates/pages/home/blocks/` - exact HTML chunks extracted from the current page. +- `tools/render-home.mjs` - renders `index.html` from the content model and templates. +- `admin/` - local admin UI for block order, enable/disable, duplicate, copy/paste, and JSON edits. + +Run the local admin: + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_SITE +npm run admin +``` + +Open http://127.0.0.1:8090/admin/ + +Render the public page after JSON changes: + +```bash +npm run render:home +``` + +If the current `index.html` must be re-extracted into fresh templates: + +```bash +npm run extract:home +``` + ## Structure - `index.html`, `beta-apply.html`, `founders.html`, `enterprise.html` - editable local pages. @@ -22,6 +53,7 @@ Open http://127.0.0.1:8081/ - `assets/vendor` - third-party browser scripts. - `docs/` - refactor notes, block model draft, and admin plan. - `originals` - untouched files copied from the original wget download. +- `backbase/ssscript.app` - original downloaded app bundle backup. - `asset-manifest.json` - source URL to local file mapping. - `tools/build-local-copy.mjs` - rebuild script. It uses `NODEDC_SOURCE_DIR` when provided, otherwise the original local rescue folder. - `tools/localize-ru.mjs` - Russian localization and NODE.DC brand patch script. diff --git a/admin/admin.css b/admin/admin.css new file mode 100644 index 0000000..80d45e3 --- /dev/null +++ b/admin/admin.css @@ -0,0 +1,268 @@ +:root { + color-scheme: dark; + --bg: #08090b; + --panel: #121417; + --panel-2: #181b20; + --line: #2a2f36; + --text: #f4f5f6; + --muted: #9097a1; + --accent: #1976ff; + --danger: #cf3b3b; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +input, +textarea { + font: inherit; +} + +.admin-shell { + display: grid; + grid-template-columns: 360px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + border-right: 1px solid var(--line); + background: #0d0f12; + padding: 20px; + overflow: auto; +} + +.sidebar-head, +.editor-head, +.actions, +.button-row, +.toggle-row { + display: flex; + align-items: center; +} + +.sidebar-head, +.editor-head { + justify-content: space-between; + gap: 16px; +} + +.eyebrow { + color: var(--muted); + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +h1, +h2 { + margin: 4px 0 0; + font-size: 24px; + line-height: 1.05; + letter-spacing: 0; +} + +.regions { + display: grid; + gap: 18px; + margin-top: 24px; +} + +.region-title { + margin: 0 0 8px; + color: var(--muted); + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.block-list { + display: grid; + gap: 8px; +} + +.block-btn { + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--text); + cursor: pointer; + padding: 11px 12px; + text-align: left; +} + +.block-btn:hover, +.block-btn.active { + border-color: var(--accent); +} + +.block-btn.disabled { + color: #5f6670; +} + +.block-meta { + display: block; + margin-top: 4px; + color: var(--muted); + font-size: 12px; +} + +.editor { + min-width: 0; + padding: 24px; +} + +.editor-head { + border-bottom: 1px solid var(--line); + padding-bottom: 20px; +} + +.actions, +.button-row { + gap: 8px; + flex-wrap: wrap; +} + +.primary-btn, +.ghost-btn, +.danger-btn, +.icon-btn { + border: 1px solid var(--line); + border-radius: 8px; + color: var(--text); + cursor: pointer; + min-height: 36px; + padding: 8px 12px; + text-decoration: none; +} + +.primary-btn { + border-color: var(--accent); + background: var(--accent); +} + +.ghost-btn, +.icon-btn { + background: var(--panel-2); +} + +.danger-btn { + border-color: #5d2525; + background: #241010; + color: #ffb9b9; +} + +.icon-btn { + width: 36px; + padding: 0; +} + +.empty-state { + margin-top: 24px; + max-width: 680px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--muted); + line-height: 1.45; + padding: 16px; +} + +.block-form { + display: grid; + gap: 18px; + margin-top: 24px; +} + +.hidden { + display: none; +} + +.field-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +label { + display: grid; + gap: 8px; + color: var(--muted); + font-size: 13px; + font-weight: 700; +} + +input, +textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--text); + outline: none; + padding: 10px 12px; +} + +input:focus, +textarea:focus { + border-color: var(--accent); +} + +.toggle-row { + justify-content: flex-start; + gap: 10px; +} + +.toggle-row input { + width: 18px; + height: 18px; +} + +.json-field textarea { + min-height: 360px; + resize: vertical; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 13px; + line-height: 1.45; +} + +.status { + min-height: 24px; + margin: 18px 0 0; + color: var(--muted); + white-space: pre-wrap; +} + +@media (max-width: 900px) { + .admin-shell { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid var(--line); + max-height: 48vh; + } + + .editor-head { + align-items: flex-start; + flex-direction: column; + } + + .field-grid { + grid-template-columns: 1fr; + } +} diff --git a/admin/admin.js b/admin/admin.js new file mode 100644 index 0000000..460d3a7 --- /dev/null +++ b/admin/admin.js @@ -0,0 +1,265 @@ +const state = { + page: null, + selected: null, + clipboard: null, +}; + +const el = { + regions: document.querySelector("#regions"), + reload: document.querySelector("#reload"), + save: document.querySelector("#save"), + render: document.querySelector("#render"), + status: document.querySelector("#status"), + empty: document.querySelector("#empty-state"), + form: document.querySelector("#block-form"), + selectedRegion: document.querySelector("#selected-region"), + selectedTitle: document.querySelector("#selected-title"), + id: document.querySelector("#block-id"), + label: document.querySelector("#block-label"), + template: document.querySelector("#block-template"), + anchor: document.querySelector("#block-anchor"), + enabled: document.querySelector("#block-enabled"), + json: document.querySelector("#block-json"), + moveUp: document.querySelector("#move-up"), + moveDown: document.querySelector("#move-down"), + duplicate: document.querySelector("#duplicate"), + copy: document.querySelector("#copy"), + pasteAfter: document.querySelector("#paste-after"), + deleteBlock: document.querySelector("#delete-block"), +}; + +function setStatus(message) { + el.status.textContent = message; +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function activeRegion() { + if (!state.selected) return null; + return state.page.regions[state.selected.regionIndex]; +} + +function activeBlock() { + const region = activeRegion(); + if (!region || state.selected.blockIndex == null) return null; + return region.blocks[state.selected.blockIndex]; +} + +function uniqueId(base) { + const ids = new Set(state.page.regions.flatMap((region) => region.blocks.map((block) => block.id))); + let candidate = `${base}-copy`; + let index = 2; + + while (ids.has(candidate)) { + candidate = `${base}-copy-${index}`; + index += 1; + } + + return candidate; +} + +function renderRegions() { + el.regions.innerHTML = ""; + + state.page.regions.forEach((region, regionIndex) => { + const regionNode = document.createElement("section"); + const title = document.createElement("h3"); + const list = document.createElement("div"); + + title.className = "region-title"; + title.textContent = region.label || region.id; + list.className = "block-list"; + + region.blocks.forEach((block, blockIndex) => { + const button = document.createElement("button"); + const meta = document.createElement("span"); + const isActive = + state.selected?.regionIndex === regionIndex && state.selected?.blockIndex === blockIndex; + + button.type = "button"; + button.className = `block-btn${isActive ? " active" : ""}${block.enabled === false ? " disabled" : ""}`; + button.textContent = block.adminLabel || block.id; + meta.className = "block-meta"; + meta.textContent = `${block.id} · ${block.template}`; + button.append(meta); + button.addEventListener("click", () => { + state.selected = { regionIndex, blockIndex }; + renderAll(); + }); + + list.append(button); + }); + + regionNode.append(title, list); + el.regions.append(regionNode); + }); +} + +function renderEditor() { + const region = activeRegion(); + const block = activeBlock(); + + if (!block) { + el.empty.classList.remove("hidden"); + el.form.classList.add("hidden"); + el.selectedRegion.textContent = "Выберите блок"; + el.selectedTitle.textContent = "Контентный слой"; + return; + } + + el.empty.classList.add("hidden"); + el.form.classList.remove("hidden"); + el.selectedRegion.textContent = region.label || region.id; + el.selectedTitle.textContent = block.adminLabel || block.id; + el.id.value = block.id || ""; + el.label.value = block.adminLabel || ""; + el.template.value = block.template || ""; + el.anchor.value = block.anchor || ""; + el.enabled.checked = block.enabled !== false; + el.json.value = JSON.stringify(block, null, 2); +} + +function renderAll() { + renderRegions(); + renderEditor(); +} + +function syncBlockFromFields() { + const block = activeBlock(); + if (!block) return; + + block.id = el.id.value.trim(); + block.adminLabel = el.label.value.trim(); + block.anchor = el.anchor.value.trim() || null; + block.enabled = el.enabled.checked; + el.json.value = JSON.stringify(block, null, 2); + renderRegions(); +} + +function syncBlockFromJson() { + const region = activeRegion(); + if (!region) return false; + + try { + const parsed = JSON.parse(el.json.value); + region.blocks[state.selected.blockIndex] = parsed; + renderAll(); + setStatus("JSON блока применён локально. Нажмите «Сохранить JSON», чтобы записать файл."); + return true; + } catch (error) { + setStatus(`Ошибка JSON: ${error.message}`); + return false; + } +} + +async function loadPage() { + const response = await fetch("/api/page/home"); + if (!response.ok) throw new Error(await response.text()); + state.page = await response.json(); + state.selected = null; + renderAll(); + setStatus("Модель home.json загружена."); +} + +async function savePage() { + if (activeBlock()) syncBlockFromFields(); + + const response = await fetch("/api/page/home", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(state.page), + }); + + if (!response.ok) throw new Error(await response.text()); + setStatus("content/pages/home.json сохранён."); +} + +async function renderHome() { + const response = await fetch("/api/render/home", { method: "POST" }); + const payload = await response.json(); + + if (!response.ok || !payload.ok) { + throw new Error(payload.error || "Render failed"); + } + + setStatus(payload.stdout || "index.html собран."); +} + +function moveSelected(delta) { + const region = activeRegion(); + const index = state.selected?.blockIndex; + if (!region || index == null) return; + + const nextIndex = index + delta; + if (nextIndex < 0 || nextIndex >= region.blocks.length) return; + + const [block] = region.blocks.splice(index, 1); + region.blocks.splice(nextIndex, 0, block); + state.selected.blockIndex = nextIndex; + renderAll(); +} + +function duplicateSelected() { + const region = activeRegion(); + const block = activeBlock(); + if (!region || !block) return; + + const duplicate = clone(block); + duplicate.id = uniqueId(block.id); + duplicate.adminLabel = `${block.adminLabel || block.id} — копия`; + duplicate.scopeIds = true; + region.blocks.splice(state.selected.blockIndex + 1, 0, duplicate); + state.selected.blockIndex += 1; + renderAll(); +} + +function copySelected() { + const block = activeBlock(); + if (!block) return; + + state.clipboard = clone(block); + setStatus(`Скопирован блок: ${block.adminLabel || block.id}`); +} + +function pasteAfterSelected() { + const region = activeRegion(); + if (!region || !state.clipboard) return; + + const pasted = clone(state.clipboard); + pasted.id = uniqueId(pasted.id); + pasted.adminLabel = `${pasted.adminLabel || pasted.id} — вставка`; + pasted.scopeIds = true; + region.blocks.splice(state.selected.blockIndex + 1, 0, pasted); + state.selected.blockIndex += 1; + renderAll(); +} + +function deleteSelected() { + const region = activeRegion(); + if (!region || state.selected.blockIndex == null) return; + + region.blocks.splice(state.selected.blockIndex, 1); + state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1); + if (state.selected.blockIndex < 0) state.selected = null; + renderAll(); +} + +for (const input of [el.id, el.label, el.anchor, el.enabled]) { + input.addEventListener("input", syncBlockFromFields); + input.addEventListener("change", syncBlockFromFields); +} + +el.json.addEventListener("blur", syncBlockFromJson); +el.reload.addEventListener("click", () => loadPage().catch((error) => setStatus(error.message))); +el.save.addEventListener("click", () => savePage().catch((error) => setStatus(error.message))); +el.render.addEventListener("click", () => renderHome().catch((error) => setStatus(error.message))); +el.moveUp.addEventListener("click", () => moveSelected(-1)); +el.moveDown.addEventListener("click", () => moveSelected(1)); +el.duplicate.addEventListener("click", duplicateSelected); +el.copy.addEventListener("click", copySelected); +el.pasteAfter.addEventListener("click", pasteAfterSelected); +el.deleteBlock.addEventListener("click", deleteSelected); + +loadPage().catch((error) => setStatus(error.message)); diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..4bfc4da --- /dev/null +++ b/admin/index.html @@ -0,0 +1,85 @@ + + + + + + NODE.DC Admin + + + +
+ + +
+
+
+
Выберите блок
+

Контентный слой

+
+
+ Открыть сайт + + +
+
+ +
+ Слева выберите блок. Сейчас это первый этап: блоки можно включать, выключать, + дублировать и менять порядок внутри своего DOM-региона. +
+ + + +

+      
+
+ + + diff --git a/content/pages/home.json b/content/pages/home.json new file mode 100644 index 0000000..7ad844d --- /dev/null +++ b/content/pages/home.json @@ -0,0 +1,89 @@ +{ + "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": [ + { + "region": "beforeCViz", + "id": "hero-waitlist", + "type": "staticHtml", + "template": "hero-waitlist.html", + "adminLabel": "Первый экран и заявка", + "anchor": "hero", + "enabled": true + }, + { + "region": "beforeCViz", + "id": "feature-video", + "type": "staticHtml", + "template": "feature-video.html", + "adminLabel": "Демо и базовая логика", + "anchor": "features", + "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": [ + { + "region": "cViz", + "id": "product-system", + "type": "staticHtml", + "template": "product-system.html", + "adminLabel": "Компоненты, режимы, FAQ и тарифная таблица", + "anchor": null, + "enabled": true + }, + { + "region": "cViz", + "id": "pricing-intro", + "type": "staticHtml", + "template": "pricing-intro.html", + "adminLabel": "Интро тарифов", + "anchor": "waitlist", + "enabled": true + }, + { + "region": "cViz", + "id": "waitlist-files", + "type": "staticHtml", + "template": "waitlist-files.html", + "adminLabel": "Форма ожидания и txt-файлы", + "anchor": "waitlist", + "enabled": true + }, + { + "region": "cViz", + "id": "demo-video-folder", + "type": "staticHtml", + "template": "demo-video-folder.html", + "adminLabel": "Иконка демо-видео в доке", + "anchor": null, + "enabled": true + }, + { + "region": "cViz", + "id": "footer", + "type": "staticHtml", + "template": "footer.html", + "adminLabel": "Футер", + "anchor": null, + "enabled": true + } + ] + } + ] +} diff --git a/docs/refactor-admin-plan.md b/docs/refactor-admin-plan.md index b184878..9d016df 100644 --- a/docs/refactor-admin-plan.md +++ b/docs/refactor-admin-plan.md @@ -166,6 +166,28 @@ Global non-block content: 5. Add Payload CMS only after static JSON rendering is visually stable. 6. Optimize media and runtime after block rendering is stable. +## Phase 1 Implementation Status + +Started on June 25, 2026. + +Implemented: + +- `content/pages/home.json` contains the first ordered block model for the home page. +- `templates/pages/home/shell.html` contains the current Webflow shell with two render slots. +- `templates/pages/home/blocks/` contains exact HTML block chunks extracted from the current localized page. +- `tools/render-home.mjs` renders `index.html` from the JSON model and templates. +- `tools/extract-home-blocks.mjs` can regenerate the first template split from the current `index.html`. +- `tools/admin-server.mjs` and `admin/` provide a local block editor. + +Current guarantee: + +- Rendering through `npm run render:home` has byte-for-byte parity with the pre-refactor `index.html`. + +Current limitation: + +- Blocks are still HTML-template blocks. The admin can manage order, enabled state, duplication, copy/paste, IDs, labels, anchors, and raw block JSON. The next pass must extract text/media fields from each block template into typed JSON fields. +- The `c-viz` wrapper is preserved as a region boundary because it is coupled to WebGL/color-flip behavior. Blocks inside that region should stay inside it until the runtime is better understood. + ## Optimization Passes Do after structural parity, not before: diff --git a/package.json b/package.json new file mode 100644 index 0000000..decbd9e --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "nodedc-site", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "extract:home": "node tools/extract-home-blocks.mjs", + "render:home": "node tools/render-home.mjs", + "admin": "node tools/admin-server.mjs" + } +} diff --git a/templates/pages/home/blocks/demo-video-folder.html b/templates/pages/home/blocks/demo-video-folder.html new file mode 100644 index 0000000..24bf4bb --- /dev/null +++ b/templates/pages/home/blocks/demo-video-folder.html @@ -0,0 +1 @@ +
Иконка MOV-файла.
демо-видео.txt
\ No newline at end of file diff --git a/templates/pages/home/blocks/feature-video.html b/templates/pages/home/blocks/feature-video.html new file mode 100644 index 0000000..cf028df --- /dev/null +++ b/templates/pages/home/blocks/feature-video.html @@ -0,0 +1 @@ +
демо-видео.mp4

Всё в одномприложение.

Универсальный инструмент для ИИ-агентов в вашем Webflow-проекте.
Контекст проекта
Подключитесь к MCP чтобы агенты получили полный контекст вашей сборки.
Группы агентов
Используйте несколько агентов параллельно чтобы добавлять кастомный код, анимации и функциональность.
Среда разработки
Используйте быстрые Bun сборки и автообновление для вашего кастомного кода.
Деплой в один клик
Нажмите и публикуйте кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта.
\ No newline at end of file diff --git a/templates/pages/home/blocks/footer.html b/templates/pages/home/blocks/footer.html new file mode 100644 index 0000000..fac8221 --- /dev/null +++ b/templates/pages/home/blocks/footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/pages/home/blocks/hero-waitlist.html b/templates/pages/home/blocks/hero-waitlist.html new file mode 100644 index 0000000..b0c8b6c --- /dev/null +++ b/templates/pages/home/blocks/hero-waitlist.html @@ -0,0 +1 @@ +

Ваш Webflow ИИ-инструменты

Несколько агентов для проекта Webflow: от Designer до кастомного кода.

Полныйагентный контроль

Интеграция с Webflow MCP, мультиагентный режим позволяет работать с несколькими экземплярами параллельно.

Компонентнаябиблиотека

Собирайте с учетом ИИ-агентов разные компонентные заготовки дают основу, а стиль и сложную функциональность можно дорабатывать промптами.

Деплой пайплайн

Деплой в один клик, непрерывный пайплайн, мгновенный откат и быстрая доставка через CDN.
Логотип NODE.DC
Бета-релиз: июль 2026

Ранний доступ

Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре.

Заявка принята. Спасибо.

Мы скоро свяжемся.

Подать заявку в бету?

Или оформить ранний Founder-доступ

Оформить Founder-доступ 350€
До 200 мест

Не получилось

Что-то пошло не так.
Попробуйте ещё раз чуть позже.

\ No newline at end of file diff --git a/templates/pages/home/blocks/pricing-intro.html b/templates/pages/home/blocks/pricing-intro.html new file mode 100644 index 0000000..232f5b0 --- /dev/null +++ b/templates/pages/home/blocks/pricing-intro.html @@ -0,0 +1 @@ +
Тарифы

Коддля No-Code,усиленный ИИ.

Подписки на приложение с
включённым объёмом в каждом тарифе.
Токенная оплата сверх лимитов.
200 Founder мест с фиксированной ценой на первый год.
\ No newline at end of file diff --git a/templates/pages/home/blocks/product-system.html b/templates/pages/home/blocks/product-system.html new file mode 100644 index 0000000..fc14785 --- /dev/null +++ b/templates/pages/home/blocks/product-system.html @@ -0,0 +1 @@ +

АгентнаяКомпонентнаябиблиотека.

Готовая к работе с агентами компонентная библиотека: добавление в один клик через приложение NODE.DC для Webflow и дальнейшая настройка под проект.

Переключение режимов ИИ

Создание нового проекта

ИИ-док

Аккаунт

Редактирование кода с ИИ

Dev-сервер с ИИ

Webflow MCP

Публикация

Одинкликдобавления.

С помощью расширения NODE.DC для Webflow.
Понимание компонентов
Используйте несколько агентов параллельно чтобы добавлять кастомный код, анимации и функциональность.
Быстрый старт
Используйте быстрые Bun сборки и автообновление для вашего кастомного кода.

Получитеболее сильныйMCP.

Полностью переработанный и более быстрый MCP-сервер.
Понимание Webflow
Используйте несколько агентов параллельно чтобы добавлять кастомный код, анимации и функциональность.

Гибкиережимы ИИ.

Разные режимы и модели для разных задач.

Переключение режимов ИИ

Создание нового проекта

ИИ-док

Аккаунт

Редактирование кода с ИИ

Dev-сервер с ИИ

Webflow MCP

Публикация

Частыевопросыи ответы

Подробный обзор возможностей, логики работы и внутреннего устройства.
FAQ
NODE.DC
Это влияет на производительность?
NODE.DC
Где размещаются мои файлы?
NODE.DC
Что делать, если я опубликовал не те файлы?
NODE.DC
Что такое ИИ-агент Webflow?
NODE.DC
Что такое собственная база знаний?
NODE.DC
Как использовать это с Webflow?
NODE.DC
Какие ИИ-модели доступны?
NODE.DC
Как это работает?
NODE.DC
Как работает публикация?
NODE.DC
Это работает с MCP?
NODE.DC
Ответить
Это влияет на производительность?

Как и любой кастомный код, это добавляет только ту нагрузку, которую несёт сам код.

Все файлы собираются, минифицируются и сжимаются, чтобы минимально влиять на проект и не плодить внешние библиотеки и CDN-скрипты.

Отправлено с iPhone
NODE.DC
Ответить
Где размещаются мои файлы?

После сборки файлы загружаются в Cloudflare R2 и отдаются через CDN-ссылку на выделенном домене проекта, чтобы получить быстрый и совместимый с Webflow результат.

Отправлено с iPhone
NODE.DC
Ответить
Что делать, если я опубликовал не те файлы?

В панели деплоя есть мгновенный откат.

Нажмите откат, верните предыдущую рабочую версию, исправьте локально и опубликуйте заново.

Отправлено с iPhone
NODE.DC
Ответить
Что такое ИИ-агент Webflow?

Помимо @plan, @build и @explore стандартных режимов агентов мы готовим @webflow режим, специально заточенный под Webflow: структуру проекта, взаимодействие с ним и эффективную разработку.

Отправлено с iPhone
NODE.DC
Ответить
Что такое собственная база знаний?

Основано на многолетнем опыте написания кастомного кода для Webflow-анимаций.

WebGL, Three.js, GSAP и кастомная функциональность собраны в набор практик, пайплайнов и модульную систему, чтобы всё работало согласованно.

Также доступна библиотека компонентов: выберите компонент, добавьте в проект и попросите ИИ адаптировать его под задачу.

Отправлено с iPhone
NODE.DC
Ответить
Как использовать это с Webflow?

При запуске NODE.DC доступна отдельная вкладка для встраивания.

В зависимости от задачи можно получить скрипты для конкретной страницы или единый app.js для всего проекта.

Сгенерированная ссылка содержит и продакшен-версию, следующую за URL деплоя, и локальную версию для предпросмотра без влияния на посетителей сайта.

Отправлено с iPhone
NODE.DC
Ответить
Какие ИИ-модели доступны?

Мы используем основные доступные модели: OpenAI до Claude до Gemini.

Оркестратор выбирает модель под задачу автоматически, но вы можете вручную переключиться на нужную модель.

Отправлено с iPhone
NODE.DC
Ответить
Как это работает?

Во время работы NODE.DC выполняет две задачи параллельно.

Поднимает локальную среду разработки на Bun, синхронизирует JavaScript и TypeScript, включает HMR и держит webhook для автоматического обновления сайта при изменениях.

Одновременно работает оркестратор ИИ-агентов: обрабатывает запросы, меняет код, проверяет состояние сборки и при необходимости обновляет проект Webflow.

Отправлено с iPhone
NODE.DC
Ответить
Как работает публикация?

Процесс многоэтапный: сначала Bun собирает, сжимает, минифицирует и подготавливает проект вместе с библиотеками.

Затем файлы отправляются в CDN: cdn.node.dc/{USERNAME}/{PROJECT-NAME}. Ссылка уже содержит нужную структуру: вы видите локальную версию, а пользователи получают опубликованный скрипт.

Отправлено с iPhone
NODE.DC
Ответить
Это работает с MCP?

Система изначально рассчитана на работу с Webflow MCP.

Подключите проект и авторизацию, чтобы ИИ-агенты получили доступ к Webflow-сборке. Подключение приложения в Webflow Designer даёт агентам доступ к контенту, элементам, стилям и CSS, а кастомный код получает полный контекст проекта.

Отправлено с iPhone

Подключить.Поставить задачу.Опубликовать.

Единый путь от разработки до продакшена.
Настройка примерно в 12 кликов: от скачивания приложения до запуска в проекте.
Встать в лист ожидания
Тарифы

Коддля No-Code,усиленный ИИ.

Подписки на приложение с
включённым объёмом в каждом тарифе.
Токенная оплата сверх лимитов.
200 Founder мест с фиксированной ценой на первый год.
Тарифы.app

Бета

Бесплатно
1 месяц

Founder

310
/ год, навсегда
Скидка 50%

Basic

20
/ месяц

Advanced

50
/ месяц
Старт
Старт
Старт
Всего доступно
Расширение (20M)
Расширение (10M)
Глубина рассуждения
Базовый интеллект
Рабочее пространство
Объём токенов
Старт
Объём токенов
Объём токенов
Рабочее пространство
Всего доступно
Расширение (20M)
Расширение (10M)
Глубина рассуждения
Базовый интеллект
Рабочее пространство
Базовый интеллект
Глубина рассуждения
Расширение (10M)
Расширение (20M)
Всего доступно
Июнь 2026
Июнь 2026
Июнь 2026
Июнь 2026
Кредиты тарифа
Без ограничений
40 000 включено
12 000 включено
40 000 включено
Рабочее пространство
Без ограничений
Без ограничений
10 проектов
Без ограничений
Базовый интеллект
Все модели
Все быстрые
Все быстрые
Все быстрые
Глубина рассуждения
Полный
Полный
Ограниченный
Полный
Расширение SM 10€
Обновления без лимита
+ 10 000 кредитов
+ 8 000 кредитов
+ 10 000 кредитов
Расширение MD 20€
Обновления без лимита
+ 26 000 кредитов
+ 20 000 кредитов
+ 26 000 кредитов
Всего доступно
40 тестировщиков
200 мест
--
--
Подать заявку в бету
Оформить Founder-доступ
(*) Условия могут меняться по ходу тестирования, но мы постараемся сохранить заявленные обещания.
Enterprise BYOK-тарифы будут доступны с сентября 2026.
публикация-в-один-клик.png
Иконка MOV-файла.
публикация-в-один-клик.png
полноценный-редактор-кода.webp
Иконка MOV-файла.
полноценный-редактор-кода.webp
ИИ-док-режим.jpg
Иконка MOV-файла.
ИИ-док-режим.jpg
глубокая-интеграция-webflow.webp
Иконка MOV-файла.
глубокая-интеграция-webflow.webp
установка-настройка-запуск.jpg
Иконка MOV-файла.
установка-настройка-запуск.jpg
\ No newline at end of file diff --git a/templates/pages/home/blocks/waitlist-files.html b/templates/pages/home/blocks/waitlist-files.html new file mode 100644 index 0000000..ae631f1 --- /dev/null +++ b/templates/pages/home/blocks/waitlist-files.html @@ -0,0 +1,4 @@ +
Логотип NODE.DC
Бета-релиз: июль 2026

Ранний доступ

Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре.

Заявка принята. Спасибо.

Мы скоро свяжемся.

Подать заявку в бету?
Иконка текстового файла.
fff
fff
Иконка текстового файла.
Ergtrr
Ggg
Иконка текстового файла.
Paul
только что нашёл ваш сайт — выглядит отлично! +
Иконка текстового файла.
s
s
Иконка текстового файла.
rkxkr
пока не уверен, но да, интересно
Иконка текстового файла.
Ali Awan
Дайте, пожалуйста, посмотреть продукт!
Иконка текстового файла.
rox del toro
ХОЧУ ДОСТУП
Иконка текстового файла.
Flavio
Привет!
Иконка текстового файла.
Daniel Clydesdale
Мы работаем с Webflow с 2016 года и используем его постоянно. Сейчас внедряем ИИ в процессы, поэтому ваш продукт нам очень интересен. Удачи с запуском!
Иконка текстового файла.
Carter Ogunsola
Для всех разработчиков: я нашёл это очень рано. Уже один из фаворитов.
Иконка текстового файла.
Marin Trajkovski
Здравствуйте, можно получить ранний доступ? Очень хочу протестировать ИИ, это сейчас действительно нужно.
Иконка текстового файла.
Arul
Интересно. Хотелось бы узнать больше.
Иконка текстового файла.
Jeremy
Вау! + +Пожалуйста, пустите меня в бету!
Иконка текстового файла.
Safin
С удовольствием протестировал бы продукт. Выглядит очень интересно.
Иконка текстового файла.
Ilja van Eck
Я теперь первый Founder? Берите деньги и запускайте это.
Иконка текстового файла.
Jordan Gilroy
Я ваш главный фанат
Иконка текстового файла.
hyun
привет
Иконка текстового файла.
kris
тестирую
Иконка текстового файла.
Syed Irfan
Интересно
Иконка текстового файла.
ramyar atlasi
Apm pflege schule Düsseldorf
Иконка текстового файла.
Muhammad Ashar
vv
Иконка текстового файла.
:DD
ka tu nx
Иконка текстового файла.
test
test
Иконка текстового файла.
Filip
Я знаю Fed!
Иконка текстового файла.
Arinbjørn
Пустите меня!
Иконка текстового файла.
Janarthanan
жду
Иконка текстового файла.
Jane Smith
Очень хочу ранний доступ к продукту!
Иконка текстового файла.
Daniel Apro
Привет, сайт огонь.
Иконка текстового файла.
Allonelabs
Нам очень нравится ваш дизайн. Спасибо!
Иконка текстового файла.
nicola romei
G.O.A.T. часто используют как сокращение от Greatest of All Time — «лучший за всё время».
Иконка TXT-файла
fff
.txt
Иконка TXT-файла
Ergtrr
.txt
Иконка TXT-файла
Paul
.txt
Иконка TXT-файла
s
.txt
Иконка TXT-файла
rkxkr
.txt
Иконка TXT-файла
Ali Awan
.txt
Иконка TXT-файла
rox del toro
.txt
Иконка TXT-файла
Flavio
.txt
Иконка TXT-файла
Daniel Clydesdale
.txt
Иконка TXT-файла
Carter Ogunsola
.txt
Иконка TXT-файла
Marin Trajkovski
.txt
Иконка TXT-файла
Arul
.txt
Иконка TXT-файла
Jeremy
.txt
Иконка TXT-файла
Safin
.txt
Иконка TXT-файла
Ilja van Eck
.txt
Иконка TXT-файла
Jordan Gilroy
.txt
Иконка TXT-файла
hyun
.txt
Иконка TXT-файла
kris
.txt
Иконка TXT-файла
Syed Irfan
.txt
Иконка TXT-файла
ramyar atlasi
.txt
Иконка TXT-файла
Muhammad Ashar
.txt
Иконка TXT-файла
:DD
.txt
Иконка TXT-файла
test
.txt
Иконка TXT-файла
Filip
.txt
Иконка TXT-файла
Arinbjørn
.txt
Иконка TXT-файла
Janarthanan
.txt
Иконка TXT-файла
Jane Smith
.txt
Иконка TXT-файла
Daniel Apro
.txt
Иконка TXT-файла
Allonelabs
.txt
Иконка TXT-файла
nicola romei
.txt
\ No newline at end of file diff --git a/templates/pages/home/shell.html b/templates/pages/home/shell.html new file mode 100644 index 0000000..2d6bc06 --- /dev/null +++ b/templates/pages/home/shell.html @@ -0,0 +1,120 @@ +NODE.DC — ИИ-платформа для Webflow + + + + +
+ +
{{NODEDC_BLOCKS_BEFORE_CVIZ}}
{{NODEDC_BLOCKS_CVIZ}}
ДАЛЕЕ — ИЮЛЬ 2026
Открытая бета
Открываем доступ всем. Будем собирать и отлаживать вместе.
ДАЛЕЕ ИЮЛЬ 2026
Альфа для Founder-доступа
На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.
СЕЙЧАС
Инвайты в закрытую бету уже отправлены.
Сейчас тестируем базовую функциональность и ИИ-интеграцию.
Белая иконка корзины.
\ No newline at end of file diff --git a/tools/admin-server.mjs b/tools/admin-server.mjs new file mode 100644 index 0000000..f4bfba0 --- /dev/null +++ b/tools/admin-server.mjs @@ -0,0 +1,151 @@ +import { createServer } from "node:http"; +import { readFile, stat, writeFile } from "node:fs/promises"; +import { extname, join, normalize } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { dirname } from "node:path"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const port = Number(process.env.PORT || 8090); +const host = process.env.HOST || "127.0.0.1"; +const pagePath = join(repoRoot, "content", "pages", "home.json"); + +const MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".avif": "image/avif", + ".webp": "image/webp", + ".mp4": "video/mp4", + ".glb": "model/gltf-binary", +}; + +function send(res, status, body, contentType = "text/plain; charset=utf-8") { + res.writeHead(status, { + "content-type": contentType, + "cache-control": "no-store", + }); + res.end(body); +} + +function sendJson(res, status, payload) { + send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8"); +} + +async function readBody(req) { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + return Buffer.concat(chunks).toString("utf8"); +} + +function runNodeScript(scriptName) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [join(repoRoot, "tools", scriptName)], { + cwd: repoRoot, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject(new Error(stderr || stdout || `${scriptName} exited with ${code}`)); + } + }); + }); +} + +function safePublicPath(urlPath) { + const withoutQuery = decodeURIComponent(urlPath.split("?")[0]); + const requestPath = + withoutQuery === "/" + ? "/index.html" + : withoutQuery === "/admin" || withoutQuery === "/admin/" + ? "/admin/index.html" + : withoutQuery; + const normalized = normalize(requestPath).replace(/^(\.\.[/\\])+/, ""); + const absolute = join(repoRoot, normalized); + + if (!absolute.startsWith(repoRoot)) { + return null; + } + + return absolute; +} + +async function serveStatic(req, res) { + const filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname); + if (!filePath) { + send(res, 403, "Forbidden"); + return; + } + + try { + const fileStat = await stat(filePath); + if (!fileStat.isFile()) { + send(res, 404, "Not found"); + return; + } + + const ext = extname(filePath); + send(res, 200, await readFile(filePath), MIME[ext] || "application/octet-stream"); + } catch { + send(res, 404, "Not found"); + } +} + +const server = createServer(async (req, res) => { + try { + const url = new URL(req.url, `http://${host}:${port}`); + + if (req.method === "GET" && url.pathname === "/api/page/home") { + send(res, 200, await readFile(pagePath, "utf8"), "application/json; charset=utf-8"); + return; + } + + if (req.method === "PUT" && url.pathname === "/api/page/home") { + const body = await readBody(req); + const parsed = JSON.parse(body); + await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`); + sendJson(res, 200, { ok: true }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/render/home") { + const result = await runNodeScript("render-home.mjs"); + sendJson(res, 200, { ok: true, ...result }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/extract/home") { + const result = await runNodeScript("extract-home-blocks.mjs"); + sendJson(res, 200, { ok: true, ...result }); + return; + } + + await serveStatic(req, res); + } catch (error) { + sendJson(res, 500, { ok: false, error: error.message }); + } +}); + +server.listen(port, host, () => { + console.log(`NODE.DC admin: http://${host}:${port}/admin/`); + console.log(`NODE.DC site: http://${host}:${port}/`); +}); diff --git a/tools/extract-home-blocks.mjs b/tools/extract-home-blocks.mjs new file mode 100644 index 0000000..a143088 --- /dev/null +++ b/tools/extract-home-blocks.mjs @@ -0,0 +1,155 @@ +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: '", 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), + }, +]; + +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: blocks + .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: blocks + .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 blocks) { + 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}`); diff --git a/tools/render-home.mjs b/tools/render-home.mjs new file mode 100644 index 0000000..cd9119f --- /dev/null +++ b/tools/render-home.mjs @@ -0,0 +1,105 @@ +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 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}}", +}; + +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 scopeDuplicateIds(html, scope) { + const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]); + if (!ids.length) { + return html; + } + + let scoped = html.replace(/\sid="([^"]+)"/g, (_match, id) => ` id="${scope}-${id}"`); + + for (const id of ids) { + const escaped = escapeRegExp(id); + scoped = scoped.replace( + new RegExp(`\\s(data-target|for|aria-controls|aria-labelledby|aria-describedby)="${escaped}"`, "g"), + (_match, attr) => ` ${attr}="${scope}-${id}"`, + ); + scoped = scoped.replace(new RegExp(`(href="[^"]*)#${escaped}(")`, "g"), `$1#${scope}-${id}$2`); + } + + return scoped; +} + +async function renderBlock(block, renderCounts) { + if (block.enabled === false) { + return ""; + } + + const templateName = block.template; + const renderCount = renderCounts.get(templateName) ?? 0; + renderCounts.set(templateName, renderCount + 1); + + const html = await readTemplate(templateName); + + if (renderCount === 0 && block.scopeIds !== true) { + return html; + } + + return scopeDuplicateIds(html, block.id); +} + +async function renderRegion(region) { + const renderCounts = new Map(); + const blocks = []; + + for (const block of region.blocks ?? []) { + blocks.push(await renderBlock(block, renderCounts)); + } + + return blocks.join(""); +} + +let output = shell; +for (const region of page.regions ?? []) { + const token = REGION_TOKEN[region.id]; + if (!token) { + throw new Error(`Unknown region: ${region.id}`); + } + + output = output.replace(token, await renderRegion(region)); +} + +for (const token of Object.values(REGION_TOKEN)) { + if (output.includes(token)) { + throw new Error(`Unrendered token left in shell: ${token}`); + } +} + +const outputPath = join(repoRoot, page.output || "index.html"); +await mkdir(dirname(outputPath), { recursive: true }); +await writeFile(outputPath, output); + +console.log(`Rendered ${page.slug} -> ${outputPath}`);