Install
openclaw skills install @clarence-g/pixelmsgRender HTML templates to pixel-perfect PNG image cards using Playwright and send them as rich image messages — instead of plain text. Use this skill whenever the user asks for a visual card, image, dashboard, widget, or styled message — including weather cards, GitHub stats, todo boards, reports, daily digests, announcements, and data summaries. Also trigger when the user says things like "给我做个图", "做张卡片", "发图片", "图片形式展示", "用图片显示", "rich message", "beautiful card", or anytime a visual representation would be more memorable or polished than a plain-text reply. When in doubt, prefer generating an image — it almost always delights more than text.
openclaw skills install @clarence-g/pixelmsgAny time a visual card beats a plain-text reply:
Check existing templates first. The templates/ directory in this skill's
root has production-ready options. Use one as-is or as a starting point whenever
it fits — don't rewrite from scratch if a template already covers the use case.
| Template | Best for | Style |
|---|---|---|
templates/weather.html | Generic weather card, any city | Glassmorphism |
templates/shanghai-weather.html | Live Shanghai weather (Open-Meteo) | Glassmorphism |
templates/github-trending.html | Top-10 GitHub Trending list | Dark Premium |
templates/github-stats.html | GitHub user profile / stats | Dark Premium |
templates/todolist.html | Categorized todo list with progress | Brand Color |
Use an existing template when: the content type maps directly (weather → weather.html, trending repos → github-trending.html).
Write a new template when: the content type is unique, or the required data doesn't fit cleanly into an existing template's structure. New templates go in templates/ and follow the Design System below.
Rules for any template:
<div id="app"><script> blocks or Alpine x-data to inject dynamic contentMinimal template:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
* { font-family: 'Inter', system-ui, sans-serif; box-sizing: border-box; }
body { margin: 0; padding: 0; background: transparent; }
</style>
</head>
<body>
<div id="app" x-data="{ title: 'Hello' }" style="width:480px; padding:32px; background:linear-gradient(135deg,#1565c0,#0d47a1);">
<h1 style="color:#fff; font-size:24px; font-weight:700;" x-text="title"></h1>
</div>
</body>
</html>
The render script is at scripts/render.sh inside this skill's directory.
Determine the skill root from where this SKILL.md lives — it's the same directory.
# Basic — mobile viewport by default
bash <skill-root>/scripts/render.sh <skill-root>/templates/weather.html
# Custom output directory and viewport
bash <skill-root>/scripts/render.sh <skill-root>/templates/github-stats.html ./output desktop
# For a new template you wrote
bash <skill-root>/scripts/render.sh /path/to/your/template.html
render.sh screenshots the #app element using a file:// URL (built
internally by screenshot.mjs) and prints a single line: the absolute
path to the PNG file. The default viewport is mobile; pass a third
argument to change it (e.g. desktop).
For more control, call screenshot.mjs directly:
node <skill-root>/screenshot.mjs <skill-root>/templates/weather.html \
--viewport mobile \
--selector '#app' \
--out ./screenshots \
--name weather
render.sh outputs one line — the absolute path to the PNG:
/Users/you/Projects/pixelmsg/screenshots/weather-default-mobile.png
Before returning the path, copy the file to the agent's workspace directory. Many platforms (including OpenClaw on Feishu/Telegram/Signal) only serve files from a specific workspace path. Accessing the file directly from the project directory will silently fail — no image appears in chat.
cp /path/to/screenshot.png ~/.openclaw/workspace/output.png
Then return the workspace path.
MEDIA: SyntaxIn your reply, output the workspace path on its own line, prefixed with
MEDIA: — nothing else on that line, no surrounding text:
MEDIA:/Users/you/.openclaw/workspace/output.png
⚠️ Critical rules:
MEDIA: line must be completely standalone — no text before or after it on the same lineHere is your weather card! 🌤️
MEDIA:/Users/you/.openclaw/workspace/weather.png
# 1. Render
bash <skill-root>/scripts/render.sh <skill-root>/templates/weather.html
# → /Users/you/Projects/pixelmsg/screenshots/weather-default-mobile.png
# 2. Copy to workspace
cp /Users/you/Projects/pixelmsg/screenshots/weather-default-mobile.png \
~/.openclaw/workspace/weather-card.png
Then in your reply:
MEDIA:/Users/you/.openclaw/workspace/weather-card.png
screenshot.mjs)| Flag | Default | Description |
|---|---|---|
--viewport | all | mobile / tablet / desktop / all |
--width | — | Custom width (overrides --viewport) |
--height | 900 | Viewport height |
--selector | #app | CSS selector of element to capture |
--out | ./screenshots | Output directory |
--name | (from filename) | Output filename prefix |
--params key=val | — | URL query params passed to the page |
--full-page | off | Capture full page instead of element |
--device-scale | 2 | Device pixel ratio (retina quality) |
Every card should feel native-quality — like a real app screenshot, not a developer prototype.
| Style | Best for | Background |
|---|---|---|
| Glassmorphism | Weather, ambient, sky-themed | Deep blue gradient + frosted panels |
| Dark Premium | GitHub, code, tech content | Near-black + subtle borders |
| Brand Color | Todo, productivity, notifications | Strong hue gradient + white text |
| Light Card | General info, digest, clean data | Light gray bg + white cards |
Do NOT mix styles. Pick one and stay consistent.
background: linear-gradient(160deg, #0f4c81 0%, #1976d2 50%, #4fc3f7 100%);
.panel { backdrop-filter: blur(16px); background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2); border-radius: 16px; }
color: #fff; /* primary */ color: rgba(255,255,255,0.65); /* secondary */
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
.panel { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; }
/* Accents: #58a6ff blue · #3fb950 green · #f78166 red · #d2a8ff purple */
color: #e6edf3; /* primary */ color: rgba(230,237,243,0.6); /* secondary */
background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
.row { background: rgba(255,255,255,0.08); border-radius: 10px; }
color: #fff; /* primary */ color: rgba(255,255,255,0.55); /* secondary */
background: #f5f7fa;
.card { background: #ffffff; border-radius: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
color: #1a1a2e; /* primary */ color: #6b7280; /* secondary */
body: margin: 0; padding: 0; background: transparent;#app defines the card — no whitespace outside itbox-sizing: border-box on #appoverflow: hidden on #app — may clip content in screenshot'Inter', system-ui, -apple-system, sans-serif12 / 14 / 16 / 20 / 24 / 32 / 48 / 64pxAlways use inline SVG. Never emoji, icon fonts, or external URLs.
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- 2–4 paths max for weather icons -->
</svg>
opacity: 0.08–0.15, pointer-events: none