Skip to content

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 an insertedNodes map.
  • 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.html
  • GET /api/templates — lists templates with dimensions
  • GET /api/designs — lists design files; GET /api/design?name= — serves one
  • GET /template/:file — serves template HTML
  • POST /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: the opencode provider, mimo-v2.6-flash-free.
  • editor.html — sidebar (template/color/font controls) + iframe stage + Ask panel
  • editor.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-color meta, CSS custom properties, ranked hex/rgba (fetched with curl; Node fetch() 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)

  1. Describe the design (e.g. "make an Instagram post about our sale, orange + black").
  2. 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
    
  3. Render:
    node ~/.config/opencode/skills/canva-design/lib/render.js \
      ./designs/<name>.html ./designs/<name>.png <W> <H> [scale]
    
    Default scale = 2 → output is 2× for crispness (1080×1080 → 2160×2160). Use 1 for exact 1:1 px.
  4. Preview in browser (agent can't see images):
    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>.html
    
    Serves http://localhost:8787/... and opens the default browser. Remote: ssh -L 8787:localhost:8787.
  5. 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 :root CSS 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 uses curl.
  • Model big-pickle was flaky; mimo-v2.6-flash-free is the working choice for /api/ask.
  • .bak files left in editor/ 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-free model 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.

  • [[Canva Editor Improvement Backlog]] — prioritised editor upgrades
  • [[JBS LEMO Shopify Storefront]] — the store some of this content promotes
  • [[Index]]