Stop using stock photos. Generate accessible, lightweight SVG figures for any blog or CMS - flow diagrams, comparison bar charts, taxonomy/Venn diagrams, annotated terminal mocks, and 1600x840 OG feature cards. Hand-authored SVG (no embedded fonts, no external assets, no AI-image latency) with a consistent palette, screen-reader metadata (title + desc + aria-labelledby), and a figcaption-required handoff to the writer. Rasterizes to compressed PNG ready for Ghost, WordPress, Webflow, or any static-site generator. Built for content marketers, indie hackers, and dev-tool blogs that want unique illustrations on every post without paying a designer or burning Midjourney credits. Trigger when the user says: 'add a figure to the post', 'illustrate this comparison', 'draw a flow diagram for X', 'make a feature/OG image', or any request to produce a chart/diagram for editorial use.
Instrucciones de origen · Vista previa de solo lectura
name
blog-figure-svg
description
Stop using stock photos. Generate accessible, lightweight SVG figures for any blog or CMS - flow diagrams, comparison bar charts, taxonomy/Venn diagrams, annotated terminal mocks, and 1600x840 OG feature cards. Hand-authored SVG (no embedded fonts, no external assets, no AI-image latency) with a consistent palette, screen-reader metadata (title + desc + aria-labelledby), and a figcaption-required handoff to the writer. Rasterizes to compressed PNG ready for Ghost, WordPress, Webflow, or any static-site generator. Built for content marketers, indie hackers, and dev-tool blogs that want unique illustrations on every post without paying a designer or burning Midjourney credits. Trigger when the user says: 'add a figure to the post', 'illustrate this comparison', 'draw a flow diagram for X', 'make a feature/OG image', or any request to produce a chart/diagram for editorial use.
Produces SVG figures intended for blog posts: in-line illustrations (1 per ~500 body words is the rule of thumb) and a templated OG feature card. Output is a clean SVG file (the editable source) rasterized to a compressed PNG (what the post references). Every figure carries title + desc + role="img" so screen readers can read it.
This skill is platform-agnostic — the SVG and PNG it produces work in any CMS (Ghost, WordPress, Webflow, Sanity) or static-site generator (Hugo, Astro, Eleventy, Jekyll, Next-MDX). It complements seo-blog-writer (handles the publish step for whatever platform you're on) and blog-topic-research (validates the topic). Use it during the illustration step of writing a post — after the prose is stable so the anchor sentences are final.
All variants write to tmp/blog-drafts/<slug>-<N>-<short-name>.svg (editable source, gitignored), then rasterize to <slug>-<N>-<short-name>.png (uploaded to the blog CDN).
Before you start
The skill expects a working directory it can write into. Default: tmp/blog-drafts/. The PNG rasterizer requires one of:
Plus pngquant (or oxipng) for compression — typical 60-80% size reduction with no visible quality loss. Core Web Vitals and ad-network reviews (Mediavine, Raptive) care about image weight.
"no SVG rasterizer found - install one of magick, rsvg-convert, inkscape, cairosvg"
command
command
echo
"no PNG compressor - install pngquant or oxipng"
The three illustration shapes
Match each figure to a paragraph the reader has just finished, and to one concrete information structure:
Shape
Use when the post...
Variant
Comparison
...cites two or more numbers (prices, latencies, accuracy, counts)
compare (bar chart)
Taxonomy
...introduces named categories (e.g. workflow / agent / RPA, or trigger / action / filter)
taxonomy (Venn, hierarchy, or labelled groups)
Process / flow
...describes a "how to" sequence, integration topology, or decision tree
flow (horizontal flow with named steps)
CLI / API mock
...shows command output, an error message, or a config blob
terminal (annotated terminal mock)
Title card
...needs an OG feature image
feature (1600x840 templated card)
Never plot data the post doesn't already cite. If you can't identify even one information structure to illustrate, skip — note in the report "no figures: post is too short / too definitional."
Hard rule for editorial pipelines: any post >=800 words needs at least 1 figure; figure count = max(1, body_words // 500). Sub-800-word definitional explainers are the only legitimate zero-figure case.
Palette and typography
Pick from these hex values. No new hues — consistency across figures is the brand:
Hex
Role
#3b82f6
accent blue — primary data series
#fb923c
orange — secondary series
#10b981
green — tertiary / positive
#0b0b11
text — titles, primary callouts
#475569 / #6b7280 / #9ca3af
greys — secondary labels, axis ticks
#cbd5e1 / #94a3b8
light greys — gridlines, weak series
#fafafa
background fill
Typography:font-family="ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif" only. No embedded web fonts — they fail to load in feed readers, dark-mode previews, and AMP renders. Sizes: title 20px bold, section labels 14-16px, axis labels 11-13px.
ViewBox:viewBox="0 0 800 <height>" for inline figures (a sane width for most CMS content columns, including Ghost's Casper, the WordPress block editor, and Hugo / Astro defaults); viewBox="0 0 1600 840" for OG cards. Do not set root width/height attributes — let the host theme scale.
SVG skeleton (every figure)
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 800 360"font-family="ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif"role="img"aria-labelledby="t1 d1"><titleid="t1">Short, informative title - what the figure shows</title><descid="d1">Long-form description for screen readers - what the bars/circles/lines depict, including all numbers shown on screen</desc><rectwidth="800"height="360"fill="#fafafa"/><!-- bars / circles / paths / labels --></svg>
Accessibility checklist:
role="img" on the root <svg>.
<title> + <desc> referenced via aria-labelledby (NOT aria-describedby — the former covers both).
Suffix IDs with the figure number (t1/d1, t2/d2, ...) so multiple figures on one page don't collide.
<desc> includes every number visible in the figure (screen readers can't OCR the chart).
Honesty: never round towards a more dramatic gap, never anchor an axis to inflate differences. If the data is "practitioner observation, not a measured study," say so in <desc> and in a small grey caption inside the figure.
For: the post's hero image (Ghost feature_image, WordPress featured_media, the static-site front-matter feature_image field, OG previews, social cards). One per post.
The card uses a tinted gradient background, a 24px grid pattern at 7% opacity, a soft radial highlight, and either a giant accent number (when the headline contains a 1-3 digit number) or a placeholder icon slot. Brand text (your wordmark, pill label) is configurable.
Customising the hero icon: replace the placeholder <g> block with cluster-specific iconography from your project. Keep stroke width 5-9, viewBox-relative coordinates (drawn for a 280x280 box), and stroke-only fills so the icon reads at thumbnail size in social previews. Examples (n8n nodes, code brackets, agent graph, RPA grid) are easy to author — see the feature script's structure.
Rasterize SVG to PNG
The SVG is the editable source. The blog references PNG only — most CMSes deliver PNG more reliably through their CDN than SVG.
# Preferred: ImageMagick at 192 DPI (renders text at 2x for sharpness)for svg in tmp/blog-drafts/<slug>-*.svg; do
png="${svg%.svg}.png"
magick -density 192 -background white "$svg" -resize 1600x "$png"done# Or one of the fallbacks:
rsvg-convert -w 1600 -b white in.svg -o out.png
inkscape --export-type=png --export-width=1600 in.svg
python3 -c "import cairosvg; cairosvg.svg2png(url='in.svg', write_to='out.png', output_width=1600)"
-density 192 renders text at 2x before resize (sharpness). -background white prevents black halos around antialiased edges. -resize 1600x is the practical ceiling for a CMS content column.
Compress before upload
ImageMagick output is 200-400 KB per figure; pngquant typically cuts that 60-80% with no visible quality loss.
for png in tmp/blog-drafts/<slug>-*.png; do
pngquant --skip-if-larger --strip --output "$png" --force 256 "$png" || truedonels -lh tmp/blog-drafts/<slug>-*.png
If pngquant isn't installed, oxipng -o 4 tmp/blog-drafts/<slug>-*.png is a slower fallback. If neither is available, surface to the user and proceed — don't block the post on compression.
Verify the PNG
# Confirm dimensions and bit depth
magick identify tmp/blog-drafts/<slug>-*.png 2>/dev/null \
|| python3 -c "from PIL import Image; import sys; [print(p, Image.open(p).size) for p in sys.argv[1:]]" tmp/blog-drafts/<slug>-*.png
Open each PNG locally and confirm: text is sharp at 100% zoom, no missing glyphs, no black halos.
Embed in the post
For each figure, identify the anchor sentence in the draft — the closing </p> of the paragraph the figure should appear after. Pick a phrase distinctive enough that str.replace finds exactly one match.
Insert with a generic <figure> block (renders cleanly in every major CMS theme and every static-site generator's default Markdown→HTML pipeline):
<figure><imgsrc="<uploaded-png-url-or-relative-path>"alt="<full description with all numbers and labels>"loading="lazy"><figcaption>One sentence restating the takeaway in plain English (15-30 words).</figcaption></figure>
Caption rules:
Required on every figure. No bare <img> and no <figure> without a <figcaption>. The seo-blog-writer skill's bundle validation refuses figures without captions.
One sentence, 15-30 words, restating the takeaway in plain English (not "Figure showing X" — say what the reader should conclude).
Restate every label and number visible in the figure. Screen readers read alt, not the figure.
50-200 chars. Longer than the caption.
Verify each PNG URL appears exactly once in the draft:
python3 -c "
import pathlib, re, sys
html = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')
for m in re.finditer(r'src=\"([^\"]+\.png)\"', html):
print(m.group(1))
" tmp/blog-drafts/<slug>.draft.html | sort | uniq -c
Each URL should print 1. Zero = anchor missed; >1 = anchor matched multiple paragraphs (extend the anchor).
Upload to your CMS
This skill doesn't ship a CMS uploader — the seo-blog-writer skill handles auth and the upload endpoint for each platform it targets. After generating PNGs:
For Ghost:seo-blog-writer's Ghost adapter exposes an image-upload snippet (POST to /ghost/api/admin/images/upload/ with the Admin API JWT).
For WordPress:seo-blog-writer's WordPress adapter posts to /wp-json/wp/v2/media with application-password auth.
For static-site generators (Hugo, Astro, Eleventy, Jekyll, Next-MDX): drop the PNGs into the project's static / public / assets directory and reference relative paths in the figure tag.
For other CMSes (Webflow, Sanity, Strapi, Contentful): write a 20-line adapter that POSTs the PNG to the platform's media endpoint, then splice the returned URL.
Failure modes
Symptom
Cause
Fix
magick: no decode delegate on .svg
ImageMagick built without rsvg
Fallback: rsvg-convert, inkscape, or cairosvg
Text rendered as boxes / missing glyphs in PNG
Embedded font referenced but not installed
Use only generic ui-sans-serif, system-ui font families; no @font-face
Black halos around shapes in PNG
Antialiased SVG rendered against a transparent background
Pass -background white to ImageMagick
PNG looks blurry
Rasterized at 96 DPI
Use -density 192 (or -w 1600 with rsvg/cairosvg)
aria-labelledby ignored by screen readers
Missing role="img" on the root <svg>
Add role="img" — without it, the SVG is treated as a graphic group
Feature card text overflows the 1600x840 canvas
Headline longer than ~120 chars
Truncate headline or use the longest tier (60pt, 4 lines, 32 chars/line)
Figcaption missing on a <figure>
Manually pasted <img> not wrapped in <figure>
Wrap in <figure>...<figcaption>...</figcaption></figure> — every figure needs a caption
Companion skills
blog-topic-research — validates that a long-tail topic has real demand signals before drafting.
seo-blog-writer — drafts, scrubs, AI-SEO-audits, and publishes the post to your CMS (Ghost, WordPress, or static-site) via the platform adapter.
Together, the three form a complete long-tail SEO publishing pipeline: research the topic, write the post, illustrate it, publish.