* feat(draw-image): add SVG template render engine * feat(draw-image): add opencode plugin and tool tests * test(draw-image): add vitest unit integration e2e suite * chore(draw-image): wire CI dependabot docs and ADR * docs(handoff): set PR number * docs(handoff): fix PR number placeholder in frontmatter * fix(ci): use tempfile and trailing newline in draw-image tests --------- Co-authored-by: opencode-agent <agent@opencode.local>
2.9 KiB
2.9 KiB
ADR-060: SVG template renderer for on-brand covers
Статус
Accepted (2026-07-29)
Контекст
В продуктовых репо slaid098 нет ни одного cover-ассета. Обложки для slaid098.dev рисовались вручную или генерировались fallback-функцией (svgFallback в route.ts — HSL-градиент + буква, off-brand). Нужна утилита, которая берёт SVG-шаблон со слотами, подставляет палитру из brand.json + контент из args, рендерит PNG через sharp с bundled Geist TTF — always on-brand.
Архитектурный выбор: размещение в .opencode/ (opencode plugin + render-движок) через volume bind-mount → видна во всех воркспейсах глобально. Отдельный package.json в .opencode/draw-image/ (deps: sharp, lucide-static) — не загрязняет .opencode/package.json (используется для tools/*.ts).
Решение
- opencode plugin (
.opencode/tools/draw-image.ts) — spawnSync wrapper вокругnode cli.ts render ..., возвращает{ path, hash, status } - Слот-система — HTML-комментарии в SVG-шаблоне парсятся в SlotSpec (name/x/y/w/h/fit/recolor/bg/border/radius). Контент резолвится: Lucide lookup → brand-logos lookup → file path
- Render pipeline — buildSvg (assemble) → render.mjs (sharp → PNG 1024×1024, fontFiles: Geist TTF)
- Идемпотентность — cover.meta.json с sha256(brand+template+args), skip при совпадении хеша
- Lucide-иконки — npm-пакет lucide-static, postinstall sync в icons/lucide/, recolor stroke → accent
- Шрифты — Geist Sans TTF bundled в fonts/, переданы в sharp через fontFiles (без этого fallback = off-brand)
- Тесты — vitest (48: unit + integration + e2e) + pytest (5: tool wrapper via _ts_loader.mjs)
Альтернативы
- @resvg/resvg-js вместо sharp — sharp уже используется в экосистеме, fontFiles опция покрывает потребность. resvg был бы отдельной dep.
- Шрифты через fontconfig (системная установка) — негибко, требует root в CI. Bundle TTF в fonts/ + fontFiles опция sharp — self-contained.
- Шаблоны в коде (не SVG-файлы) — менее гибко, требует правок кода для нового шаблона. SVG-файлы в templates/ — ноль правок кода.
- Отдельный package.json vs расширить .opencode/package.json — отдельный package.json изолирует deps (sharp тяжёлая native dep) от tools/*.ts.