Как работает Конструктор замороженных шаблонов: путь от «сделай мне новости» до готового раздела
Подробный разбор простыми словами: что происходит от момента запроса пользователя до появления готового раздела на сайте — кто принимает запрос, какие инструменты вызываются, какие действия выполняются по шагам, как доставляется движок и дизайн-макет, насколько он гибкий и как его адаптировать под другие структуры. С реальными именами компонентов, функций, навыков и MCP.
“Раньше добавить раздел значило, что ИИ-агент вручную пишет тридцать файлов. Конструктор делает это за один вызов — собирая раздел из проверенных деталей, не генерируя ни строки кода. Эта статья — про то, как именно.”
Это подробный разбор для тех, кто хочет понять механизм до конца, а не на уровне «оно как-то само». Мы пройдём весь путь: от фразы пользователя «сделай мне раздел новостей» до готовых страниц на сайте — кто принимает запрос, какие инструменты вызываются, что они делают по шагам, как доставляется и из чего собран дизайн-макет, и как всё это адаптировать под другие задачи. По дороге будут настоящие имена файлов, функций, навыков и MCP — чтобы по этому документу можно было задавать точные вопросы и развивать проект дальше. Платформа, о которой речь, — платформа для агентной инженерии.
Кто участвует — карта компонентов
Сначала познакомимся с действующими лицами. В сборке участвует несколько частей, и у каждой — своя роль. Важная мысль наперёд: ни одна часть не генерирует код. Всё, что происходит, — это копирование проверенных файлов и подстановка значений (названия раздела, языков, метки). Поэтому результат одинаков у любой ИИ-модели.
| Компонент | Что это и зачем | Где живёт (реальное имя) |
|---|---|---|
| Гермес (или любой агент) | Мозг/оркестратор: принимает запрос, понимает намерение, выбирает инструмент, переспрашивает перед изменением. Может быть и одиночный кодящий агент — Гермес не обязателен. | процесс fractera-hermes; персона SOUL.md |
| Навык compose-frozen-template | Инструкция-сценарий «как собрать структуру»: по каким осям сопоставлять запрос, что подтвердить, что вызвать. Лежит во ВСЕХ шести агентах (самодостаточность). | .agents/skills/compose-frozen-template/SKILL.md |
| MCP-мост template-constructor-bridge | Канал, через который агент вызывает сборку как инструмент. Порт 3224. Два инструмента (ниже). | bridges/platforms/template-constructor-mcp-server.js |
| Data-сервис + закрытый стор | Хранит «базис» — проверенные детали (кирпичи) и движок. Отдаёт их только для чтения. Порт 3300. | services/data/server.js · стор services/data/frozen-templates/ |
| Композер (эмиттер) | Скрипт, который реально собирает раздел в проекте: копия файлов + подстановка токенов. Сердце механизма. | .agents/skills/compose-frozen-template/compose-frozen-template.mjs |
| Движок отрисовки (engine) | Общий «дизайн-макет» и логика, как страница выглядит и формирует SEO. Доставляется в проект один раз. | стор → engine/ → ставится в слот как lib/content-v1/ и components/content-page-v1/** |
| Инструмент пересборки | Делает изменения видимыми: та же кнопка «Deploy», что в подвале. Порт 3225. | bridges/platforms/deploy-mcp-server.js · owner_deploy_rebuild_slot |
Путь запроса — шаг за шагом
Теперь пройдём весь маршрут. Каждый шаг — это конкретное действие конкретного компонента.
Шаг 1. Запрос пользователя
Всё начинается с обычной фразы в чате: «сделай мне раздел новостей», «добавь блог», «хочу документацию». Пользователь не пишет код и не нажимает кнопок — только говорит, что хочет.
Шаг 2. Гермес понимает запрос и переспрашивает
Агент загружает навык compose-frozen-template и действует строго по нему. Перед любой генерацией он читает набор языков сайта из переменной NEXT_PUBLIC_SUPPORTED_LANGUAGES (это прививка против бага «сайт на языке, которого у него нет»). Затем, поскольку сборка изменяет проект, действует правило «переспроси перед изменением»: агент проговаривает обратно — «Если я правильно понимаю: собрать раздел формата news по адресу /news, языки en, ru, две страницы-заглушки. Делаю?» — и ждёт явного «да».
Шаг 3. Матчинг по «оболочке» (envelope)
Это ключевая идея конструктора. Каждая структура описывается несколькими независимыми осями. Запрос «проецируется» на эти оси, и подбирается примитив (кирпич), который подходит на 100% по каждой оси. Не подходит хотя бы по одной — это уже не тот кирпич, и будет честный отказ с указанием оси. Чтобы увидеть доступные кирпичи, агент вызывает инструмент owner_template_list_primitives (только чтение).
| Ось | Что значит | Значения сегодня | Слот |
|---|---|---|---|
| source (источник) | Откуда берётся список «детей» раздела | files (скан файлов) · db-at-build (запрос к БД на сборке) · runtime | A — поставщик списка |
| depth (глубина) | Сколько уровней вложенности (хаб → подхаб → …) | 1 (плоский список) · 2 · 3 · 4 | базовая |
| rendering (отрисовка) | Статика или «динамический дескриптор», когда статика невозможна | static · dynamic-descriptor | базовая |
| i18n (языки) | Многоязычность, одинаково на каждом уровне | mono · multi | B — единообразный аспект |
| roles (доступ) | Кому виден раздел; гейт встраивается одинаково на каждый уровень | public · guest · список ролей | B — единообразный аспект |
Сегодня в базисе один готовый кирпич — `files-depth1`: плоский список документов из файлов, многоязычный (новости / блог / лента документации). Остальные клетки сетки — в дорожной карте, их «заморозят», только когда они докажут себя живой разработкой.
Шаг 4. Предпросмотр (dry_run)
Перед реальной записью агент вызывает инструмент сборки с флагом `dry_run: true`. Это возвращает предпросмотр — что именно будет создано (адрес раздела, число страниц, языки, метка, формат), без единой записи на диск. Агент показывает это пользователю и получает подтверждение.
Шаг 5. Вызов инструмента сборки
После «да» агент вызывает мутирующий инструмент owner_template_compose_structure (MCP-мост на порту 3224). Этот инструмент сам ничего не «придумывает» — он оркеструет:
- тянет «базис» из закрытого стора: GET http://127.0.0.1:3300/frozen-templates/tree — весь набор файлов как карта «путь → содержимое»;
- распаковывает этот набор во временную папку на сервере;
- запускает композер: spawn node compose-frozen-template.mjs --store <временная папка> --out <папка слота> --tab news --format news --languages en,ru --label-en News --label-ru Новости --samples 2 …;
- разбирает вывод композера (успех или честный отказ по оси) и считает публичные адреса страниц (view_urls), учитывая режим: secure → https://домен/язык/раздел, IP-режим → http://ip:3000/...;
- возвращает результат и напоминание: теперь нужна пересборка слота.
Шаг 6. Что делает композер (эмиттер) — по порядку
Скрипт compose-frozen-template.mjs — детерминированный сборщик. Он выполняет строго такую последовательность (имена функций — настоящие):
- МАТЧ. Находит в registry.json примитив, у которого базовые оси (source, depth, rendering) РАВНЫ запросу; если нет — refuse() с указанием оси (выход 2). Проверяет, что нужные аспекты (i18n, roles) этот кирпич поддерживает.
- УСТАНОВКА ДВИЖКА — installEngine(). Копирует движок в слот «если файла ещё нет» (copy-if-absent). Версионируемую часть кладёт в неймспейс lib/content-v1/ и components/content-page-v1/ (versionizeContent / versionizePath); личность (brand, author, languages) НЕ версионирует — она одна на сайт.
- СБОРКА ВКЛАДКИ — composeTab(). Копирует шаблон вкладки из primitives/files-depth1/tab/** в app/[lang]/<раздел>/, подставляя токены ({{TAB}}, {{TAB_PASCAL}}, {{FORMAT}}, {{LABEL}} и т.д.).
- ШОВ A (источник списка). Копирует providers/files/list-provider.ts в _lib вкладки — это «откуда берётся список». Сменить files на БД = поменять только этот шов.
- ШОВ B (аспекты). Если включены роли — оборачивает содержимое макета в <RouteGuard …> через токены {{ASPECT_OPEN}}/{{ASPECT_CLOSE}} в layout.tsx, одинаково на каждом уровне.
- ДАННЫЕ И ЯЗЫКИ. Пишет _data/en.ts (полная база + метка), на каждый дополнительный язык — частичный override _data/<lang>.ts (перевод только метки), и _data/index.ts со сборкой языков (per-key fallback на английский).
- СТРАНИЦЫ-ЗАГЛУШКИ. Из шаблона __ITEM__ печёт N документов (sample-1, sample-2 …): Lorem-текст + картинка-заглушка, но с идеальной структурой (SEO-метаданные, хлебные крошки, оглавление, FAQ).
- СПИСОК И РЕГИСТРАЦИЯ. Пишет _list.generated.ts напрямую; registerCollection() добавляет вкладку в lib/parser-fs.mjs (COLLECTIONS); patchPackageJson() прописывает скрипты gen:lists / predev / prebuild; appendSitemap() напоминает добавить путь в sitemap.
Шаг 7. Пересборка — иначе изменения не видны
Слот работает в продакшен-режиме: только что записанные файлы НЕ видны, пока проект не пересобран. Поэтому финальный шаг — вызвать инструмент owner_deploy_rebuild_slot (MCP-мост деплоя, порт 3225). Это ровно та же «Deploy», что и кнопка в подвале сайта: пересборка (next build) + перезапуск процесса (pm2 reload) + проверка здоровья. Никаких секретов в чат — это делает сам инструмент или кнопка владельца.
Шаг 8. Результат
После успешной пересборки агент сообщает реальные публичные адреса из view_urls / site_url (например, «Готово — раздел новостей доступен по https://ваш-домен/ru/news»). Дальше владелец просто заменяет текст-заглушку и картинки на свои, а каждая новая запись — это одна новая папка.
Движок и дизайн-макет: что именно доставляется
Самая частая путаница — «а где же дизайн». Дизайн живёт в движке (engine) — это общий слой отрисовки, который доставляется в проект один раз (copy-if-absent) и переиспользуется всеми разделами. Он версионируется «бок о бок»: новый мажор кладётся РЯДОМ (lib/content-v2), не перетирая старый, а каждый раздел «прибит» к своей версии — поэтому старые новости не сломаются, когда приедет новый движок. Вот из чего состоит движок:
| Файл движка | За что отвечает |
|---|---|
| components/content-page/standard-content-page.tsx | Хром страницы — общий каркас/макет: заголовок, хлебные крошки, оглавление, FAQ, блок основателя, ссылки. |
| components/content-page/post-body.tsx | Рисует тело статьи из «блоков» (см. ниже). |
| lib/content/blocks/{types,registry,inline}.tsx | Словарь блоков (типы) + сопоставление блок→рендерер + разметка внутри текста (жирный, ссылки). |
| lib/content/create-content-post.tsx | Фабрика страницы: по формату (news/blog/document) выбирает тип JSON-LD и тип автора, собирает метаданные, OpenGraph и три блока структурированных данных, и рендерит через StandardContentPage. |
| lib/parser-fs.mjs | Шов A для files: сканирует файлы на сборке и строит список «детей» раздела. |
| lib/auth-guard/route-guard.client.tsx | Аспект ролей: гейт доступа, который сохраняет статичность страницы; на чужой роли показывает локализованный тост «доступ запрещён». |
| lib/{brand,author,languages}.ts · lib/seo/alternates.ts | Личность сайта: имя бренда, автор, набор языков, SEO-альтернативы. Читается из переменных окружения → переносимо в любой стартер. НЕ версионируется. |
| app/[lang]/error.tsx · app/global-error.tsx | Границы ошибок (прививка против падений): любой сбой в дереве [lang] деградирует мягко, а не «голым 500». |
Из чего собран дизайн — словарь блоков
Дизайн страницы — данные, а не код. Автор (человек или агент) описывает страницу набором «блоков», а движок их рисует. Это и есть гибкость макета: чтобы изменить страницу, меняют данные блоков, а не вёрстку. Вот основные блоки (полный список — в lib/content/blocks/types.ts):
| Блок (kind) | Что рисует |
|---|---|
| p · h2 · h3 | Абзац и заголовки разделов. |
| list · olist | Маркированный и нумерованный списки. |
| quote · note · callout | Цитата, короткая ремарка, врезка-«а вы знали». |
| table | Сравнительная таблица, статическая, работает без JS (последний столбец выделен). |
| figure · code · cta | Картинка/видео с подписью, блок кода, кнопка-ссылка. |
| founder | Карточка основателя (фото, имя, роль, соцссылки) — фирменная врезка бренда. |
| docref | Карточка-ссылка на полный документ с кнопкой скачивания. |
| columns · group | Контейнеры: колонки и группировка. Внутрь вкладывается ЛЮБОЙ блок (включая другой контейнер) — рекурсивно. |
Отдельно работают пресеты формата (параметр format): news, blog, document — это один и тот же движок, но с разным типом структурированных данных (NewsArticle / BlogPosting / TechArticle) и типом автора (человек / организация). Поэтому «новость», «пост блога» и «страница документации» выглядят семейно, но корректно размечены каждый под свой смысл.
Насколько гибкий макет и как его настроить или адаптировать
Главный вопрос: можно ли это гнуть под другие шаблоны. Да — и есть несколько уровней настройки, от простого к глубокому:
- Содержимое и порядок страницы — меняются данными блоков (добавить/убрать/переставить блоки), без касания кода.
- Личность и стиль бренда — имя, автор, языки, SEO — берутся из переменных окружения (lib/brand, lib/author, lib/languages), поэтому один движок выглядит «своим» на любом сайте.
- Новый тип секции (например, особый вид карточки) — это добавить член в lib/content/blocks/types.ts + рендерер в registry.tsx; существующие страницы не трогаются.
- Изменить сам макет — правят движок В СТОРЕ (единый источник), а не копию; версионирование «бок о бок» гарантирует, что уже собранные разделы не сломаются.
- Адаптировать под ДРУГУЮ структуру (документация по категориям — глубина 2; каталог из БД — источник db-at-build) — это НЕ переписывание ядра, а «заморозка нового кирпича»: новый шаблон вкладки и/или новый поставщик списка. Ядро конструктора закрыто для правок и открыто для расширения (Open/Closed).
Не хватает элементов дизайна? Как добавить новый блок
Реальный сценарий: вы оформляете страницу и понимаете — нужного элемента в словаре блоков нет (например, аккордеон, галерея-сетка, таймлайн). Вот что делать по порядку.
Если нужного вида действительно нет — добавить новый блок это ровно два файла (так и написано в самом каталоге: «add a member here + a renderer in the registry, nothing else»):
- ТИП. В lib/content/blocks/types.ts добавить член в union LeafBlock (простой блок) или ContainerBlock (если внутри будут другие блоки) — например { kind: 'gallery'; images: { src: string; alt: string }[] }.
- РЕНДЕРЕР. В lib/content/blocks/registry.tsx добавить ОДНУ запись в карту BLOCK_RENDERERS по этому kind — функцию, возвращающую JSX. Текст внутри полей пропускают через inline() из ./inline.tsx, чтобы работали жирный и ссылки. Контейнер рендерит детей рекурсивно через ctx.renderBlocks — поэтому внутрь можно вложить любой блок.
- Всё. Больше ничего менять не нужно: PostBody — тонкий диспетчер над этой картой; страницы и фабрика create-content-post подхватят новый блок автоматически.
Где именно добавлять — зависит от того, кому блок нужен:
| Куда добавить | Кто получит блок |
|---|---|
| FES — lib/content/blocks/ (витрина) | Только маркетинг-сайт самой платформы (его новости/блог/документация). |
| Движок в сторе — services/data/frozen-templates/engine/lib/content/blocks/ | ВСЕ будущие собранные разделы в стартерах: composer доставит обновлённый движок в каждый слот (с учётом версионирования). |
А если «не хватает» — это не один блок, а целый вид структуры (интерактивный калькулятор, лента на пользователя, дашборд) — это уже не уровень блока. Граница простая: блок — это кусок ОДНОЙ страницы; примитив — это целая СТРУКТУРА из страниц. Если это всё ещё композиция статичных частей — делаете контейнер-блок; если нужна другая топология, источник данных или динамика — это новый примитив конструктора (харвест кирпича) или классическая разработка.
Кто это делает: добавление блока — обычная разработка (агент или человек). Если возможность запрашивают через консультанта/Гермес и её ещё нет — агент честно об этом скажет и предложит завести её (черновик через propose-new-agent-skill-or-mcp или новый шаг), а не выдумает на лету. Запись файлов в защищённом режиме выполняет роль architect.
Как замороженный шаблон работает с SEO-данными
SEO-данные (теги `<title>`/`description`, `hreflang`, canonical, OpenGraph, Twitter-карточка, структурированные данные JSON-LD) не пишутся руками и не дублируются в каждой странице. Они рождаются на сборке из типизированных данных страницы через специальную функцию `generateMetadata`. Это критично для мультиязычных страниц: без корректного `hreflang` Google считает языковые версии конкурирующими дублями. Разберём по атрибутам — отдельно для страницы-поста и страницы-роутера, с именами компонентов.
Откуда берутся данные — типизированная сущность поста
Каждый пост — это co-located папка с данными `_data/{meta,en,ru,index}.ts`. Резолвер `{{tab}}Post(data, lang)` (в `_lib/post.ts`) собирает из них типизированную сущность `ContentPost` под нужный язык: `title`, `seoTitle`, `description`, `keywords`, `tags`, `date`, `readingMinutes`, `ogImage`/`heroImage`, `blocks`, `faq`, `inLanguage`. Именно эта типизированная сущность на сборке превращается в мета-записи — то самое, о чём была гипотеза.
Страница-пост (/blog/post123) — функция generateMetadata фабрики
Тонкий `page.tsx` реэкспортит `generateMetadata` из `_components/index.tsx`; тот вызывает фабрику `createContentPost({ format, subPath, resolve, chrome, titleSuffix })` (компонент `lib/content/create-content-post.tsx`), которая и возвращает `generateMetadata`. На сборке, для каждого языка, она строит из `resolve(lang)` такие атрибуты:
| Атрибут (Next Metadata) | Откуда / что | Компонент |
|---|---|---|
| title | `${seoTitle} | ${titleSuffix(lang)}` → `<title>`/og:title | create-content-post |
| description | из `ContentPost.description` | create-content-post |
| keywords | если задан в данных поста | create-content-post |
| alternates | `buildAlternates(lang, subPath)` → canonical + hreflang | lib/seo/alternates |
| robots | index: true, follow: true | create-content-post |
| openGraph | title/description/url/type=article/publishedTime/images | create-content-post |
| summary_large_image + title/description/images | create-content-post |
Картинку сниппета выбирает `snippetImage()`: берёт `ogImage`, иначе видимый `heroImage`, и делает URL абсолютным через `abs()` (`BRAND.siteUrl`). Правило: если у поста есть любая картинка — она обязана попасть в сниппет.
Структурированные данные (JSON-LD) — в самом компоненте страницы
Помимо мета-тегов, функция `Page` той же фабрики печатает `<script type="application/ld+json">` с тремя блоками: (1) Article — тип по формату (`NewsArticle`/`BlogPosting`/`TechArticle` через карту `JSONLD_TYPE`), с `headline`/`description`/`datePublished`/`inLanguage`/`author`/`publisher`/`image`/`keywords`; автор — `Person`+`sameAs` или `Organization` (по карте `AUTHOR_KIND`); (2) BreadcrumbList (хлебные крошки); (3) FAQPage — если у поста есть `faq`. Тип статьи и тип автора — единственная реальная разница между форматами news/blog/document; всё остальное общее.
Страница-роутер (/blog) — своя generateMetadata
У индекс-страницы раздела — своя `generateMetadata` в её `_components/index.tsx`: `title`/`description` берутся из типизированного `get{{TAB_PASCAL}}Ui(lang)` (UI-хром вкладки), `alternates: buildAlternates(lang, "/{{TAB}}")`, и собственные `openGraph`/`twitter` (заголовок/описание раздела + лого бренда как картинка, тот же origin `BRAND.siteUrl`) — чтобы при шеринге `/{{TAB}}` карточка была «раздельной», а не сайтовой. В теле страницы печатается `BreadcrumbList` JSON-LD.
hreflang и canonical — компонент buildAlternates
Ключевой SEO-компонент — `buildAlternates(lang, subPath)` (`lib/seo/alternates.ts`). Он возвращает: canonical = сама страница (self-canonical, а не кросс-канонический на английский — это и есть защита от схлопывания версий), и `languages` = `x-default` + все языки слота абсолютными URL. `subPath` передаёт вызывающая страница: пост — `/{{TAB}}/<slug>`, роутер — `/{{TAB}}`, поэтому каждая страница указывает на свой реальный URL. Набор языков — из единого авторитета (`lib/languages` ← `NEXT_PUBLIC_SUPPORTED_LANGUAGES`).
Почему hreflang не «повисает» — generateStaticParams по языкам
Чтобы `hreflang`-ссылки указывали на реально существующие страницы, все языки должны быть пререндерены. За это отвечает `app/[lang]/layout.tsx` слота: `generateStaticParams()` = `SUPPORTED_LANGUAGES.map(lang => ({ lang }))` — перечисляет все языки из того же единого источника. Так как это на уровне `[lang]`-layout, оно покрывает весь поддерев, включая собранные табы. `revalidate` даёт ISR; язык не из набора → `notFound()`. Значит рекламируются (через hreflang) ровно те языки, что и пререндерятся.
Закон двух слотов — почему это масштабируется
Чтобы сборка не превратилась в «тысячу шаблонов под каждую комбинацию», действует строгий закон: любое свойство структуры лежит РОВНО в одном из двух слотов и они НЕ взаимодействуют между собой.
| Слот | Что держит | Примеры |
|---|---|---|
| A — поставщик списка (данные) | Откуда берутся «дети» раздела | скан файлов · запрос к БД на сборке · (в будущем) рантайм |
| B — единообразный аспект (макет) | Сквозное правило, встроенное ОДИНАКОВО на каждом уровне | языки (i18n) · роли/доступ · (в будущем) тема |
Смысл закона: стоимость растёт аддитивно, а не как произведение всех комбинаций. Если фиче нужен особый случай «по глубине» или «по источнику» — это запах плохого дизайна, и его отвергают, а не подгоняют конструктор под него.
Честный отказ — когда конструктор говорит «нет»
Если запрос не ложится на готовый кирпич на 100% — конструктор не подгоняет плохой вариант и не сочиняет код. Он честно называет ось, по которой не сошлось, и предлагает один из двух путей: (а) предложить архитектору заморозить новый кирпич — но только если форма проверена живой разработкой и повторяется; (б) классическая разработка в рамках архитектуры. Примеры:
- «документация по категориям» → глубина 2 → отказ по оси depth (кирпича глубины 2 пока нет) → предложить харвест/классику;
- «каталог товаров из базы» → источник db-at-build → отказ по оси source (поставщик объявлен, но ещё не заморожен);
- «живой дашборд» → данные на пользователя/запрос → отказ по оси rendering/source → классическая разработка;
- «новости, но только для залогиненных» → files, глубина 1 + аспект roles → СОБИРАЕТСЯ (files-depth1 с --roles user, гейт встраивается единообразно).
Что это даёт и куда развивать
Итог: «добавь раздел на сайт» превращается из аккуратной задачи по коду в просьбу, которую может сделать кто угодно, а результат одинаков каждый раз, на собственном сервере владельца. Дальше базис растёт по одному проверенному кирпичу (документация по категориям, каталог из БД на сборке, динамический дескриптор) — каждый въезжает, когда докажет себя. Эволюция этой идеи из первого прототипа подробно описана в заметке Конструктор замороженных шаблонов, а смежная тема многоязычного контента — в архитектуре мультиязычного контента.
Продолжение: не только создать раздел, но и изменить и удалить — тоже без кода
Собрать раздел — это полдела. Дальше им нужно жить: добавлять статьи, править тексты и переводы, убирать устаревшее, переименовывать саму секцию. Раньше это снова означало ручную правку файлов агентом — тот самый «код руками», от которого мы уходим. Теперь весь жизненный цикл контента закрыт одним инструментом — owner_content_manage_collection (MCP-мост content-crud-bridge, порт 3226). Принцип тот же, что у конструктора: агент описывает ДАННЫЕ — инструмент пишет файлы, без генерации кода, одинаково у любой модели. Описать содержимое статьи — это не программирование; собирать из этого файлы — работа инструмента.
Одна операция параметризуется двумя осями: что делаем (создать / изменить / удалить) и над чем (группа-вкладка / страница-пост). Шесть сочетаний — один инструмент:
| Операция × цель | Что происходит | Что передаёт агент |
|---|---|---|
| создать группу | Заводит новую вкладку (делегирует Конструктору замороженных шаблонов выше) | имя, формат, языки, метки |
| добавить страницу | Новая папка поста из вашего контента; список постов пересобирается сам | данные: заголовок, блоки, FAQ + переводы |
| изменить страницу | Переписывает содержимое поста (каркас маршрута не трогает) | новые данные поста |
| изменить группу | Меняет «хром» вкладки: заголовок, хлебные крошки, метки | локализованные UI-строки |
| удалить страницу | Убирает папку поста; список пересобирается | адрес поста |
| удалить группу | Убирает всю вкладку и её строку регистрации | имя вкладки |
Чтобы добавленная страница не оказалась бракованной (это реальный класс ошибок, на котором уже спотыкались), перед записью срабатывают гейты целостности — и если что-то нарушено, операция честно отклоняется, а не выпускает поломку наружу:
- имя папки строго равно адресу (slug) — иначе ссылка в списке ведёт на несуществующую страницу (404);
- язык-перевод только из набора сайта (английский + настроенные) — нельзя протащить язык, которого на сайте нет (например, испанский вместо русского); нужен новый — сначала добавьте его в настройках и пересоберите;
- скан на «инородные символы» (иероглифы, арабица и т.п.), случайно попавшие в русский или английский текст — частый артефакт модели;
- карточка основателя — только последним блоком (это подпись бренда, а не заметка по тексту);
- обязательная ссылка-якорь на корень сайта внутри текста;
- жёсткий гейт неподставленных токенов — как и у конструктора.
И ещё одно: каждая операция по завершении фиксируется в специальной таблице Deployment (deployment_records) — что изменено, кем, какой страницы касается. Это журнал развёртываний, видимый в админке; он — структурная часть конвейера, а не то, что агент должен «не забыть» сделать.
Почему агенту «нельзя программировать» — и что это значит на самом деле
У оркестратора (Гермес) и у любого агента есть жёсткое правило: они не программируют. Важно понять его правильно — это не значит «нельзя менять сайт». Менять сайт можно и нужно, это и есть работа. Значит оно вот что: любое изменение делается вызовом инструмента, а не правкой файлов руками. Контент — через owner_content_manage_collection; целый раздел — через Конструктор; настройки — через инструмент настроек; «сделать видимым» — через пересборку. Нет подходящего инструмента — агент не пишет код наугад, а делегирует кодящему агенту или предлагает завести новую способность. Так даже самая слабая модель не «застывает» в духе «я не умею писать код», а уверенно действует через инструменты.
Всё это — самодостаточно: и навык manage-content-collections, и сам инструмент доставлены во ВСЕ шесть агентов, поэтому проект с единственной моделью (без Гермеса и без памяти) тоже умеет полный цикл. Это первый из восьми сценариев работы с контентом (четыре по файлам, четыре по базе данных) — и шаблон для остальных.
Я хочу, чтобы собирать сайт было как разговаривать, а не как собирать мебель. Конструктор — это место, где вы говорите, чего хотите, и оно появляется: правильное, ваше и на вашей машине. А этот документ — чтобы вы видели, как именно, и могли вести проект дальше осознанно.
Roma ArmstrongFounder at Fractera.aiЧастые вопросы
- ИИ-модель пишет код для нового раздела?
- Нет. Композер copy-frozen-template.mjs только копирует проверенные файлы и подставляет значения (название раздела, языки, метку). Кода не генерируется — поэтому любая из шести моделей даёт одинаковый результат.
- Где физически лежат шаблоны и движок?
- В закрытом сторе data-сервиса: services/data/frozen-templates/ (registry.json + engine/ + primitives/ + providers/ + aspects/). Он отдаётся только для чтения по адресу :3300/frozen-templates/tree. В стартере FNS лежит только навык и композер, самих шаблонов там нет.
- Почему после сборки я сразу не вижу раздел?
- Слот работает в продакшен-режиме: новые файлы видны только после пересборки. Её делает инструмент owner_deploy_rebuild_slot (или кнопка «Deploy» в подвале) — это next build + перезапуск + проверка здоровья.
- Насколько можно менять дизайн?
- Сильно. Содержимое страницы — это данные из блоков (меняются без кода). Новый тип блока добавляется в lib/content/blocks/types.ts + registry.tsx. Сам макет правится в движке (в сторе), а версионирование «бок о бок» не даёт сломать уже собранные разделы.
- Как сделать раздел другой структуры — например, документацию по категориям?
- Это другая клетка сетки (глубина 2). Сегодня готов один кирпич — files-depth1 (плоский список). Новую структуру «замораживают» отдельным кирпичом, когда форма проверена живой разработкой и повторяется; до тех пор — классическая разработка. Ядро конструктора при этом не переписывается.
- Как замороженные страницы не попадают под санкции Google за дубли языковых версий?
- Каждая страница на сборке через generateMetadata получает hreflang (все языки) и self-canonical (сама на себя) из компонента buildAlternates, а generateStaticParams в [lang]/layout пререндерит все языки из того же источника (NEXT_PUBLIC_SUPPORTED_LANGUAGES). Поэтому Google видит легитимные переводы одной страницы, а не конкурирующие дубли, и hreflang не указывает на несуществующую (404) версию.
- Что делать, если для оформления не хватает готового блока?
- Сначала попробуйте собрать нужное из существующих блоков и контейнеров (columns/group вкладываются рекурсивно). Если вида действительно нет — добавьте новый блок двумя файлами: член типа в lib/content/blocks/types.ts + рендерер в lib/content/blocks/registry.tsx. Чтобы блок стал доступен всем будущим разделам стартеров, добавьте его в движок в сторе (services/data/frozen-templates/engine). Это аддитивно и не ломает существующие страницы.
- Новые страницы работают без JavaScript?
- Да. Раздел статический и полностью читается с выключенным JavaScript. Даже аспект ролей сделан так, чтобы сохранить статичность.