Тема
Фаза 0 — Контракт темы
Статус: спека фазы 0. Что такое тема, как она разрешает шаблоны, откуда берёт токены и как принимает дизайн из claude.ai/design без потери SEO. Стек темы (Blade + Tailwind 4 + Vite + острова) — канонический стек, зона 1. Опирается на расширяемость → каскад тем и глоссарий → тема.
Что такое тема
Тема — приватный composer-пакет визуального слоя: Blade-переопределения блоков/виджетов
theme.json(дизайн-токены) + Vite-сборка (+ React-острова). Определяет, как выглядят блоки, не меняя их данные. Темы переключаются — контент остаётся. Смена темы = смена пакета, а не правка контента.
Тема не содержит контентной логики, не создаёт таблиц, не регистрирует роутов бизнес- уровня. Её единственная ответственность — представление. Всё поведение — в ядре/модулях.
Структура пакета темы
cms-theme-corporate/
├── composer.json # extra.cms.theme (манифест темы)
├── theme.json # дизайн-токены (единственный источник вида)
├── resources/
│ ├── views/
│ │ ├── blocks/ # переопределения блоков (hero.blade.php, …)
│ │ ├── widgets/ # переопределения виджетов
│ │ ├── layouts/ # app.blade.php (header/footer/meta)
│ │ └── components/ # компоненты темы (кнопка, карточка)
│ ├── css/ # входной Tailwind (@import токенов из theme.json)
│ ├── js/ # entry Vite + корни React-островов
│ └── islands/ # React-компоненты островов (если есть)
├── content/ # переносимый демо-контент темы (страницы-примеры)
├── public/ # скомпилированные ассеты (Vite build)
└── vite.config.js # сборка ассетов темыМанифест темы
jsonc
// composer.json → extra.cms.theme
{
"extra": {
"cms": {
"theme": {
"name": "corporate",
"parent": null, // родительская тема (наследование каскадом)
"minimum_core_version": "2.0.0",
"supports_blocks": ["*"], // какие типы блоков тема умеет рисовать
"areas": ["header", "sidebar", "footer"], // области виджетов, которые тема рендерит
"tailwind": 4,
"has_islands": true, // есть ли React-острова
"demo_content": "content/", // папка переносимого демо-контента
"csp_sources": { // внешние источники темы для CSP ядра
"font-src": ["https://fonts.gstatic.com"] // (ревизия ядра №2: фильтр security.csp)
}
}
}
}
}supports_blocks — честная декларация: если блок нового модуля не покрыт темой, ядро рисует его нейтральным fallback ядра (каскад ниже), а не ломает страницу.
areas — декларация областей виджетов (WidgetRegistry): ядро валидирует, что виджет размещают только в области, которую тема реально рендерит (<x-cms-area>). Смена темы на тему без области — виджеты области не рендерятся (деградация, не падение), админка предупреждает.
Каскад разрешения шаблона
Blade-шаблон блока/виджета/страницы резолвится по цепочке приоритетов через множественные view-namespace:
проект (app/Local) → активная тема → родительская тема → нейтральный fallback ядра- проект (
app/Local/views/) — точечная кастомизация конкретного сайта, переживает обновление темы; - активная тема — основной вид;
- родительская тема — child-theme наследует родителя, переопределяя только нужное (WordPress child themes / October);
- fallback ядра — нейтральный шаблон блока: гарантия, что новый блок отрисуется хоть как-то, даже если тема про него не знает.
Гарантия: рендер никогда не падает из-за отсутствия шаблона в теме — цепочка всегда заканчивается fallback'ом ядра. Согласуется с barrier-рендером блоков.
Template suggestions (специфичность)
В духе Drupal — от специфичного к общему, ядро пробует по очереди:
blocks/hero--variant-accent.blade.php → вариант оформления (color-variant поля)
blocks/hero--page-home.blade.php → контекст страницы
blocks/hero.blade.php → базовый шаблон блока
(fallback ядра)Тема реализует ровно те suggestions, что нужны; остальное падает на базовый шаблон. Даёт тонкую настройку вида без разрастания условий в одном шаблоне.
Дизайн-токены: theme.json → Tailwind + CSS-переменные
theme.json — единственный источник вида (цвета, шрифты, радиусы, отступы-шкала, тени). Из него генерируется:
theme.json
├── CSS custom properties (:root { --color-accent: … }) → рантайм-переключение
├── Tailwind-пресет (theme.extend) → классы садятся из claude.ai/design
└── общий конфиг для трёх зон → тот же токен в Blade/Filament-акценте/React-кабинетеjsonc
// theme.json (фрагмент)
{
"colors": { "accent": "#0a5", "ink": "#111", "surface": "#fff" },
"fonts": { "sans": "Inter, sans-serif", "head": "Manrope, sans-serif" },
"radius": { "sm": "4px", "md": "8px", "lg": "16px" },
"spacing": { "section": "clamp(3rem, 6vw, 6rem)" }
}Стили блока — только здесь, не в схеме блока (Q9): контент-менеджер не задаёт цвет/шрифт/отступ, их задаёт тема через токены. Блок может выбрать вариант оформления (color-variant поле → light/dark/accent), но сами варианты рисует дизайнер в теме. Это и есть техническая реализация «средней свободы».
Приём дизайна из claude.ai/design
Выход дизайна — React + Tailwind. На публичной части (зона 1) React в рантайме не нужен — точность достигается иначе (фронт-архитектура):
| Что | Конвейер |
|---|---|
| Статический блок | JSX → Blade-компонент: разметка и Tailwind-классы 1:1 (механически, задача для Claude Code) |
| Интерактивный блок | корень React-острова без переделки (React 19) |
| Токены | JSX-Tailwind-конфиг → общий theme.json → пресет |
| Сверка | галерея блоков /_gallery: каждый блок на демо-данных, скриншот-сверка с макетом |
Правила посадки:
- конвертировать JSX→Blade сразу, не «временно оставим React для статики» (иначе статический блок тащит гидрацию зря);
- просить дизайн поблочно (hero, карточка, форма), не страницей — чище ложится;
- иконки — Blade-icons/SVG-компоненты, не инлайн-копипаста (blade spec);
- вид определяется классами, а не тем, Blade это рендерит или React — потому Blade даёт ту же точность без рантайма.
React-острова темы
Тема поставляет корни островов в resources/islands/. Правила (канонический стек):
- остров — только блоку, которому нужно состояние (калькулятор, живой фильтр, чат);
- табы/аккордеоны/анимации — CSS/Alpine, не React;
- 3+ острова на странице, знающих друг о друге → это уже приложение → зона 3 (Inertia);
- остров получает server-rendered HTML + точечную гидрацию (блок с
interactive(true)в BlockRegistry); без флага React не подключается.
Демо-контент темы
Тема привозит стартовый контент, чтобы клиент не начинал с белого листа (модель данных):
content/— переносимый формат (YAML/JSON на сущность), демо-страницы/виджеты/настройки;- при активации темы —
cms:content:importизcontent/, идемпотентно, по стабильным ключам (slug/key), не по id; - демо несёт
_vблоков — импорт применяет data-миграцию до актуальной версии.
Переключение и совместимость
- смена активной темы (
cms:theme:use corporate) меняет только слой представления; контент и данные не трогаются; - тема декларирует
minimum_core_versionиsupports_blocks— preflight предупреждает, если тема не покрывает используемые блоки или требует более новое ядро; - обновление темы — через центр обновлений как обычный пакет;
app/Local-переопределения проекта переживают обновление темы (верх каскада).
Управление темами из админки
Страница «Темы» в Filament (контент-менеджеру и студии, по правам):
- галерея установленных тем: скриншот (из
demo_content/манифеста), версия, статус совместимости (supports_blocks/areas против фактически используемых); - превью до переключения — подписанная токен-ссылка рендерит сайт в кандидат-теме только для держателя ссылки (тот же механизм, что preview черновика), прод не затронут;
- «Активировать» — та же транзакция, что
cms:theme:use: preflight (блоки/области/ версия ядра) →view:clear→ пересборка манифеста резолва (ниже) → инвалидация page-cache → событиеThemeChanged(прогрев, CDN-purge слушают его); - установка новой темы — через центр обновлений (composer-пакет из Satis), не upload zip: тот же пайплайн preflight/бэкап/откат, что у модуля.
Кеш резолва шаблонов
Каскад проект → тема → parent → fallback ядра не сканирует файловую систему на каждый рендер: при активации темы/деплое компилируется манифест резолва — карта «view-имя (+suggestion) → конкретный путь». Рендер блока = один lookup по карте.
- манифест пересобирается на
cms:theme:use, деплое и обновлении темы; - в
APP_DEBUGрезолв живой (правки шаблонов видны сразу), в проде — только манифест; cms:theme:resolve {view}— отладка: показывает всю цепочку кандидатов и кто выиграл (аналогcms:cache:inspectиз производительности);- это же «безопасно»: тема обращается к блокам/виджетам только через каскад и props — прямых вызовов моделей/БД в шаблонах нет (контрактный тест темы).
Контрактные тесты темы (cms-testing)
- манифест валиден;
parent(если есть) разрешим; - для каждого блока из
supports_blocksшаблон резолвится (или явно падает на fallback); theme.jsonвалиден → генерирует корректный Tailwind-пресет + CSS-переменные;- Vite-сборка проходит; острова (если
has_islands) собираются; - демо-контент
content/импортируется идемпотентно по стабильным ключам; - вывод шаблонов — только
;{!! !!}вне whitelist SEO-полей — красный тест; - каждая область из
areasприсутствует в layout (<x-cms-area>); демо-контент не ссылается на необъявленные области.
Чеклист фазы 0 (контракт темы)
- [ ] Структура пакета темы + манифест
extra.cms.theme(включаяareas). - [ ] Каскад разрешения шаблона (проект→тема→parent→fallback ядра) + гарантия fallback.
- [ ] Template suggestions (специфичность).
- [ ]
theme.json→ Tailwind-пресет + CSS-переменные + общий конфиг трёх зон. - [ ] Стили только в теме, не в схеме блока (Q9); варианты через
color-variant. - [ ] Конвейер JSX→Blade + галерея блоков для сверки.
- [ ] React-острова темы с дисциплиной островов.
- [ ] Демо-контент
content/— переносимый формат, идемпотентный импорт. - [ ] Страница «Темы» в админке: галерея, превью по токену, активация с preflight.
- [ ] Манифест резолва шаблонов (компиляция каскада) +
cms:theme:resolveдля отладки. - [ ] Контрактные тесты темы.
Связи
- Канонический стек, зона 1 — версии Blade/Tailwind/Vite/React.
- Фронт-архитектура — почему Blade, а не React-темы; посадка дизайна.
- Расширяемость → каскад тем, глоссарий → тема.
- Контракт блока —
view(),interactive, варианты. - Модель данных — демо-контент.
- Решение Q9.