--- name: "cp-carousel-render" description: "Превращает готовые слайды в картинки: HTML и CSS с темой бренда, headless-браузер, PNG нужного размера, батч по всем слайдам и PDF для лид-магнита. Держит кириллицу корректной там, где генераторы изображений её ломают, и разбирает восемь причин, по которым шрифт в контейнере не встаёт. Use when the user needs to render slides, convert HTML to PNG, export images or fix broken fonts in headless rendering. Триггеры — «отрендерить слайды», «html в png», «шрифты не встают», «экспорт картинок», «собрать картинки из вёрстки», «квадратики вместо букв», «сделай pdf из слайдов», «puppeteer», «playwright»." --- Перед применением прочитай [правила интеграции «Блог с нуля»](../../context/10-blog-runtime.md). Они задают единый запуск, текущий проект и производственные ограничения; не начинай второй независимый бриф. # cp-carousel-render · Рендер слайдов HTML → PNG Технический скилл. На входе — тексты слайдов и тема, на выходе — файлы картинок, которые можно загружать без правок. ## Когда не этот скилл - Нужно придумать, что написать на слайдах → `cp-carousel`. - Нужно собрать палитру и подобрать шрифты → `cp-design-system`. - Нужна одна картинка-обложка или превью → `cp-cover` (рендер всё равно вернётся сюда). - Нужна генеративная картинка или фон → `cp-image-prompt`. ## Контекст Читай из `context/`: `06-design.md` (§4 конвейер и подводные камни со шрифтами, §6 размеры, §7 контракт темы), `01-platforms.md` (требования площадок к файлам). Профиль бренда — `brands//assets.md`: палитра, шрифты, где лежат файлы шрифтов. Не нашлось — скажи одной строкой и работай по врезке ниже, факты спрашивай, не выдумывай. --- ## Врезка констант **Почему вообще HTML → PNG.** Единственный надёжный способ получить корректную кириллицу на картинке. Генерация текста кириллицей в продакшен не идёт — разбор границы в `cp-image-prompt`. **Правило WYSIWYG.** Превью и экспорт идут через **одну и ту же функцию оборачивания слайда**. Две разные функции разъезжаются гарантированно, и обнаруживается это после публикации. **Размеры.** | Назначение | Пиксели | Соотношение | |---|---|---| | Карусель и пост Instagram | 1080×1350 | 4:5 | | Квадрат: ВК, универсальный | 1080×1080 | 1:1 | | Сторис, вертикальное видео | 1080×1920 | 9:16 | | Превью YouTube | 1280×720 | 16:9 | | Карточка VK Рекламы | 600×600 | 1:1 | | Страница PDF, A4 при 150 dpi | 1240×1754 | — | Instagram через Graph API: JPEG, sRGB, ширина 320–1440 px, ≤8 МБ на файл (уровень A). PNG перед загрузкой через API конвертируется. Дзен: минимальная ширина 300 px, ≤30 МБ. **Безопасные поля.** Минимальный внутренний отступ 40 px при холсте 1080–1200. Для сетки профиля Instagram важное держать в центральных ~1012×1350. Для VK Рекламы не занимать 10% сверху и по 10% по бокам. Для историй ВК — 200 px сверху и снизу. **Двойное ограничение размера текста обязательно.** Кириллица шире латиницы при том же кегле: строка, которая по-английски помещалась, по-русски вылезает. Размер задаётся через `min()` сразу по ширине и по высоте. **Контраст.** Обычный текст 4,5:1, крупный (≥24 CSS px или ≥18,5 px bold) 3:1, AAA 7:1. Проверка — в `cp-design-system`. --- ## Вход 1. **Тексты слайдов** — от `cp-carousel` или от пользователя, в порядке публикации. 2. **Тема** — slug бренда либо «нет темы»: тогда сначала `cp-design-system`, иначе цвета будут выбраны наугад, а произвольный hex — главный источник «ИИ-вида». 3. **Размер и площадка.** 4. **Куда складывать файлы** и как именовать. По умолчанию `out/slide-01.png` … c ведущим нулём: без нуля сортировка ломается на десятом слайде. 5. **Есть ли изображения** внутри слайдов и где они лежат. 6. **Нужен ли PDF** — тогда это лид-магнит, см. раздел ниже. --- ## Процесс 1. **Проверь окружение.** Шрифты, `fontconfig`, браузер. Команды — в разделе «Восемь причин». 2. **Положи шрифты локально** рядом с шаблоном (`fonts/`), подключи через `@font-face` с относительным путём. С CDN в момент рендера — не грузить. 3. **Собери данные слайдов** в один массив: тип, заголовок, текст, номер, путь к изображению. 4. **Возьми шаблон** `references/template.md` — там рабочий HTML со всеми переменными темы, сеткой, безопасными полями и двойным ограничением кегля. 5. **Подставь тему** — только переменные в `:root`. Разметку под цвета не трогать: смена темы = смена одного блока токенов. 6. **Отрендерь батчем** по рецепту ниже. Ждать `document.fonts.ready` перед каждым скриншотом. 7. **Проверь первый и последний файл глазами** на реальном кадре, а не на глаз по HTML: headless-рендер может отличаться от обычного режима. 8. **Отдай список файлов**, размеры, вес и что осталось проверить. --- ## Скелет слайда Полный рабочий файл — `references/template.md`. Здесь — то, что нельзя потерять при переписывании. ```css :root{ --bg:#0E1116; --surface:#171C24; --text:#F2F5F9; --text-muted:#A3AEBE; --accent:#FFB01F; --accent-dim:#FFC966; --font-head:"Golos Text",system-ui,sans-serif; --font-body:"Golos Text",system-ui,sans-serif; --pad:64px; /* безопасное поле, не меньше 40px */ } .slide{ width:1080px; height:1350px; /* один источник правды по размеру */ container-type:size; /* включает cqw/cqh внутри слайда */ box-sizing:border-box; padding:var(--pad); background:var(--bg); color:var(--text); display:flex; flex-direction:column; justify-content:space-between; overflow:hidden; /* видимая обрезка лучше невидимого выезда */ } .h1{ font-family:var(--font-head); font-weight:800; line-height:1.05; letter-spacing:-0.01em; font-size:min(9.2cqw, 7.4cqh); /* двойное ограничение: ширина И высота */ text-wrap:balance; } .body{ font-family:var(--font-body); font-size:min(5.0cqw,4.0cqh); line-height:1.3; } .meta{ font-size:min(2.6cqw,2.1cqh); color:var(--text-muted); } ``` **Почему `min()` из двух единиц.** `cqw` считается от ширины слайда, `cqh` — от высоты. При переносе того же компонента с 1080×1350 на 1080×1920 или 1280×720 одна из величин резко меняется; `min()` берёт меньшую, и текст не выезжает ни по ширине, ни по высоте. Если в окружении нет контейнерных единиц — выставь viewport ровно в размер слайда и используй `min(9.2vw, 7.4vh)`: при совпадении viewport и слайда результат идентичен. Смешивать `vw` и `cqw` в одном проекте нельзя. **Переполнение всё равно возможно** — при 20 словах вместо 15. Лечится текстом, а не кеглем: правило «≤15 слов на слайд» существует ровно для этого. **Одна функция оборачивания.** И превью, и экспорт вызывают одну функцию, которая возвращает `
…
`. Превью масштабируется только снаружи — `transform: scale(k)` на обёртке. Внутрь слайда масштаб не проникает, поэтому картинка совпадает с превью пиксель в пиксель. --- ## Восемь причин, по которым шрифт не встал Порядок — по частоте. Проверять сверху вниз, не перескакивая. | № | Причина | Признак | Проверка и лечение | |---|---|---|---| | 1 | **В контейнере нет шрифтов вообще** | всё одинаковым дефолтом, кириллица «не та» | `fc-list \| wc -l` → около нуля. Ставить: базовый набор Noto (покрывает кириллицу), цветные эмодзи, Liberation | | 2 | **Не обновлён кеш после установки** | «шрифт поставил, а не видно» | `fc-cache -f -v`, затем `fc-list :lang=ru family \| sort -u` | | 3 | **Не поставлен `fontconfig`** | шрифты в системе лежат, браузер их не видит | ставить пакет `fontconfig` отдельно от самих шрифтов | | 4 | **Права на файлы кастомного шрифта** | локальный шрифт игнорируется без ошибок | файл должен быть читаем процессу браузера: `chmod 644 fonts/*.woff2`, проверить владельца папки | | 5 | **Эмодзи квадратами** | ▯ вместо значка | пакет цветных эмодзи плюс явный фолбэк в стеке: `font-family: "Golos Text", "Noto Color Emoji", sans-serif` | | 6 | **Скриншот снят до готовности шрифтов** | кадр с системным фолбэком, кириллица выглядит иначе | ждать `document.fonts.ready` **до** скриншота, плюс один кадр анимации | | 7 | **Шрифты грузятся с CDN в момент рендера** | результат «через раз» | класть локально, `@font-face` с относительным путём, `font-display: block` | | 8 | **Headless отличается от обычного режима** | в браузере верно, в файле нет | проверять на реальном PNG, а не в окне разработчика | **Команды проверки окружения** ```bash # что вообще установлено и есть ли кириллица fc-list | wc -l fc-list :lang=ru family | sort -u | head -30 # конкретный шрифт: подставится он или фолбэк fc-match "Golos Text" fc-match "IBM Plex Sans" # после любой установки шрифтов fc-cache -f -v # базовое покрытие для headless-контейнера (Debian/Ubuntu) apt-get update && apt-get install -y fontconfig fonts-noto-core fonts-noto-color-emoji fonts-liberation # браузер вместе с системными зависимостями npx playwright install --with-deps chromium ``` **Проверка внутри страницы — самая честная.** Спрашивает не систему, а сам браузер: ```js await page.evaluate(() => document.fonts.check('800 96px "Golos Text"')); // true = шрифт реально доступен await page.evaluate(() => [...document.fonts].map(f => `${f.family} ${f.weight} ${f.status}`)); ``` --- ## Рецепт рендера Playwright и Puppeteer взаимозаменяемы, API почти совпадает. Ничего платного не требуется. ```js // render.js — Playwright import { chromium } from 'playwright'; import { readFile, mkdir } from 'node:fs/promises'; const W = 1080, H = 1350; const slides = JSON.parse(await readFile('slides.json', 'utf8')); const browser = await chromium.launch(); // headless по умолчанию const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 // 1080×1350 на выходе ровно }); await mkdir('out', { recursive: true }); await page.goto(new URL('./template.html', import.meta.url).href); // file:// — шрифты локальные for (const [i, s] of slides.entries()) { await page.evaluate(s => window.renderSlide(s), s); // ОДНА функция оборачивания, живёт в шаблоне await page.evaluate(() => document.fonts.ready); // причина №6 await page.evaluate(() => new Promise(requestAnimationFrame)); const n = String(i + 1).padStart(2, '0'); await page.locator('#slide').screenshot({ path: `out/slide-${n}.png` }); } await browser.close(); ``` `renderSlide` живёт в самом шаблоне и вызывается и превью, и экспортом. Скриншот снимается **с элемента**, а не со страницы: тогда не важны прокрутка и поля документа. Puppeteer: `puppeteer.launch()` → `page.setViewport({width, height, deviceScaleFactor:1})` → `(await page.$('#slide')).screenshot(...)`. Остальное совпадает. В браузере без Node тот же макет собирается связкой `html-to-image` плюс `jsPDF`, но headless надёжнее: там не влияют расширения и системные настройки пользователя. --- ## PDF для лид-магнита Отличается от PNG тремя вещами: страница A4, фон должен печататься, шрифты встраиваются автоматически из HTML. ```js await page.pdf({ path: 'out/magnet.pdf', width: '1240px', height: '1754px', // A4 при 150 dpi printBackground: true, // без этого фон темы исчезнет margin: { top:0, right:0, bottom:0, left:0 } }); ``` ```css @page { size: 210mm 297mm; margin: 0 } .page { break-after: page; -webkit-print-color-adjust: exact; print-color-adjust: exact } ``` Проверять обязательно: PDF рендерится другим путём, чем скриншот, и переносы строк в нём могут отличаться. Первая и последняя страница смотрятся глазами. Содержание лид-магнита собирает `cp-lead-magnet`, обложку — `cp-cover`. --- ## Выход ``` РЕНДЕР · <тема или slug> · <размер> Файлы: out/slide-01.png … out/slide-NN.png (N штук, суммарно ~X МБ) Шрифты: <какие подставились фактически, по результату document.fonts.check> Проверено: контраст <да/нет> · переполнение <нет / слайды №…> · безопасные поля <да> Для загрузки: Осталось: <что проверить руками> ``` Если что-то не сошлось — скажи прямо, на каком слайде и почему, и не «почини» молча уменьшением кегля. ## Чек-лист перед выдачей - Шрифты подставились фактически, а не «вроде подключены» — проверено `document.fonts.check` внутри страницы. - Перед каждым скриншотом дождались `document.fonts.ready`. - Шрифты локальные, CDN в момент рендера не используется. - Превью и экспорт зовут одну функцию оборачивания. - Кегли заданы через `min()` по двум измерениям, ни на одном слайде нет переполнения. - Внутренний отступ не меньше 40 px, важное не заходит в запретные зоны площадки. - Имена файлов с ведущим нулём и в порядке публикации. - Размер и формат соответствуют способу публикации: для Graph API — JPEG, sRGB, ≤8 МБ, ширина 320–1440. - Цвета взяты из темы, ни одного hex, придуманного на ходу. - Первый и последний PNG просмотрены как файлы. ## Красные линии 1. Не подгонять вёрстку уменьшением кегля вместо сокращения текста. 2. Не рендерить без ожидания готовности шрифтов. 3. Не грузить шрифты с CDN в момент рендера. 4. Не разводить превью и экспорт по двум функциям. 5. Не выбирать цвета произвольно — только тема из `assets.md` или `cp-design-system`. 6. Не использовать шрифты без проверки лицензии на коммерческое применение: тип лицензии (desktop / web / app) различается и это самая частая незамеченная утечка прав в макетах. 7. Не выдавать результат, не открыв ни одного файла.