# Лендинги и веб-страницы — публикуемый single-file HTML

Ты собираешь **одностраничные сайты и лендинги** как один самодостаточный `index.html`, готовый к публикации через `app(action=publish)` на `<slug>.apps.samreshuuu.ru`.

## Разделение труда — это главное
- **Ты (LLM) пишешь только КОНТЕНТ** в виде JSON: заголовки, копирайт, бренд-цвет, пресет стиля, порядок секций, поля формы.
- **Скрипт детерминированно собирает СТРУКТУРУ**: HTML, экранирование, CSS, палитру с контрастом, SEO-мету, доступность.
- **Никогда не пиши HTML руками.** Креатив (текст, цвет, состав секций) — твой; точность (разметка, контраст, единый файл) — скрипта.

## Когда использовать этот навык, а когда — нет
- ✅ Этот навык: публикуемый лендинг / промо-страница / мини-сайт одним HTML-файлом → потом `app(action=publish)`.
- ❌ Слайды / презентация-дек → `html_presentation_ru`.
- ❌ Интерактивный React-UI / компоненты внутри приложения → `frontend_design_ru` / `taste_skill_ru`.
- ❌ График или диаграмма в пузыре → `render_visual`.
- ❌ SEO-страница сравнения с конкурентом (контент) → `competitor_alternatives_ru` (рендер можно отдать этому навыку).

## Скрипты (в песочнице)
Лежат в `$SKILLS_ROOT/landing_builder_ru/scripts/` (путь уже в `sys.path`):
- `render.py` — собирает `index.html` из JSON-конфига. **Главный инструмент.**
- `palette.py` — из одного бренд-HEX выводит WCAG-AA палитру (зови сам, если нужно проверить контраст; `render.py` делает это автоматически).
- `md_render.py` — markdown-подмножество → HTML (используется внутри секций `content`/`faq`).
- `inject.py` — идемпотентно добавляет scrollspy + активную навигацию (необязательный второй проход).

## Рабочий процесс
1. `read_skill('landing_builder_ru')` (этот навык).
2. Напиши `cfg.json` с контентом (схема ниже).
3. Собери страницу:
   ```bash
   python $SKILLS_ROOT/landing_builder_ru/scripts/render.py --input cfg.json --out $OUTPUT_ROOT/index.html
   ```
4. (По желанию) добавь интерактив: `python $SKILLS_ROOT/landing_builder_ru/scripts/inject.py $OUTPUT_ROOT/index.html`
5. Файл в `$OUTPUT_ROOT/index.html` сам отрендерится как inline-артефакт — покажи пользователю.
6. Нужен живой URL до публикации (много страниц, проверка на телефоне, показ коллеге) — подними превью: `app(action=preview, command="python3 -m http.server $PORT --bind 0.0.0.0 --directory $OUTPUT_ROOT", name="landing")`. Отдай пользователю вернувшийся публичный URL и итерируй по фидбеку: правишь `cfg.json` → перезапускаешь `render.py` → тот же `app(action=preview, …)` возвращает тот же URL.
7. **Публикация — только по явной просьбе** пользователя: вызови `app(action=publish, path="$OUTPUT_ROOT/index.html")` (outward-facing, почти необратимо).

## Инструмент `app` — все действия
- `app(action=publish, path=…, form_schema=…, csp_allowlist=…, slug=…, change_description=…)` — опубликовать HTML. Без `slug` — новый сайт; со `slug` — новая ревизия по тому же URL.
- `app(action=list)` — приложения организации. `app(action=leads, slug=…)` — заявки с формы.
- `app(action=revisions, slug=…)` — история ревизий; `app(action=rollback, slug=…, revision=N)` — вернуть ревизию.
- `app(action=fork, slug=…)` — копия под новым slug; `app(action=unpublish, slug=…)` — снять с публикации.
- `app(action=preview, command=…, name=…, cwd=…)` — dev-сервер в песочнице; `app(action=preview_logs, name=…)`; `app(action=preview_stop, name=…)`.
- Старых имён `publish_app` / `manage_app` больше нет — всё через `app(action=…)`.

## Превью в песочнице (`action=preview`)
- Песочница выдаёт команде `$PORT`, `HOST=0.0.0.0`, `BROWSER=none`. Сервер ОБЯЗАН слушать именно `$PORT` и `0.0.0.0` (не `127.0.0.1`, не хардкод порта) — иначе форвардинг не найдёт порт и превью не станет `ready`.
- Команда должна быть **foreground**-процессом: без `&`, без `nohup`, без демонизации — иначе процесс считается упавшим сразу.
- Браузер не открывать (`--open`, `xdg-open` и подобное запрещены; `BROWSER=none` уже выставлен).
- Порт детерминирован по `(сессия, name)` — тот же `name` после перезапуска даёт тот же публичный URL. Повторный `action=preview` с тем же `name` сам гасит старый процесс.
- URL приходит только когда порт реально отвечает; отдавай пользователю именно его.
- Превью «ещё поднимается» или упало → `app(action=preview_logs, name=…)`, чини причину по хвосту лога и перезапускай `action=preview`. Закончил — `app(action=preview_stop, name=…)`.

## Схема конфига (`cfg.json`)
```json
{
  "meta": {"title": "...", "description": "... (для SEO/OG)", "lang": "ru"},
  "brand": {"color": "#2563EB", "font": "Inter (опционально)"},
  "preset": "editorial | technical | minimal | playful",
  "sections": [
    {"type": "hero", "id": "top", "headline": "...", "subhead": "...",
     "ctas": [{"label": "...", "href": "#lead", "variant": "primary|ghost"}]},
    {"type": "features", "title": "...",
     "items": [{"icon": "⚡", "title": "...", "body": "..."}]},
    {"type": "pricing", "title": "...",
     "items": [{"name": "...", "price": "1 490 ₽", "period": "за польз./мес",
                "featured": true, "features": ["...", "..."],
                "cta": {"label": "...", "href": "#lead", "variant": "primary"}}]},
    {"type": "content", "title": "...", "markdown": "...(markdown-подмножество)"},
    {"type": "faq", "title": "...", "items": [{"q": "...", "a": "...(markdown)"}]},
    {"type": "cta", "id": "lead", "title": "...", "body": "...", "form": true}
  ],
  "lead_form": {
    "submit_label": "Получить демо",
    "turnstile": true,
    "fields": [
      {"name": "name", "label": "Имя", "type": "text", "required": true},
      {"name": "phone", "label": "Телефон", "type": "tel", "required": true}
    ]
  }
}
```
Секцию с формой делай `{"type":"cta","id":"lead","form":true}`, а сами поля описывай в `lead_form`. На `app(action=publish)` продублируй те же поля в `form_schema`.

## Канон публикуемости (скрипт уже это соблюдает — не нарушай вручную)
- Ровно один самодостаточный HTML; CSS — инлайном в `<style>`; без отдельных css/js и относительных путей.
- Внешние библиотеки/шрифты — только с разрешённых CDN-хостов; их канонический список — в описании инструмента `app` (раздел ПУБЛИКАЦИЯ), не дублируй его здесь. Картинки — абсолютные `https:` или `data:`.
- Форма лида = `<form data-lead-form>` (хост сам перепишет `action`).
- Капча = `<div class="cf-turnstile" data-sitekey="{{turnstile_sitekey}}">` (хост подставит ключ).
- **Бейдж «Сделано на Самрешу» НЕ добавляй** — его впечатывает хост.
- Если страница реально куда-то стучится (`fetch`/API) — перечисли хосты в `csp_allowlist` при вызове `app(action=publish)`.

## Дизайн без «AI-slop»
- Реальная иерархия: один сильный акцент (бренд-цвет), не раскрашивай всё подряд.
- Осмысленная типографика и щедрые отступы; не центрируй абсолютно всё.
- Контраст не «на глаз» — его считает `palette.py` (WCAG AA). Не подменяй цвета вручную.
- Копирайт по фреймворкам: **PAS** (Problem-Agitate-Solve), **AIDA** или **BAB** (Before-After-Bridge) — особенно в hero и cta.
- Конкретика вместо общих слов: цифры, выгоды, факты — не «инновационное решение».

## Pitfalls
- Не пиши HTML руками — только JSON-конфиг для `render.py`.
- Не публикуй без явной просьбы пользователя (`app(action=publish)` outward-facing). Нужен показать результат — это `app(action=preview)`, а не публикация.
- Не запускай preview-команду в фоне (`&`, `nohup`) и не хардкодь порт/`127.0.0.1` — только `$PORT` и `0.0.0.0`.
- Не вставляй бейдж/Turnstile-ключ сам — это бьёт по host-bake.
- Не используй внешние CSS/JS и относительные пути — страницу зарубит CSP/канон.
