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 added 2026-07-21.

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
├── editor/           # Interactive editor (port 8790)
│   ├── editor-server.js   # HTTP server: UI + templates + /api/export + /api/chat
│   ├── editor.html        # Sidebar + iframe stage + Chat with Hal panel
│   ├── editor.js          # Contenteditable, drag/resize, CSS-var binding, chat logic
│   └── README.md          # Usage docs
└── lib/
    ├── 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
  • Chat with Hal — in-page AI chat panel that can edit the canvas (HTML responses applied live)
  • 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).

Architecture

  • editor-server.js — Express-like HTTP server (port 8790):
  • GET / — serves editor.html
  • GET /api/templates — lists templates with dimensions
  • GET /template/:file — serves template HTML
  • POST /api/export — renders HTML to WebP (Playwright → temp PNG → ImageMagick convert)
  • POST /api/chat — proxies to OpenCode Zen API via curl (Node fetch stalls)
  • editor.html — sidebar (template/color/font controls) + iframe stage + chat panel
  • editor.js — contenteditable setup, drag/resize handles, CSS-var binding, chat send/receive

Chat with Hal

The chat panel talks to the same AI as this OpenCode session via the Zen API.

Important findings (2026-07-21): - hy3-free model requires account balance despite being labeled "free" in docs - big-pickle is actually free (routes to MiMo V2.5 via Xiaomi) - Node's fetch() stalls against opencode.ai/zen/v1 — switched to curl spawn - Error responses now properly surfaced (both OpenAI-style {error:{message:...}} and opencode-style {code:..., message:...})

Chat system prompt instructs the model to return <!DOCTYPE html> documents for canvas edits, or plain text for questions.

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 to a working dir, edits copy + CSS variables (--primary, --bg, fonts).
  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. Post-process with ImageMagick if needed: magick in.png -quality 85 out.jpg.

Requirements / setup notes

  • playwright is installed in lib/ (browsers cached in ~/.cache/ms-playwright).
  • DISPLAY=:1 is available on this machine, so xdg-open opens a real window.
  • 1300+ fonts installed (Noto families) — reference by name in font-family.
  • ImageMagick 7.1 on system (used for PNG→WebP conversion in export).
  • Node v26.4.0, npx 12.0.1.
  • OpenCode Zen API key from ~/.config/opencode/opencode.json.

Export Formats

  • Default: WebP (quality 90) — smaller file size, modern format
  • Temp file: PNG (rendered by Playwright), converted by ImageMagick
  • Content-type: image/webp

Why local (not the Canva MCP)

The official Canva MCP (https://mcp.canva.com/mcp, OAuth2 via mcp-remote) gives true Canva but is cloud-bound and needs a Canva account. The local skill was chosen for privacy, offline use, and zero account. Switch paths later if real Canva integration is wanted.

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.