| name | overlay-svg-text |
| description | How to overlay on-brand SVG text on Lossless OG / share imagery — Hack Bold gradient-filled h1, thin sans eyebrow, Poor Story handwritten note. Use whenever a generated OG image needs title/eyebrow/sub text composited on top before it ships (the empty-region zone from `generate-consistent-og-images` is the canvas this skill paints into), whenever an unfurl preview looks too anonymous without a wordmark or subtitle, whenever a fundraise-deck slide needs a brand-flavored title overlay on a hero image, whenever the user says "overlay text on the OG image", "add a title to the banner", "make the unfurl say something", "drop a wordmark on this", or names this skill directly. Encodes the brand-wide type system (Hack Bold for gradient h1, thin sans for eyebrow, Poor Story for handwritten notes), the per-site gradient-from-DESIGN.md discipline (the brand SVGs are raster-baked references, not editable gradient sources — the editable gradient lives in each project's DESIGN.md), the canonical SVG fill-with-gradient pattern (simpler and more portable than mask/union for this use case), and the sharp-based compositing pipeline that writes JPEG-out for delivery per the open-graph-share-seo-geo skill. |
Overlay SVG Text on Brand Imagery
Two layers of brand identity on every share image. The base image — generated via generate-consistent-og-images — already follows the empty-region-first composition rule, leaving the top 1/3 of the frame deliberately clean. This skill paints that clean zone with three text elements: a thin sans eyebrow, a Hack-Bold gradient-filled h1, and a Poor Story handwritten note. Everything else (layout, colors, font weights) is locked at the project's DESIGN.md level.
When to use this skill
- An OG image was just generated and needs a wordmark / title overlay before it ships
- An existing OG image looks too anonymous in unfurls — needs a brand-flavored title or subtitle
- A fundraise-deck slide / hero illustration needs a brand-consistent title overlay
- A user-facing share preview is launching and needs the project's name + a one-liner baked in
- The user says: "overlay text on this", "add a title to the banner", "make the unfurl say something", "drop a wordmark on this", "compose the OG with text"
The Lossless brand type system for overlays
Three text elements, in canonical vertical order from top of frame to bottom of overlay zone:
| Element | Font | Weight | Color | Notes |
|---|
| Eyebrow | Project's display sans (or Inter as fallback) | Thin (200) or Light (300) | white at 70–80% alpha, OR --color-text-soft | Often uppercase with wide tracking (0.18em–0.24em); the project's typography.eyebrow token wins if present |
| H1 (gradient) | Hack Bold (Hack Nerd Font Mono Bold is what most Lossless dev machines have installed) | Bold (700) | gradient-filled from the project's imagery.gradient or components.gradient-text.background in DESIGN.md | Monospaced + gradient is the brand's "data" aesthetic — short noun-phrase or shell-command-style; never sentence-case prose |
| Note (handwritten) | Poor Story (Google Fonts, Korean handwriting family) — preferred. Any cartoonish handwriting font is acceptable as fallback (e.g. Caveat, Patrick Hand, Indie Flower) | Regular | white (#ffffff) — almost always | The handwriting/Hack contrast is the brand move; never serif, never another mono |
The three elements together compose the brand's signature: clean thin sans label → monospaced gradient title → cartoonish white handwritten phrase. The handwriting/mono contrast is the most-defining axis; don't lose it by going all-sans or all-mono.
Where the gradient comes from
Not from trademark__The-Lossless-Group--Vibrant.svg or gradient__The-Lossless-Group--Rounded-Rectangle.svg. Those files are raster-baked exports (each is a <clipPath> of glyph/shape paths with a base64 JPEG as the fill) and the gradient inside them isn't an editable <linearGradient> element — it's pixel data. Treat them as visual reference only.
The editable gradient for any given project lives in that project's DESIGN.md. Look in this order:
imagery.gradient — a Lossless extension key if the project has declared an OG-specific gradient
components.gradient-text.background — most Lossless splashes define this for their hero gradient-text move
- The brand-spine colors (
accent, accent-warm, thread — or primary, secondary, tertiary) at 0% / 55% / 100% stops
For the lossless-agent-skills splash, that resolves to:
linear-gradient(90deg, #5eead4 0%, #fbbf24 55%, #e879f9 100%)
(teal) (amber) (magenta)
For the ai-labs splash:
linear-gradient(95deg, #ffb733 0%, #5cc8ff 55%, #d96fff 100%)
(sodium) (cyan) (plum)
Per-project gradient + brand-wide type system = images that look like family with a project on each member. Use Lossless Group's brand gradient (teal/violet/etc. — the JPEG-baked one) only when the overlay belongs to The Lossless Group itself, e.g. a corporate splash, an "About" page, a consulting-firm asset. Site-specific imagery uses site-specific gradients.
The SVG-text-with-gradient technique
Two compatible patterns. The first is the default; the second is for the rare cases where you need the gradient to extend past the letterforms or to be clipped by a more complex shape.
Pattern A — fill="url(#gradient)" on <text> (default)
<svg width="1312" height="736" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#5eead4"/>
<stop offset="55%" stop-color="#fbbf24"/>
<stop offset="100%" stop-color="#e879f9"/>
</linearGradient>
</defs>
<text x="64" y="120"
font-family="Hack Nerd Font Mono, Hack, ui-monospace, monospace"
font-weight="700"
font-size="96"
fill="url(#brand)">
Agent Skills
</text>
</svg>
The gradient renders inside each glyph's bounding box. Simple, portable across librsvg / resvg / browser / sharp. This is what 95% of overlays should use.
Pattern B — gradient rectangle clipped by text (mask/union style)
<defs>
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="0">...</linearGradient>
<clipPath id="text-clip">
<text x="64" y="120"
font-family="Hack Nerd Font Mono"
font-weight="700"
font-size="96">
Agent Skills
</text>
</clipPath>
</defs>
<rect x="0" y="0" width="1312" height="200"
fill="url(#brand)"
clip-path="url(#text-clip)"/>
Use this when the gradient should sweep across a region wider than the text itself (e.g., the gradient should flow as if it were on a banner behind the text but be visible only through the letterforms). For all simple cases, Pattern A is shorter and renders identically.
The compositing pipeline
The base OG image is a JPEG; the overlay is an SVG; the output is a JPEG with the SVG painted on top. The cleanest tool for this is sharp (Node, librsvg under the hood). The canonical script lives at templates/compose-overlay.mjs in this skill.
pnpm add -D sharp
node templates/compose-overlay.mjs \
--base public/ogimage__Lossless-Agent-Skills--BannerImage.jpg \
--overlay overlays/banner.svg \
--out public/ogimage__Lossless-Agent-Skills--BannerImage--overlaid.jpg \
--quality 90
The script:
- Reads the base JPEG and gets its dimensions
- Reads the SVG, validates that its
width/height match the base (or scales it)
- Composites the SVG at (0, 0) — the SVG positions its own elements internally
- Writes the result as JPEG at the chosen quality (default 90; the
open-graph-share-seo-geo skill targets ~85–95 for OG)
- Archives the prior canonical JPEG to
.ogimage-archive/<name>--<YYYY-MM-DD>.jpg first if the output path already exists
Alternative renderers (when sharp isn't an option)
rsvg-convert (librsvg CLI, Homebrew): rsvg-convert overlay.svg -o overlay.png then composite with ffmpeg -i base.jpg -i overlay.png -filter_complex "[0][1]overlay=0:0" out.jpg
resvg (Rust, faster than librsvg): resvg overlay.svg overlay.png + same ffmpeg step
- ImageMagick:
convert base.jpg overlay.svg -composite out.jpg — works but text rendering is less reliable than librsvg
Sharp is the documented default because it's already in the Astro Knots ecosystem and produces byte-identical output to librsvg-CLI without the two-step PNG→JPEG dance.
Font availability
Sharp's renderer (librsvg under the hood) reads fonts via fontconfig on Linux/macOS and via the system font store on Windows. For the local-dev case where fonts are installed in ~/Library/Fonts/ (macOS) or ~/.fonts/ (Linux), no extra step is needed — just reference the family name in the SVG's font-family attribute.
For CI builds (GitHub Actions, Vercel build container, etc.) where the host doesn't have Hack or Poor Story installed, embed the WOFF2 binary as a data URL inside the SVG:
<defs>
<style type="text/css">
@font-face {
font-family: 'Hack Bold';
src: url(data:font/woff2;base64,d09GMgABAAAA...) format('woff2');
font-weight: 700;
}
@font-face {
font-family: 'Poor Story';
src: url(data:font/woff2;base64,d09GMgABAAAA...) format('woff2');
font-weight: 400;
}
</style>
</defs>
Recipe to generate the data URL:
fonttools ttLib --woff2 ~/Library/Fonts/HackNerdFontMono-Bold.ttf
base64 -i HackNerdFontMono-Bold.woff2 | tr -d '\n' > hack-bold.b64
A WOFF2-embedded SVG balloons by ~80–250 KB (Hack Bold is ~200 KB WOFF2; Poor Story is ~80 KB WOFF2). Acceptable for a one-time composited PNG/JPEG output — the resulting raster is the only thing that ships, the SVG is intermediate.
Layout — where the three elements sit inside the empty-region zone
The base OG image was generated with Top 1/3 of frame is empty negative space as the locked composition. The overlay's safe zone is therefore the top third of the canvas. Within that:
┌────────────────────────────────────────────────┐ ← y = 0
│ ┌─ padding 6% of height ─┐ │
│ │ │ │
│ ┃ EYEBROW · THIN · ALLCAPS ← ~12% down (eyebrow baseline)
│ │ │ │
│ ┃ Gradient H1 Title ← ~22% down (h1 baseline; bold mono)
│ │ │ │
│ ┃ ✎ a handwritten phrase, white ← ~30% down │
│ │ │ │
└──┴─────────────────────────┴────────────────────┘ ← y = floor(height / 3)
↑ ↑
left padding 5–6% right edge stays clean
Vertical positions are expressed as percentages of the canvas height so the same SVG template re-aspects cleanly when fed different dimensions. For the four canonical aspect ratios from generate-consistent-og-images:
| Format | Dimensions | Top-zone height | Eyebrow y | H1 y | Note y |
|---|
BannerImage | 1312×736 | 245 px | ~88 px | ~162 px | ~221 px |
BannerImageTall | 896×1120 | 373 px | ~134 px | ~246 px | ~336 px |
PortraitImage | 736×1312 | 437 px | ~157 px | ~289 px | ~394 px |
SquareImage | 1024×1024 | 341 px | ~123 px | ~225 px | ~307 px |
The compose script computes these from the base image dimensions; never hard-code pixel positions for a specific aspect ratio in the template SVG. Author the SVG once with percentage-based y-values; the script substitutes them per output.
The flow
Five steps. Steps 1–2 are setup; steps 3–5 are the per-image work.
Step 1 — Confirm the base imagery is ready
The base JPEGs must already exist (output of generate-consistent-og-images). If they don't, hand off to that skill first. The overlay paints onto what's there; it can't conjure a base.
Step 2 — Resolve the gradient
Read the project's DESIGN.md:
- Look for
imagery.gradient (an extension key on some projects)
- Fall back to
components.gradient-text.background
- Fall back to constructing
linear-gradient(<angle>, <accent>, <accent-warm>, <thread>) from the project's brand-spine semantic colors
For the lossless-agent-skills splash:
linear-gradient(90deg, #5eead4 0%, #fbbf24 55%, #e879f9 100%)
For ai-labs:
linear-gradient(95deg, #ffb733 0%, #5cc8ff 55%, #d96fff 100%)
Translate to SVG <linearGradient> stops; x1/y1/x2/y2 derives from the CSS angle.
Step 3 — Author the overlay SVG
Use templates/overlay.svg as the starting point. Fill in:
- The gradient stops (from step 2)
- The eyebrow text + the project's display-sans family
- The H1 text (short — 1–3 words; the gradient sweeps across one phrase, not a paragraph)
- The handwritten note (a phrase, never a full sentence; ≤32 chars reads best at any aspect ratio)
- The canvas
width and height (match the base image exactly)
For multi-aspect runs (BannerImage + BannerImageTall + PortraitImage + SquareImage), author one SVG template with percentage-based positions and let the compose script re-render it per output. Don't hand-author four separate SVGs.
Step 4 — Composite
Run node compose-overlay.mjs --base <jpg> --overlay <svg> --out <jpg> (or the per-format loop variant in the same script).
The script archives any existing canonical at .ogimage-archive/<name>--<YYYY-MM-DD>.jpg before writing the new one (per the generate-consistent-og-images preservation discipline).
Step 5 — Verify visually
Open the output JPEG. Check:
- Eyebrow is readable but not loud — wide tracking + thin weight + 70–80% white alpha. If it's the loudest element, the rest of the brand hierarchy collapses.
- H1 gradient is visible inside the letterforms, not bleeding outside them — if the gradient looks pixelated or banded, the gradient stops are too close together or the canvas is too narrow.
- Handwritten note reads as a note, not a headline — Poor Story at the right size feels like a margin scribble. If it looks like a second headline, scale down or pick a more whimsical handwriting fallback.
- No clipping at the safe-zone boundary — the bottom of the handwritten note should be ≥1.5em above the subject's top edge in the base image.
If the overlay clips the subject (text overlapping the robots / bench / terminal in the base), the base image's empty-region zone wasn't large enough. Re-generate the base via generate-consistent-og-images with a longer "Top 1/3" clause or Top 40% in the prompt; don't shrink the overlay text below readable size to work around it.
Anti-patterns
- Cloning the Lossless Group brand SVGs as the gradient source. They're raster-baked — there's no real gradient to extract, only a JPEG inside a clipPath. Use the project's DESIGN.md gradient instead. The brand SVGs are visual reference for the aesthetic (mono + gradient), not source data.
- Putting Hack on the eyebrow or the handwritten note. Hack is reserved for the gradient H1. The mono/handwriting contrast is the brand move; collapsing both to mono erases it.
- Sentence-case prose in the H1. Gradient on a phrase that wraps to two lines breaks the gradient's continuity — letters at the start of line 2 read in colors that don't follow from where line 1 left off. Keep H1 to 1–3 words, one line.
- A handwritten note longer than ~32 characters. The handwriting font becomes hard to read past that length; the move is "marginalia," not "subtitle."
- Hard-coded pixel positions for a specific aspect ratio in the SVG template. Use percentage-based y-values + let the compose script re-render per output. Otherwise you maintain four separate SVGs and they drift.
- Outputting PNG when the deliverable is OG / share imagery. Per the
open-graph-share-seo-geo skill: JPEG-over-WebP for unfurler compatibility. Sharp's .jpeg({ quality: 90 }) is the canonical output step.
- Overwriting the canonical OG JPEG in place. Same preservation rule as
generate-consistent-og-images — archive the prior bytes to .ogimage-archive/<name>--<YYYY-MM-DD>.jpg before writing the new one. The unfurler URL stays stable; byte history survives.
- Embedding fonts every time when running locally. Wasteful when fontconfig already finds Hack and Poor Story in
~/Library/Fonts/. Embed only for CI / cross-machine portability or when the SVG itself is the deliverable.
- Letting the eyebrow tracking go below
0.12em. The brand eyebrow is wide-tracked on purpose; tight tracking reads as body type and breaks the hierarchy.
- Choosing a serif or another mono as the handwriting fallback when Poor Story is unavailable. The whole point of the handwritten element is the contrast against Hack. A serif or another mono erases the contrast. Pick Caveat, Patrick Hand, Indie Flower, Shadows Into Light, etc. — cartoonish-handwriting only.
See also
templates/overlay.svg — the canonical SVG scaffold with placeholders for gradient stops, eyebrow, h1, and handwritten note
templates/compose-overlay.mjs — the canonical Node script that composites SVG onto JPEG via sharp, with multi-aspect loop + archive discipline
generate-consistent-og-images skill — the upstream skill that produces the base JPEGs this skill overlays text onto; defines the empty-region-first composition that creates the overlay safe zone
maintain-design-md skill — where the gradient stops actually live (components.gradient-text.background or imagery.gradient); this skill is a consumer of those tokens, not an author
open-graph-share-seo-geo skill — the delivery-side discipline (JPEG-over-WebP, cache headers, the og:image:type invariant); cares that the overlaid JPEG ends up on the right URL, not how the text was painted
- Google Fonts — Poor Story: https://fonts.google.com/specimen/Poor+Story
- Source Foundry — Hack: https://sourcefoundry.org/hack/