OpenCode Canva Design Skill¶
Local, account-free "Canva" inside OpenCode. Turns a plain-language request into a finished PNG/WebP by rendering HTML/CSS templates via the cached Chromium (Playwright). No Canva account, no API.
Status: Built 2026-07-11; interactive editor 2026-07-21; descriptor editing (Ask box) + site-colour extraction 2026-10-11.
Core idea: a design is a real HTML file on disk. The browser never rewrites it — it collects change descriptors and the server patches the source through lib/patch.js. The model returns descriptors, never a document ("ask, don't write").
Location¶
~/.config/opencode/skills/canva-design/
canva-design/
├── SKILL.md # skill definition + workflow + dimensions catalog
├── templates/ # 9 starter HTML/CSS templates (edit inline CSS vars + copy)
│ ├── social-post.html 1080×1080
│ ├── story.html 1080×1920
│ ├── linkedin-banner.html 1584×396
│ ├── youtube-thumbnail.html 1280×720
│ ├── banner.html 1200×628
│ ├── flyer.html 1080×1350
│ ├── quote-card.html 1080×1080
│ ├── announcement.html 1080×1080
│ └── logo.html 600×600
├── designs/ # working design files (real HTML) + user-created designs
├── editor/ # Interactive editor (port 8790)
│ ├── editor-server.js # HTTP server: UI + templates + /api/export + /api/ask
│ ├── editor.html # Sidebar + iframe stage + Ask (descriptor) panel
│ ├── editor.js # Contenteditable, drag/resize, CSS-var binding, staging
│ └── README.md # Usage docs
└── lib/
├── patch.js # deterministic, formatting-preserving patcher (the only writer)
├── apply-descriptors.js # apply a JSON descriptor plan from the CLI (dry-run / --write)
├── palette.js # extract a site's colour scheme for Ask (extractSite)
├── render.js # HTML → PNG → WebP via Chromium + ImageMagick
├── serve.js # serve + open in browser (preview)
└── brand.json # default brand kit (colors/fonts/logo)
Interactive Editor¶
A browser-based Canva-style editor running on http://localhost:8790/.
Features¶
- Template picker — 9 templates with correct dimensions (1080×1080, 1080×1920, etc.)
- Live CSS-var editing — background, primary, accent, text, muted colors + display/body fonts
- Drag & resize — blue handles on text elements; images are draggable and resizable
- Image upload — "+ Add Image" button or drag-and-drop files onto the canvas
- Ask box — plain-language request → list of change descriptors (
text,style,attr,cssvar,insert); each is dry-run-validated, staged into the iframe (applyToDom), and shown in a pending list (Apply/Discard). Insert undo via aninsertedNodesmap. - Site colours — put a URL in the request and Ask fetches that site's palette, then only allows colours from it.
- Export — renders to temp PNG via Playwright, converts to WebP via ImageMagick (
convert -quality 90)
Running the Editor¶
cd ~/.config/opencode/skills/canva-design/editor
node editor-server.js
# → http://localhost:8790/
Or via detached process:
setsid nohup node editor-server.js > /tmp/canva-editor.log 2>&1 &
Autostarted via Hyprland config (~/.config/hypr/modules/autostart.lua). Binds 127.0.0.1:8790.
Architecture¶
editor-server.js— Express-like HTTP server (port 8790):GET /— serves editor.htmlGET /api/templates— lists templates with dimensionsGET /api/designs— lists design files;GET /api/design?name=— serves oneGET /template/:file— serves template HTMLPOST /api/export— renders HTML to WebP (Playwright → temp PNG → ImageMagick convert)POST /api/ask— plain-language request → descriptor plan; an optional site URL is fetched for its palette and enforced server-side. Model: theopencodeprovider,mimo-v2.6-flash-free.editor.html— sidebar (template/color/font controls) + iframe stage + Ask paneleditor.js— contenteditable setup, drag/resize handles, CSS-var binding, descriptor staging, pending-changes list (Apply/Discard)
The server reads routes at process start — restart it after code changes.
Ask box (change descriptors)¶
/api/ask turns a plain-language request into a list of change descriptors, each
dry-run-validated against the source before it's shown. Descriptor kinds: text, style,
attr, cssvar, insert (sanitised — no <script>, no inline handlers, no javascript:
URLs). catalog(html) gives the model each element's exact ed path, so it never
hand-counts child indices. Ambiguous requests come back as {"question": "..."} rather than
a guess.
The old freeform chat (/api/chat, model returned whole <!DOCTYPE html> documents) was
removed 2026-10-11 in favour of this deterministic path.
Site colours¶
lib/palette.js → extractSite(url) does two-tier extraction:
- HTML tier —
theme-colormeta, CSS custom properties, ranked hex/rgba (fetched withcurl; Nodefetch()stalls against this box). - Rendered tier — cached Chromium: body background/text, h1, links, buttons, header, logo.
/api/ask auto-detects a URL in the request and enforces swatch membership — the model
may only use colours actually fetched; if no palette is found it must return
{"question": "..."}.
Image Upload¶
- Button: "+ Add Image" in sidebar opens file picker
- Drag-and-drop: drop any image file onto the stage area
- Images injected as
<img>elements with base64 data URLs (embeds in export) - Resizable via corner handle (scales proportionally)
- Draggable via blue move handle
CLI Usage (without editor)¶
- Describe the design (e.g. "make an Instagram post about our sale, orange + black").
- OpenCode copies a template into
designs/, then edits copy + CSS variables (--primary,--bg, fonts) — preferably as a descriptor plan applied by the deterministic patcher:node ~/.config/opencode/skills/canva-design/lib/apply-descriptors.js \ ./designs/<name>.html plan.json # dry-run (prints the diff) node ~/.config/opencode/skills/canva-design/lib/apply-descriptors.js \ ./designs/<name>.html plan.json --write # persist - Render:
Default
node ~/.config/opencode/skills/canva-design/lib/render.js \ ./designs/<name>.html ./designs/<name>.png <W> <H> [scale]scale= 2 → output is 2× for crispness (1080×1080 → 2160×2160). Use1for exact 1:1 px. - Preview in browser (agent can't see images):
Serves
node ~/.config/opencode/skills/canva-design/lib/serve.js ./designs/<name>.png # or preview the live HTML for iteration: node ~/.config/opencode/skills/canva-design/lib/serve.js ./designs/<name>.htmlhttp://localhost:8787/...and opens the default browser. Remote:ssh -L 8787:localhost:8787. - Extract a site's palette directly:
node -e 'require("~/.config/opencode/skills/canva-design/lib/palette").extractSite("https://example.com").then(console.log)'
Tests¶
lib/test-patch.js— descriptor patcher suite (321 checks)lib/test-palette.js— site-colour suite (19 checks)
Theming & Brand Notes¶
- Colours live in each template's
:rootCSS custom properties; Ask maps requests onto those vars. - Embedded images are inlined as data URLs so they travel with the file (render, editor,
export). SVG works too via
data:image/svg+xml;base64,…. - Site palettes are extracted live, never hard-coded — e.g. lemo.com yields primary
#0079CA, navy#003399.
Known gotchas¶
- Node
fetch()stalls against some sites (and the Zen/opencode API) — networking usescurl. - Model
big-picklewas flaky;mimo-v2.6-flash-freeis the working choice for/api/ask. .bakfiles left ineditor/are publicly reachable via the/editor/*route — see [[Canva Editor Improvement Backlog]] housekeeping.
Notes & History¶
- Earlier versions of this editor used freeform chat that returned whole
<!DOCTYPE html>documents. That path was removed 2026-10-11; edits now go through the Ask box / descriptor plan above. hy3-freemodel required account balance despite being labelled "free" in the docs (2026-07-21).
Example¶
LEMO EGG.2B.302.CLL Instagram post demo: /home/dini/designs/lemo/instagram-post.{html,png} — used the store's original EGG_CLL.webp product image as the hero.
LEMO "NEW SITE COMING SOON" post (2026-10-11): ~/.config/opencode/skills/canva-design/designs/lemo-coming-soon.html — built from templates/social-post.html via a descriptor plan; palette pulled live from lemo.com (#0079CA / #003399), and the real lemo.com logo embedded as an SVG data URL.
Related¶
- [[Canva Editor Improvement Backlog]] — prioritised editor upgrades
- [[JBS LEMO Shopify Storefront]] — the store some of this content promotes
- [[Index]]