opencode-config/docs/decisions/060-pr-134-draw-image-svg-template-renderer.md
Sergey 56b67d2d5c
feat(draw-image): SVG template renderer for on-brand covers (#134)
* 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>
2026-07-29 23:42:50 +03:00

2.9 KiB
Raw Blame History

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).

Решение

  1. opencode plugin (.opencode/tools/draw-image.ts) — spawnSync wrapper вокруг node cli.ts render ..., возвращает { path, hash, status }
  2. Слот-система — HTML-комментарии в SVG-шаблоне парсятся в SlotSpec (name/x/y/w/h/fit/recolor/bg/border/radius). Контент резолвится: Lucide lookup → brand-logos lookup → file path
  3. Render pipeline — buildSvg (assemble) → render.mjs (sharp → PNG 1024×1024, fontFiles: Geist TTF)
  4. Идемпотентность — cover.meta.json с sha256(brand+template+args), skip при совпадении хеша
  5. Lucide-иконки — npm-пакет lucide-static, postinstall sync в icons/lucide/, recolor stroke → accent
  6. Шрифты — Geist Sans TTF bundled в fonts/, переданы в sharp через fontFiles (без этого fallback = off-brand)
  7. Тесты — 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.