Split product system into editable sections
This commit is contained in:
@@ -38,6 +38,17 @@ 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;
|
||||
const productSystemHtml = html.slice(cVizOpenEnd, pricingIntroStart);
|
||||
const productSectionStarts = [...productSystemHtml.matchAll(/<section\b/g)].map((match) => match.index);
|
||||
|
||||
if (productSectionStarts.length < 5) {
|
||||
throw new Error(`Expected at least 5 product-system sections, found ${productSectionStarts.length}`);
|
||||
}
|
||||
|
||||
const productSections = productSectionStarts.map((start, index) => {
|
||||
const end = productSectionStarts[index + 1] ?? productSystemHtml.length;
|
||||
return productSystemHtml.slice(start, end);
|
||||
});
|
||||
|
||||
let shell = [
|
||||
html.slice(0, heroStart),
|
||||
@@ -68,12 +79,48 @@ const blocks = [
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "product-system",
|
||||
id: "component-library",
|
||||
type: "staticHtml",
|
||||
template: "product-system.html",
|
||||
adminLabel: "Компоненты, режимы, FAQ и тарифная таблица",
|
||||
template: "component-library.html",
|
||||
adminLabel: "Компонентная библиотека и MCP",
|
||||
anchor: null,
|
||||
html: html.slice(cVizOpenEnd, pricingIntroStart),
|
||||
html: productSections[0],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "ai-modes",
|
||||
type: "staticHtml",
|
||||
template: "ai-modes.html",
|
||||
adminLabel: "Гибкие режимы ИИ",
|
||||
anchor: null,
|
||||
html: productSections[1],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "faq",
|
||||
type: "staticHtml",
|
||||
template: "faq.html",
|
||||
adminLabel: "Частые вопросы и ответы",
|
||||
anchor: null,
|
||||
html: productSections[2],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "publish-cta",
|
||||
type: "staticHtml",
|
||||
template: "publish-cta.html",
|
||||
adminLabel: "Финальный CTA перед тарифами",
|
||||
anchor: "hero",
|
||||
html: productSections[3],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "pricing-table",
|
||||
type: "staticHtml",
|
||||
template: "pricing-table.html",
|
||||
adminLabel: "Тарифная таблица",
|
||||
anchor: "pricing-wrap",
|
||||
html: productSections[4],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
@@ -128,12 +175,18 @@ function inferFieldGroup(path) {
|
||||
if (path === "content.legalHtml") return "Юридический блок";
|
||||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||||
if (path.startsWith("content.features.")) return "Пункты";
|
||||
if (path.startsWith("content.sliderItems.")) return "Слайдер";
|
||||
if (path.startsWith("content.oneClick.")) return "Один клик";
|
||||
if (path.startsWith("content.mcp.")) return "MCP";
|
||||
if (path.startsWith("content.faq.")) return "FAQ";
|
||||
if (path.startsWith("content.cta.")) return "CTA";
|
||||
if (path.startsWith("content.plans.") || path.startsWith("content.pricing.")) 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 field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null, options = {}) {
|
||||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options };
|
||||
}
|
||||
|
||||
function tokenizeBlock(block, definition) {
|
||||
@@ -151,7 +204,7 @@ function tokenizeBlock(block, definition) {
|
||||
throw new Error(`Could not find "${value}" while tokenizing ${block.id}`);
|
||||
}
|
||||
|
||||
html = html.replace(value, token);
|
||||
html = editableField.replaceAll ? html.split(value).join(token) : html.replace(value, token);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -163,6 +216,24 @@ function tokenizeBlock(block, definition) {
|
||||
};
|
||||
}
|
||||
|
||||
const demoSliderItems = [
|
||||
{ videoSrc: "./assets/media/various-modes.mp4", title: "Переключение режимов ИИ" },
|
||||
{ videoSrc: "./assets/media/project-flow.mp4", title: "Создание нового проекта" },
|
||||
{ videoSrc: "./assets/media/ai-dock-mode.mp4", title: "ИИ-док" },
|
||||
{ videoSrc: "./assets/media/account.mp4", title: "Аккаунт" },
|
||||
{ videoSrc: "./assets/media/code-edits.mp4", title: "Редактирование кода с ИИ" },
|
||||
{ videoSrc: "./assets/media/dev-server.mp4", title: "Dev-сервер с ИИ" },
|
||||
{ videoSrc: "./assets/media/webflow-mcp.mp4", title: "Webflow MCP" },
|
||||
{ videoSrc: "./assets/media/publish-flow.mp4", title: "Публикация" },
|
||||
];
|
||||
|
||||
function sliderEditableFields(prefix = "content.sliderItems", labelPrefix = "Слайд") {
|
||||
return demoSliderItems.flatMap((_, index) => [
|
||||
field(`${prefix}.${index}.videoSrc`, `${labelPrefix} ${index + 1}: видео`, "media", "attr"),
|
||||
field(`${prefix}.${index}.title`, `${labelPrefix} ${index + 1}: подпись`),
|
||||
]);
|
||||
}
|
||||
|
||||
const typedBlockDefinitions = {
|
||||
"hero-waitlist": {
|
||||
type: "heroWaitlist",
|
||||
@@ -300,6 +371,279 @@ const typedBlockDefinitions = {
|
||||
field("content.features.3.bodyHtml", "Пункт 4: текст", "html"),
|
||||
],
|
||||
},
|
||||
"component-library": {
|
||||
type: "componentLibrary",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Агентная",
|
||||
middle: "Компонентная",
|
||||
main: "библиотека.",
|
||||
},
|
||||
introHtml:
|
||||
'Готовая к работе с агентами <span class="darker-70">компонентная</span> библиотека: добавление в один клик через приложение NODE.DC для Webflow и дальнейшая настройка под проект.',
|
||||
sliderItems: demoSliderItems,
|
||||
oneClick: {
|
||||
heading: {
|
||||
muted: "Один",
|
||||
middle: "клик",
|
||||
main: "добавления.",
|
||||
},
|
||||
introHtml: 'С помощью <span class="darker-70">расширения NODE.DC для Webflow.</span> ',
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Понимание компонентов",
|
||||
bodyHtml:
|
||||
'Используйте <span class="darker-70">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность.',
|
||||
},
|
||||
{
|
||||
eyebrow: "Быстрый старт",
|
||||
bodyHtml:
|
||||
'Используйте быстрые <span class="darker-70">Bun</span> сборки и автообновление для вашего <span class="darker-70">кастомного кода.</span>',
|
||||
},
|
||||
],
|
||||
},
|
||||
mcp: {
|
||||
heading: {
|
||||
muted: "Получите",
|
||||
middle: "более сильный",
|
||||
main: "MCP.",
|
||||
},
|
||||
introHtml: "Полностью переработанный и более быстрый MCP-сервер. ",
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Понимание Webflow",
|
||||
bodyHtml:
|
||||
'Используйте <span class="darker-70">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.middle", "Заголовок: средняя строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
...sliderEditableFields(),
|
||||
field("content.oneClick.heading.muted", "Один клик: приглушённая строка"),
|
||||
field("content.oneClick.heading.middle", "Один клик: средняя строка"),
|
||||
field("content.oneClick.heading.main", "Один клик: основная строка"),
|
||||
field("content.oneClick.introHtml", "Один клик: вводный текст", "html"),
|
||||
field("content.oneClick.items.0.eyebrow", "Один клик: пункт 1, подпись"),
|
||||
field("content.oneClick.items.0.bodyHtml", "Один клик: пункт 1, текст", "html"),
|
||||
field("content.oneClick.items.1.eyebrow", "Один клик: пункт 2, подпись"),
|
||||
field("content.oneClick.items.1.bodyHtml", "Один клик: пункт 2, текст", "html"),
|
||||
field("content.mcp.heading.muted", "MCP: приглушённая строка"),
|
||||
field("content.mcp.heading.middle", "MCP: средняя строка"),
|
||||
field("content.mcp.heading.main", "MCP: основная строка"),
|
||||
field("content.mcp.introHtml", "MCP: вводный текст", "html"),
|
||||
field("content.mcp.items.0.eyebrow", "MCP: пункт 1, подпись"),
|
||||
field("content.mcp.items.0.bodyHtml", "MCP: пункт 1, текст", "html"),
|
||||
],
|
||||
},
|
||||
"ai-modes": {
|
||||
type: "aiModes",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Гибкие",
|
||||
main: "режимы ИИ.",
|
||||
},
|
||||
introHtml: 'Разные <span class="darker-70">режимы</span> и <span class="darker-70">модели</span> для разных задач.',
|
||||
sliderItems: demoSliderItems,
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
...sliderEditableFields(),
|
||||
],
|
||||
},
|
||||
faq: {
|
||||
type: "faqSection",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Частые",
|
||||
middle: "вопросы",
|
||||
main: "и ответы",
|
||||
},
|
||||
introHtml: '<span class="darker-70">Подробный обзор</span> возможностей, логики работы и внутреннего устройства.',
|
||||
docsHref: "https://docs.ssscript.app/",
|
||||
docsLabel: "Полная документация",
|
||||
windowTitle: "FAQ",
|
||||
faq: [
|
||||
{
|
||||
question: "Это влияет на производительность?",
|
||||
answerHtml:
|
||||
"<p>Как и любой кастомный код, это добавляет только ту нагрузку, которую несёт сам код. </p><p>Все файлы собираются, минифицируются и сжимаются, чтобы минимально влиять на проект и не плодить внешние библиотеки и CDN-скрипты.</p>",
|
||||
},
|
||||
{
|
||||
question: "Где размещаются мои файлы?",
|
||||
answerHtml:
|
||||
"<p>После сборки файлы загружаются в Cloudflare R2 и отдаются через CDN-ссылку на выделенном домене проекта, чтобы получить быстрый и совместимый с Webflow результат.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что делать, если я опубликовал не те файлы?",
|
||||
answerHtml:
|
||||
"<p>В панели деплоя есть мгновенный откат. </p><p>Нажмите откат, верните предыдущую рабочую версию, исправьте локально и опубликуйте заново.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что такое ИИ-агент Webflow?",
|
||||
answerHtml:
|
||||
"<p>Помимо <strong>@plan</strong>, <strong>@build</strong> и <strong>@explore</strong> стандартных режимов агентов мы готовим <strong>@webflow</strong> режим, специально заточенный под Webflow: структуру проекта, взаимодействие с ним и эффективную разработку.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что такое собственная база знаний?",
|
||||
answerHtml:
|
||||
"<p>Основано на многолетнем опыте написания кастомного кода для Webflow-анимаций. </p><p>WebGL, Three.js, GSAP и кастомная функциональность собраны в набор практик, пайплайнов и модульную систему, чтобы всё работало согласованно. </p><p>Также доступна библиотека компонентов: выберите компонент, добавьте в проект и попросите ИИ адаптировать его под задачу.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как использовать это с Webflow?",
|
||||
answerHtml:
|
||||
"<p>При запуске <em>NODE.DC</em> доступна отдельная вкладка для встраивания. </p><p>В зависимости от задачи можно получить скрипты для конкретной страницы или единый app.js для всего проекта. </p><p>Сгенерированная ссылка содержит и продакшен-версию, следующую за URL деплоя, и локальную версию для предпросмотра без влияния на посетителей сайта.</p>",
|
||||
},
|
||||
{
|
||||
question: "Какие ИИ-модели доступны?",
|
||||
answerHtml:
|
||||
"<p>Мы используем основные доступные модели: <strong>OpenAI</strong> до <strong>Claude</strong> до <strong>Gemini</strong>. </p><p>Оркестратор выбирает модель под задачу автоматически, но вы можете вручную переключиться на нужную модель.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как это работает?",
|
||||
answerHtml:
|
||||
"<p>Во время работы NODE.DC выполняет две задачи параллельно.</p><p>Поднимает локальную среду разработки на Bun, синхронизирует JavaScript и TypeScript, включает HMR и держит webhook для автоматического обновления сайта при изменениях. </p><p>Одновременно работает оркестратор ИИ-агентов: обрабатывает запросы, меняет код, проверяет состояние сборки и при необходимости обновляет проект Webflow.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как работает публикация?",
|
||||
answerHtml:
|
||||
"<p>Процесс многоэтапный: сначала Bun собирает, сжимает, минифицирует и подготавливает проект вместе с библиотеками.</p><p>Затем файлы отправляются в CDN: <code>cdn.node.dc/{USERNAME}/{PROJECT-NAME}</code>. Ссылка уже содержит нужную структуру: вы видите локальную версию, а пользователи получают опубликованный скрипт.</p>",
|
||||
},
|
||||
{
|
||||
question: "Это работает с MCP?",
|
||||
answerHtml:
|
||||
"<p>Система изначально рассчитана на работу с Webflow MCP. </p><p>Подключите проект и авторизацию, чтобы ИИ-агенты получили доступ к Webflow-сборке. Подключение приложения в Webflow Designer даёт агентам доступ к контенту, элементам, стилям и CSS, а кастомный код получает полный контекст проекта.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.middle", "Заголовок: средняя строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
field("content.docsHref", "Документация: ссылка", "url", "attr"),
|
||||
field("content.docsLabel", "Документация: кнопка"),
|
||||
field("content.windowTitle", "Окно: название"),
|
||||
...Array.from({ length: 10 }, (_, index) => [
|
||||
field(`content.faq.${index}.question`, `FAQ ${index + 1}: вопрос`, "text", "text", "FAQ", {
|
||||
replaceAll: true,
|
||||
}),
|
||||
field(`content.faq.${index}.answerHtml`, `FAQ ${index + 1}: ответ`, "html", "html", "FAQ"),
|
||||
]).flat(),
|
||||
],
|
||||
},
|
||||
"publish-cta": {
|
||||
type: "publishCta",
|
||||
content: {
|
||||
cta: {
|
||||
heading: {
|
||||
muted: "Подключить.",
|
||||
middle: "Поставить задачу.",
|
||||
main: "Опубликовать.",
|
||||
},
|
||||
intro: "Единый путь от разработки до продакшена.",
|
||||
detailHtml:
|
||||
'<span class="darker-70">Настройка примерно в 12 кликов:</span> от скачивания приложения до запуска в проекте.',
|
||||
href: "index.html#hero",
|
||||
buttonLabel: "Встать в лист ожидания",
|
||||
},
|
||||
},
|
||||
editableFields: [
|
||||
field("content.cta.heading.muted", "CTA: приглушённая строка"),
|
||||
field("content.cta.heading.middle", "CTA: средняя строка"),
|
||||
field("content.cta.heading.main", "CTA: основная строка"),
|
||||
field("content.cta.intro", "CTA: вводный текст", "text", "text", "CTA"),
|
||||
field("content.cta.detailHtml", "CTA: пояснение", "html", "html", "CTA"),
|
||||
field("content.cta.href", "CTA: ссылка", "url", "attr", "CTA"),
|
||||
field("content.cta.buttonLabel", "CTA: кнопка", "text", "text", "CTA"),
|
||||
],
|
||||
},
|
||||
"pricing-table": {
|
||||
type: "pricingTable",
|
||||
content: {
|
||||
pricing: {
|
||||
eyebrow: "Тарифы",
|
||||
heading: {
|
||||
muted: "Код",
|
||||
middle: "для No-Code,",
|
||||
main: "усиленный ИИ.",
|
||||
},
|
||||
introHtml: "Подписки на приложение с<br/>включённым объёмом в каждом тарифе. <br/>",
|
||||
tokenBillingHtml: '<span class="darker-70">Токенная</span> оплата сверх лимитов.',
|
||||
founderLimitHtml: '<span class="darker-70">200 Founder</span> мест с фиксированной ценой на первый год.',
|
||||
windowTitle: "Тарифы.app",
|
||||
discountLabel: "Скидка 50%",
|
||||
betaHref: "beta-apply.html",
|
||||
founderHref: "founders.html",
|
||||
betaButtonLabel: "Подать заявку в бету",
|
||||
founderButtonLabel: "Оформить Founder-доступ",
|
||||
basicButtonLabel: "Выбрать Basic",
|
||||
advancedButtonLabel: "Выбрать Advanced",
|
||||
noteHtml: "(*) Условия могут меняться по ходу тестирования, но мы постараемся сохранить заявленные обещания.",
|
||||
enterpriseNoteHtml:
|
||||
'<a href="enterprise.html" class="darker-40">Enterprise</a> BYOK-тарифы будут доступны с сентября 2026.',
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
name: "Бета",
|
||||
priceHtml: "Бесплатно",
|
||||
period: "1 месяц",
|
||||
},
|
||||
{
|
||||
name: "Founder ",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>310',
|
||||
period: "/ год, навсегда",
|
||||
},
|
||||
{
|
||||
name: "Basic",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>20',
|
||||
period: "/ месяц",
|
||||
},
|
||||
{
|
||||
name: "Advanced",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>50',
|
||||
period: "/ месяц",
|
||||
},
|
||||
],
|
||||
},
|
||||
editableFields: [
|
||||
field("content.pricing.eyebrow", "Тарифы: надзаголовок"),
|
||||
field("content.pricing.heading.muted", "Тарифы: приглушённая строка"),
|
||||
field("content.pricing.heading.middle", "Тарифы: средняя строка"),
|
||||
field("content.pricing.heading.main", "Тарифы: основная строка"),
|
||||
field("content.pricing.introHtml", "Тарифы: вводный текст", "html"),
|
||||
field("content.pricing.tokenBillingHtml", "Тарифы: оплата сверх лимитов", "html"),
|
||||
field("content.pricing.founderLimitHtml", "Тарифы: Founder-лимит", "html"),
|
||||
field("content.pricing.windowTitle", "Окно: название"),
|
||||
field("content.pricing.discountLabel", "Founder: бейдж скидки"),
|
||||
field("content.plans.0.name", "Бета: название"),
|
||||
field("content.plans.0.priceHtml", "Бета: цена", "html"),
|
||||
field("content.plans.0.period", "Бета: период"),
|
||||
field("content.plans.1.name", "Founder: название"),
|
||||
field("content.plans.1.priceHtml", "Founder: цена", "html"),
|
||||
field("content.plans.1.period", "Founder: период"),
|
||||
field("content.plans.2.name", "Basic: название"),
|
||||
field("content.plans.2.priceHtml", "Basic: цена", "html"),
|
||||
field("content.plans.2.period", "Basic: период"),
|
||||
field("content.plans.3.name", "Advanced: название"),
|
||||
field("content.plans.3.priceHtml", "Advanced: цена", "html"),
|
||||
field("content.plans.3.period", "Advanced: период"),
|
||||
field("content.pricing.betaHref", "Бета: ссылка", "url", "attr"),
|
||||
field("content.pricing.founderHref", "Founder: ссылка", "url", "attr"),
|
||||
field("content.pricing.betaButtonLabel", "Бета: кнопка"),
|
||||
field("content.pricing.founderButtonLabel", "Founder: кнопка"),
|
||||
field("content.pricing.basicButtonLabel", "Basic: кнопка"),
|
||||
field("content.pricing.advancedButtonLabel", "Advanced: кнопка"),
|
||||
field("content.pricing.noteHtml", "Тарифы: примечание", "html"),
|
||||
field("content.pricing.enterpriseNoteHtml", "Тарифы: Enterprise-примечание", "html"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const staticElements = {
|
||||
@@ -479,7 +823,7 @@ function tokenizeStaticShell(source, definition) {
|
||||
tokenized = replaceShellLiteral(
|
||||
tokenized,
|
||||
"<script>\nwindow.__API_ORIGIN__=",
|
||||
`<script id="node-dc-static-config" type="application/json">{"assets":{"icon":"${shellToken(
|
||||
`\n<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__=`,
|
||||
|
||||
Reference in New Issue
Block a user