docs: thumbnails skill and humanizer skill

This commit is contained in:
Rotimi Best
2026-07-27 13:06:43 +01:00
parent 776c57525b
commit 1a44983210
9 changed files with 990 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
---
name: create-thumbnail
description: Generate on-brand ClassroomIO thumbnails (blog posts, changelogs, YouTube videos) as 1920×1080 PNGs. Builds HTML+CSS from the brand token system and renders deterministically via headless Chrome. Use when the user asks to create/make a thumbnail, cover image, or video/blog banner for ClassroomIO.
---
You are producing a **1920×1080 (16:9) ClassroomIO thumbnail** by writing an HTML file that
consumes the brand token CSS in this skill, then rendering it to PNG with `render.py`. The
output must look designed-in-Figma, not templated. The "Self-Host ClassroomIO with Dokploy"
two-tile thumbnail is the **quality bar to match or beat — it is NOT a template to clone.**
## Vary the composition — do not default to one layout
The brand system is fixed (colors, font, tiles, pills, gradients). The **composition is not.**
Every thumbnail must be a deliberate design choice, not a find-and-replace of the last one.
- **Pick the pattern from the content**, not from what you made last time (see the catalog below).
A release is a hero icon; a guide is editorial; a multi-item feature is a concept map. Only a
true "X with/via Y" integration should reach for the two-tile connect.
- **When you DO reuse a pattern** (e.g. a series like "self-host with Dokploy / Coolify / Railway"
where consistency is the point), still vary within it — the focal treatment, the pill copy, the
headline emphasis, the accent/texture. Series consistency is a choice the user should confirm,
not your default.
- **Actively avoid** shipping three near-identical images in a row. If the last thumbnail you made
used a layout, reach for a different one unless the user asked to match a set.
- Treat `template-two-tile.html` as **one** starting point among the patterns, not THE template.
For other patterns, compose fresh from `thumbnail.css` classes.
If a request is ambiguous about style, propose a pattern (and, for a series, ask whether to match
the existing set) before generating — don't silently reuse the previous layout.
## Why this pipeline produces quality (do not skip these)
1. **Deterministic fonts.** Geist is embedded as base64 `@font-face`. A Google Fonts `<link>`
does NOT load before the headless screenshot — the headline silently falls back to system
sans and looks off-brand. This is the single biggest failure mode. `render.py` handles it.
2. **Real assets, recolored.** ClassroomIO logo = the actual `apps/website/static/logo-512.png`,
recolored white via `filter: brightness(0) invert(1)`. Never redraw it.
3. **Official third-party logos.** For a partner/tool, fetch the OFFICIAL svg (e.g. Dokploy:
`https://dokploy.com/icon.svg`). Prefer an svg whose paths are `fill="white"` (or force white)
so it drops onto the blue tile cleanly. A wrong-but-close logo destroys credibility.
4. **Honest gradients, never frosted-white chips.** Tiles and pills are real blue gradients with
iridescent borders (see `thumbnail.css`). `rgba(255,255,255,0.1)` frosted glass reads off-brand.
## Design spec (source of truth)
The full pattern catalog and Figma-exact tokens live in
`company/blog-thumbnail-agent/README.md` — **read it before generating**, alongside the six
reference PNGs in that folder. `thumbnail.css` in this skill already encodes those tokens
(background vignette, glass tiles, pills, frame, title). Consume the CSS classes; don't re-derive.
### Pick the pattern from the content (each is a distinct look — rotate them)
- **A — Editorial / link:** blog posts, guides pointing to a URL. Frame on TOP. Link pill + optional `Guide` pill. Title centered or bottom-left. No tiles.
- **B — Concept map:** multi-item features (languages, integrations) as a connected node graph over a faint globe.
- **C — Before → After:** upgrades, limit increases. Two glass tiles + arrow, action pill below, title bottom. Frame on BOTTOM.
- **D — Hero icon:** releases, security, single-concept. One big centered glass tile, version/`New Feature` pill, 2-line title. Frame on BOTTOM.
- **Two-tile connect** (C/D hybrid): "X with/via Y" integrations — two tiles joined by an arrow node. `template-two-tile.html` is a starting point. **Powerful but overused — don't reach for it by reflex.**
Beyond the pattern, vary the levers that keep a set from looking identical: background
**texture** (chevrons / stacked cards / constellation / globe — match the topic), **title
placement** (centered vs bottom-left), **pill copy**, and the **focal treatment**. A guide about
one platform often reads better as Pattern A or D (single hero) than as another two-tile clone.
## Workflow
1. **Choose a pattern deliberately** and confirm intent if unstated: pattern, the partner/tool (and
whether to use its real logo), the headline, the pill text, and any URL. Recommend a pattern that
fits the content; don't over-ask. If this belongs to an existing series, ask whether to match that
set or give it a fresh look — do not silently reuse the last layout.
2. **Fetch third-party assets** if needed (official svg logo). Verify it renders white on blue.
3. **Author the HTML** in the scratchpad. Copy a template (e.g. `template-two-tile.html`) as a
starting point, or write fresh linking `thumbnail.css`:
- `<link rel="stylesheet" href="<abs-path-to-skill>/thumbnail.css" />`
- Use `data:image/png;base64,__CIO_LOGO_B64__` for the ClassroomIO logo (render.py fills it).
- Paste the partner's white svg inline inside a `.svg-icon` container.
- Keep everything except the headline low-contrast so it reads at thumbnail size.
4. **Render:** `python3 <skill>/render.py <input.html> <output.png>`. This inlines local
stylesheets, substitutes `__GEIST_WOFF2_B64__` / `__CIO_LOGO_B64__`, and screenshots at
1920×1080 (device scale 1). Requires Google Chrome at the standard macOS path.
5. **Review the PNG visually** (Read the image). Check: brand font actually applied, logos correct
and white, text not clipped, gradients present, ≤2 title lines. Iterate on the HTML if needed.
6. **Deliver:** save the final PNG to `~/Downloads/` (or a path the user names) and report it.
Output is ~880KB — under YouTube's 2MB thumbnail cap.
## Files in this skill
- `SKILL.md` — this guide.
- `thumbnail.css` — Figma-exact brand tokens (background, tiles, pills, connector, frame, title).
- `template-two-tile.html` — starting point for the two-tile connect pattern.
- `render.py` — inlines assets + renders HTML → PNG via headless Chrome.
Brand palette: deep blue `#0233BD` · brand blue `#2F6BF1` · light blue `#ADC3FF` · violet `#8071F2`.
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Render a ClassroomIO thumbnail HTML to a 1920x1080 PNG via headless Chrome.
Resolves the two stable brand assets from the repo and inlines them as base64
so the render is deterministic (a Google Fonts <link> does NOT load before the
screenshot — that is the #1 cause of off-brand headlines). Third-party logos are
NOT handled here: fetch the official svg per task and inline it in your HTML.
Placeholders replaced in the HTML before rendering:
__GEIST_WOFF2_B64__ -> apps/dashboard/static/fonts/geist/geist-latin.woff2
__CIO_LOGO_B64__ -> apps/website/static/logo-512.png
Usage:
python3 render.py <input.html> <output.png>
"""
import base64
import pathlib
import re
import subprocess
import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
GEIST = REPO_ROOT / "apps/dashboard/static/fonts/geist/geist-latin.woff2"
CIO_LOGO = REPO_ROOT / "apps/website/static/logo-512.png"
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
def b64(path: pathlib.Path) -> str:
if not path.exists():
sys.exit(f"Missing brand asset: {path}")
return base64.b64encode(path.read_bytes()).decode()
def inline_local_stylesheets(html: str, base_dir: pathlib.Path) -> str:
"""Replace <link rel="stylesheet" href="local.css"> with an inline <style>
so placeholders inside the CSS also get substituted."""
pattern = re.compile(r'<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>')
def replace(match: "re.Match[str]") -> str:
href = match.group(1)
if href.startswith(("http://", "https://", "//")):
return match.group(0)
css_path = (base_dir / href).resolve()
if not css_path.exists():
sys.exit(f"Linked stylesheet not found: {css_path}")
return f"<style>\n{css_path.read_text()}\n</style>"
return pattern.sub(replace, html)
def main() -> None:
if len(sys.argv) != 3:
sys.exit("Usage: python3 render.py <input.html> <output.png>")
src = pathlib.Path(sys.argv[1]).resolve()
out = pathlib.Path(sys.argv[2]).resolve()
if not src.exists():
sys.exit(f"Input HTML not found: {src}")
html = src.read_text()
html = inline_local_stylesheets(html, src.parent)
html = html.replace("__GEIST_WOFF2_B64__", b64(GEIST))
html = html.replace("__CIO_LOGO_B64__", b64(CIO_LOGO))
inlined = src.with_suffix(".inlined.html")
inlined.write_text(html)
subprocess.run(
[
CHROME,
"--headless",
"--disable-gpu",
"--hide-scrollbars",
"--force-device-scale-factor=1",
"--window-size=1920,1080",
f"--screenshot={out}",
f"file://{inlined}",
],
check=True,
)
print(f"Rendered {out}")
if __name__ == "__main__":
main()
@@ -0,0 +1,68 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="thumbnail.css" />
<style>
/* Hero-icon (Pattern D) layout */
.stage {
position: absolute; top: 132px; left: 0; right: 0;
display: flex; flex-direction: column; align-items: center;
}
.pill-wrap { margin-top: 52px; }
.title {
margin-top: 46px; text-align: center; font-size: 100px;
}
/* DigitalOcean mark sits a touch larger in the tile */
.tile .svg-icon { width: 168px; }
/* small floating accent squares around the hero tile */
.accent {
position: absolute; padding: 2px; border-radius: 16px;
background: linear-gradient(135deg, #0233BD 23%, #ADC3FF 42%, #8071F2 54%, #0233BD 82%);
box-shadow: 0 18px 36px rgba(6, 18, 66, 0.4);
}
.accent > div {
width: 100%; height: 100%; border-radius: 14px;
background: linear-gradient(145deg, #0233BD 10%, #2F6BF1 94%);
}
</style>
</head>
<body>
<div class="texture">
<div class="card-line" style="width:520px;height:520px;"></div>
<div class="card-line" style="width:780px;height:720px;border-radius:80px;"></div>
<div class="card-line" style="width:1060px;height:920px;border-radius:110px;"></div>
<div class="card-line" style="width:1380px;height:1140px;border-radius:140px;"></div>
<div class="spark" style="top:170px;left:360px;">✦</div>
<div class="spark" style="top:240px;right:330px;font-size:26px;">✦</div>
<div class="spark" style="bottom:250px;left:220px;">✦</div>
</div>
<!-- Frame on the BOTTOM edge (Pattern D) -->
<div class="frame bottom">
<div class="brand">
<img src="data:image/png;base64,__CIO_LOGO_B64__" alt="ClassroomIO" />
<div class="name">ClassroomIO</div>
</div>
<div class="domain"><span class="globe"></span><span>classroomio.com</span></div>
</div>
<div class="stage">
<div style="position:relative;">
<div class="accent" style="width:70px;height:70px;top:-34px;left:-64px;"><div></div></div>
<div class="accent" style="width:44px;height:44px;bottom:-18px;right:-58px;"><div></div></div>
<div class="tile-wrap"><div class="tile">
<div class="svg-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#ffffff"><path d="M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z"/></svg>
</div>
<div class="label">DigitalOcean</div>
</div></div>
</div>
<div class="pill-wrap"><div class="pill"><span>SELF-HOST GUIDE</span></div></div>
<div class="title">Self-Host ClassroomIO<br/><span class="tail">on </span>DigitalOcean</div>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="thumbnail.css" />
<style>
/* Layout-only overrides for the two-tile connect pattern */
.stage { position: absolute; top: 238px; left: 0; right: 0; display: flex; flex-direction: column; align-items: center; }
.tiles { display: flex; align-items: center; }
.pill-wrap { margin-top: 56px; }
.title { position: absolute; bottom: 70px; left: 0; right: 0; text-align: center; font-size: 104px; }
</style>
</head>
<body>
<div class="texture">
<div class="card-line" style="width:520px;height:520px;"></div>
<div class="card-line" style="width:760px;height:700px;border-radius:80px;"></div>
<div class="card-line" style="width:1040px;height:900px;border-radius:110px;"></div>
<div class="card-line" style="width:1360px;height:1120px;border-radius:140px;"></div>
<div class="spark" style="top:150px;right:360px;">✦</div>
<div class="spark" style="bottom:250px;left:150px;">✦</div>
<div class="spark" style="bottom:180px;right:220px;font-size:26px;">✦</div>
</div>
<!-- Frame on ONE edge only. Two-tile/hero patterns use the TOP frame here. -->
<div class="frame top">
<div class="brand">
<img src="data:image/png;base64,__CIO_LOGO_B64__" alt="ClassroomIO" />
<div class="name">ClassroomIO</div>
</div>
<div class="domain"><span class="globe"></span><span>classroomio.com</span></div>
</div>
<div class="stage">
<div class="tiles">
<!-- Left tile: ClassroomIO -->
<div class="tile-wrap"><div class="tile">
<img src="data:image/png;base64,__CIO_LOGO_B64__" alt="ClassroomIO" />
<div class="label">ClassroomIO</div>
</div></div>
<!-- Connector: arrow node between the two tiles -->
<div class="connector">
<div class="dash"></div>
<div class="node"><div class="node-inner">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M13 6l6 6-6 6"/></svg>
</div></div>
<div class="dash"></div>
</div>
<!-- Right tile: the partner/tool. Paste the OFFICIAL white svg inside .svg-icon
(fetch it per task; e.g. Dokploy: https://dokploy.com/icon.svg). -->
<div class="tile-wrap"><div class="tile">
<div class="svg-icon"><!-- PARTNER_SVG_HERE --></div>
<div class="label">Partner</div>
</div></div>
</div>
<!-- Category / action pill -->
<div class="pill-wrap"><div class="pill"><span>DEPLOY IN MINUTES</span></div></div>
</div>
<!-- Title: bold white, ≤2 lines. Lighter tail via <span class="tail"> for hierarchy. -->
<div class="title">Primary Headline<br/><span class="tail">with </span>Partner</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
/* ClassroomIO thumbnail brand tokens — Figma-exact. Consume these; do not eyeball.
Palette: deep blue #0233BD · brand blue #2F6BF1 · light blue #ADC3FF · violet #8071F2 */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: block;
src: url(data:font/woff2;base64,__GEIST_WOFF2_B64__) format('woff2');
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1920px; height: 1080px; }
body {
font-family: 'Geist', sans-serif;
width: 1920px; height: 1080px; position: relative; overflow: hidden;
/* Royal-blue full-bleed with center-bright vignette */
background: radial-gradient(120% 110% at 50% 42%, #2B5BE0 0%, #1D49C7 42%, #12318F 78%, #0E2A7E 100%);
}
/* --- faint white texture (keep ~5-12% opacity; never competes with title) --- */
.texture { position: absolute; inset: 0; pointer-events: none; }
.card-line {
position: absolute; top: 50%; left: 50%;
border: 2px solid rgba(255, 255, 255, 0.06);
border-radius: 60px; transform: translate(-50%, -58%);
}
.spark { position: absolute; color: #8FE8DC; opacity: .85; font-size: 34px; }
/* --- frame (logo + wordmark on ONE edge, domain on the other; never both edges) --- */
.frame { position: absolute; left: 0; right: 0; display: flex; justify-content: space-between; padding: 0 72px; align-items: center; }
.frame.top { top: 52px; }
.frame.bottom { bottom: 52px; }
.brand { display: flex; align-items: center; gap: 18px; }
.brand img { width: 52px; height: 52px; filter: brightness(0) invert(1); }
.brand .name { color: #fff; font-size: 40px; font-weight: 700; letter-spacing: -1px; }
.domain { display: flex; align-items: center; gap: 14px; color: #fff; font-size: 34px; font-weight: 600; }
.domain .globe { width: 36px; height: 36px; border: 2.5px solid #fff; border-radius: 50%; position: relative; opacity: .92; }
.domain .globe::before { content: ''; position: absolute; inset: -2.5px 8px; border: 2.5px solid #fff; border-radius: 50%; }
.domain .globe::after { content: ''; position: absolute; left: -2.5px; right: -2.5px; top: 50%; height: 0; border-top: 2.5px solid #fff; transform: translateY(-1px); }
.domain span { text-decoration: underline; text-underline-offset: 6px; }
/* --- glass icon tile (patterns C & D): blue gradient fill + iridescent border, NOT frosted white --- */
.tile-wrap {
padding: 2.5px; border-radius: 44px;
background: linear-gradient(135deg, #0233BD 23%, #ADC3FF 42%, #8071F2 54%, #0233BD 82%);
box-shadow: 0 40px 80px rgba(6, 18, 66, 0.45);
}
.tile {
width: 300px; height: 300px; border-radius: 42px;
background: linear-gradient(145deg, #0233BD 10%, #2F6BF1 94%);
display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;
}
.tile::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 55%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0)); }
.tile img { width: 172px; height: 172px; filter: brightness(0) invert(1); position: relative; z-index: 1; }
.tile .svg-icon { width: 186px; height: auto; position: relative; z-index: 1; }
.tile .svg-icon svg { width: 100%; height: auto; }
.tile .label { position: absolute; bottom: 24px; left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.85); font-size: 26px; font-weight: 600; letter-spacing: .5px; }
/* --- connector (two-tile / before->after) --- */
.connector { display: flex; align-items: center; margin: 0 -6px; z-index: 2; }
.connector .dash { width: 44px; height: 4px; border-radius: 4px; background: rgba(255, 255, 255, 0.35); }
.connector .node {
width: 74px; height: 74px; margin: 0 10px; border-radius: 50%;
padding: 2.5px; background: linear-gradient(135deg, #0233BD 23%, #ADC3FF 42%, #8071F2 54%, #0233BD 82%);
box-shadow: 0 16px 32px rgba(6, 18, 66, 0.4);
}
.connector .node-inner { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(145deg, #0233BD 10%, #2F6BF1 94%); display: flex; align-items: center; justify-content: center; }
.connector .node-inner svg { width: 36px; height: 36px; }
/* --- pill / badge: fully gradient-built (fill + iridescent border + clipped gradient text) --- */
.pill-wrap {
padding: 2px; border-radius: 999px;
background: linear-gradient(180deg, #0233BD 24%, #C1D4FF 55%, #93B4FF 80%);
box-shadow: 0 16px 32px rgba(6, 18, 66, 0.35);
}
.pill { height: 70px; padding: 0 40px; border-radius: 999px; background: linear-gradient(180deg, #0233BD 0%, #0C4BDA 100%); display: flex; align-items: center; }
.pill span {
font-size: 30px; font-weight: 700; letter-spacing: .5px;
background: linear-gradient(180deg, #FFFFFF 0%, #8EACFF 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
/* --- title: bold white Title Case, tight tracking; lighter 78%-white tail for hierarchy --- */
.title { color: #fff; font-weight: 700; line-height: 1.02; letter-spacing: -3px; }
.title .tail { font-weight: 500; color: rgba(255, 255, 255, 0.78); }