Our review
Generates branded PNG chart visualizations for TSVC articles by extracting data from research files, creating bilingual HTML templates, and taking screenshots via Playwright.
Strengths
- Automates creation of branded, bilingual (English+Spanish) charts
- Enforces design constraints and density limits for readability
- Integrates with article pipeline to insert image references automatically
- Uses local assets and no external CDN for reliability
Limitations
- Requires Playwright and Node.js setup
- Only works for TSVC brand and specific slug structure
- Limited to max 5 bars, 3 cards, etc., may need splitting complex data
When you need to add professional, branded charts to a TSVC article from research data quickly and consistently.
For simple text-based articles or when data is too large for the density limits.
Security analysis
CautionThe skill uses Bash to run a screenshot script that launches a local HTTP server and uses Playwright to capture chart images. No destructive or exfiltrating commands are present, and all operations are scoped to generating visualizations within the article output directory. The risk is low but present due to tool usage.
- •Uses Bash to execute screenshot.js, which starts a local HTTP server and uses Playwright to capture chart images; this is a legitimate purpose but involves running system commands.
Examples
Use the create-visualizations skill for slug 'startup-accelerator-programs' to generate charts from the research data.I have an article slug 'fundraising-tips'. Please run the visualization pipeline on it.Execute the create-visualizations pipeline for the slug 'market-sizing-guide' to produce bilingual charts and insert them.name: create-visualizations description: Generate TSVC-branded PNG chart visualizations (English + Spanish) from an article's research data. Use after write-article has produced output/<slug>/. Scans research files, presents 3-5 chart options, generates bilingual HTML templates, screenshots to PNG via Playwright in a single pass, and inserts image references into the article. argument-hint: [slug] allowed-tools: Task, Read, Write, Edit, Bash, Glob
Create Visualizations — TSVC Chart Pipeline
You are a visualization pipeline for The Startup VC (thestartupvc.com). Given an article slug, you extract chart-worthy data from research files, present options, generate branded HTML charts, screenshot them to PNG, and insert image references into the final article.
Slug: $ARGUMENTS
Design Philosophy
Clarity over cleverness. These charts help entrepreneurs make business decisions. If data is hard to parse at a glance, the design has failed.
- One insight per visualization. If you need a second comparison, make a second chart.
- Data hierarchy. The most important number is the largest, boldest element.
- Generous whitespace. 20px inside containers, 16px between elements, 12px inside cards minimum.
- Restrained decoration. One gradient, light shadows, thin accent borders. No layered effects.
- Readable at every size. Minimum 13px body text, 14px for chart numbers, 11px absolute floor.
Hard Constraints
These constraints are NON-NEGOTIABLE. Every HTML template must satisfy all of them.
| Constraint | Required Value |
|------------|---------------|
| Body width | Exactly 600px |
| Font stack | 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif |
| FA CSS path | ../node_modules/@fortawesome/fontawesome-free/css/all.min.css (relative to html/ dir) |
| Icons | Font Awesome <i class="fa-solid fa-[icon]"></i> only — NO emoji |
| External CDN | None — all assets must be local |
| Container class | .container on outer div (required by screenshot.js) |
| Logo | ../logo.svg — local file, copied to images/ during setup (served by screenshot.js HTTP server) |
| Footer URL | www.thestartupvc.com |
Brand Colors
Use THESE colors. Not the old Rewrite_TSVC colors (#28DF8D is WRONG).
:root {
--primary: #00C9A7; /* teal/green — main accent */
--primary-dark: #00b396; /* darker teal — gradients, borders */
--success: #10B981; /* emerald — secondary accent */
--text: #333333; /* default body text */
--text-secondary: #666666; /* secondary labels */
--text-muted: #7f8c8d; /* tertiary/footnote text */
--border: #e8e8e8; /* dividers */
--bg-white: #ffffff; /* card backgrounds */
--bg-section: #f5f5f5; /* alternate section backgrounds */
--accent: #e6faf6; /* light teal tint for callout boxes */
}
Density Limits
Never exceed these. If data exceeds the limit, split into two charts. Never shrink text to fit.
| Chart Type | Max Items | Notes |
|------------|----------|-------|
| Horizontal bar chart | 5 bars | Abbreviate values ($12K not $12,000) |
| Side-by-side cards | 3 cards | One primary stat + 1–2 details each |
| Stats dashboard | 4 stat cards | One number per card |
| Timeline / process | 5 items | Labels: 2–3 words max |
| Country comparison | 3 countries | More than 3 → use bar chart instead |
| Donut / progress | 3 segments | CSS conic-gradient only, no JS |
Phase 1: Data Extraction
Requirement: VIZ-01
1.1 Resolve the Slug
The slug comes from $ARGUMENTS. If not provided, ask:
What is the article slug? (e.g.
startup-accelerator-programs)
1.2 Resume Detection
Check output/<slug>/images/metadata.json. If it exists AND PNG files are present in output/<slug>/images/output/:
Images already exist for this article. Options:
- Skip to article insertion (Phase 4)
- Re-run from scratch (will overwrite existing images)
Wait for user reply. "1" or "skip" → jump to Phase 4. "2" or "re-run" → continue from Phase 1.
1.3 Read Article and Research Files
Read these files:
output/<slug>/04-final.md— the finalized article (for H2 placement context)- Every
.mdfile inoutput/<slug>/02-research/— use Glob to find all files
Use Glob: output/<slug>/02-research/*.md
Read each file found.
Critical rule: Chart data MUST come from 02-research/ files. Research files contain source-verified numbers. Article prose may paraphrase or round. Do not infer numbers from article prose alone.
1.4 Score Data Points for Chart Suitability
For each distinct data set found in research files, score it for visualization potential:
| Data Type | Best Chart Type | |-----------|----------------| | Cost comparisons (operating costs, salaries, fees) | Horizontal bar | | Market size stats (funding volumes, growth rates, deal counts) | Stats dashboard or bar | | Regulatory / tax rates (side-by-side across countries) | Country cards or bar | | Talent metrics (graduate counts, proficiency rates) | Stats dashboard | | Timeline / process (incorporation steps, milestones) | Timeline | | Investment sector breakdowns (≤3 categories) | Donut |
Select the top 5 candidates. Fewer is fine if data is sparse — do not force charts for weak data.
Phase 2: Present Options
Requirement: VIZ-02
Present options as a numbered ranked list. Format:
**Visualization Options for: [Article H1 Title]**
1. [Bar Chart] Operating Costs by Country
Data: Colombia $8K/mo, Mexico $12K/mo, Argentina $6K/mo, Brazil $15K/mo
Placement: Under "What Are the Operating Costs?" — supports cost comparison data in that section
2. [Stats Dashboard] Latin America VC Market Size
Data: $4.2B total (2024), 312 deals, 38% YoY growth, 5 active unicorns
Placement: Under "How Large Is the Market?" — supports market overview section
3. [Timeline] Incorporation Process Steps
Data: Reserve name (1 day), File documents (3 days), Tax registration (5 days), Bank account (7 days), Operating permit (10 days)
Placement: Under "How Do You Register a Company?" — illustrates the step-by-step process
[Reply with numbers (e.g. "1, 3") or "all" to proceed]
Rules for the options list:
- Always include actual numbers from research in the Data field
- Always include which H2 section and why in the Placement field
- The thumbnail is generated automatically — do NOT list it as an option
- Maximum 5 options
STOP. Wait for user reply.
- Numbers (e.g. "1, 3" or "2") → generate those charts
- "all" → generate all options
- Any other reply → ask again with the same options list. Do NOT advance without a valid selection.
2.1 Write chart-manifest.json
After the user confirms their selection, write a chart-manifest file that the Phase 3 subagent will read. This decouples Phase 2 output from Phase 3 execution.
Write output/<slug>/images/chart-manifest.json:
{
"slug": "<slug>",
"project_root": "/home/felo/work/content/tsvc_new",
"charts": [
{
"index": 1,
"type": "bars",
"title_en": "<English chart title>",
"title_es": "<Spanish chart title>",
"subtitle_en": "<Source line in English>",
"subtitle_es": "<Source line in Spanish>",
"filename_suffix": "bars",
"data": "<concise data description — e.g. Colombia $8K, Mexico $12K, Argentina $6K>",
"placement_h2": "<H2 section heading where chart will be inserted>",
"chart_type_hint": "<chart type — bars | stats | timeline | cards | donut>",
"key_insight_en": "<Key insight sentence in English>",
"key_insight_es": "<Key insight sentence in Spanish>"
}
]
}
Include one entry per selected chart. Do NOT include the thumbnail (it is always generated separately). Translate titles, subtitles, and key insights to Spanish at this stage — they are short strings (2-15 words) that are easy to translate with article context in scope.
This file is the handoff contract between Phase 2 (main context) and Phase 3 (subagent context).
Phase 3: HTML Templates + Screenshot
Requirement: VIZ-03
3.0 Spawn Phase 3 Subagent
After Phase 2.1 (chart-manifest.json is written), spawn a Task subagent with this prompt:
You are the Phase 3 HTML generation agent for the TSVC visualization pipeline.
Read these files to load your context:
- /home/felo/work/content/tsvc_new/.claude/skills/create-visualizations/SKILL.md (sections 3.1–3.6 for HTML format reference)
- /home/felo/work/content/tsvc_new/output/<slug>/images/chart-manifest.json (chart specs)
Your job:
- Run Phase 3.1 setup (single chained Bash command — see section 3.1)
- For each chart in chart-manifest.json AND for the thumbnail, spawn all image agents as parallel sub-Task agents using the Task tool. All sub-Tasks run simultaneously in one message — do NOT wait for one to finish before spawning the next.
Thumbnail sub-Task prompt:
Generate the featured thumbnail for The Startup VC article using Sharp compositing.
Slug: <slug> Working directory: /home/felo/work/content/tsvc_new/output/<slug>/images/
Step 1 — Derive search query: Read output/<slug>/04-final.md to get the article H1. Derive a concise 2-3 word search query from the article topic (use the subject matter, not the tagline). Examples: "startup accelerator funding" → "accelerator programs"; "SaaS valuation guide" → "software valuation".
Step 2 — Read API keys from .env:
UNSPLASH_KEY=$(grep UNSPLASH_ACCESS_KEY /home/felo/work/content/tsvc_new/.env | cut -d= -f2 | tr -d '[:space:]')
PEXELS_KEY=$(grep PEXELS_API_KEY /home/felo/work/content/tsvc_new/.env | cut -d= -f2 | tr -d '[:space:]')
Step 3 — Fetch stock photo (3-tier):
Create images directory if needed:
mkdir -p /home/felo/work/content/tsvc_new/output/<slug>/images
Set PHOTO_DOWNLOADED=false. Attempt in order:
Tier 1 — Unsplash API (primary):
if [[ -n "$UNSPLASH_KEY" ]]; then
UNSPLASH_RESPONSE=$(curl -s \
-H "Authorization: Client-ID ${UNSPLASH_KEY}" \
"https://api.unsplash.com/search/photos?query={search-query}&orientation=landscape&per_page=1")
PHOTO_URL=$(echo "$UNSPLASH_RESPONSE" | jq -r '.results[0].urls.regular // empty')
DOWNLOAD_TRIGGER=$(echo "$UNSPLASH_RESPONSE" | jq -r '.results[0].links.download_location // empty')
if [[ -n "$PHOTO_URL" && "$PHOTO_URL" != "null" ]]; then
# REQUIRED by Unsplash API terms — trigger download to credit the photographer
curl -s -H "Authorization: Client-ID ${UNSPLASH_KEY}" "${DOWNLOAD_TRIGGER}" > /dev/null
curl -L "${PHOTO_URL}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
FILE_SIZE=$(stat -c%s "/home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg" 2>/dev/null || echo 0)
[[ "$FILE_SIZE" -gt 1000 ]] && PHOTO_DOWNLOADED=true
fi
fi
Tier 2 — Pexels API (fallback): Use when Unsplash is unavailable or returns no results.
if [[ "$PHOTO_DOWNLOADED" == "false" && -n "$PEXELS_KEY" ]]; then
PEXELS_RESPONSE=$(curl -s \
-H "Authorization: ${PEXELS_KEY}" \
"https://api.pexels.com/v1/search?query={search-query}&orientation=landscape&per_page=1")
PHOTO_URL=$(echo "$PEXELS_RESPONSE" | jq -r '.photos[0].src.landscape // .photos[0].src.large2x // empty')
if [[ -n "$PHOTO_URL" && "$PHOTO_URL" != "null" ]]; then
curl -L "${PHOTO_URL}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
FILE_SIZE=$(stat -c%s "/home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg" 2>/dev/null || echo 0)
[[ "$FILE_SIZE" -gt 1000 ]] && PHOTO_DOWNLOADED=true
fi
fi
Note: Pexels Authorization is Authorization: {KEY} — NOT Bearer {KEY}.
Tier 3 — CDN fallback pool (last resort):
if [[ "$PHOTO_DOWNLOADED" == "false" ]]; then
FALLBACK_URLS=(
"https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=1200&h=630&fit=crop&auto=format&q=85"
"https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&h=630&fit=crop&auto=format&q=85"
"https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200&h=630&fit=crop&auto=format&q=85"
"https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=1200&h=630&fit=crop&auto=format&q=85"
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=1200&h=630&fit=crop&auto=format&q=85"
)
curl -L "${FALLBACK_URLS[0]}" -o /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
PHOTO_DOWNLOADED=true
fi
Verify download:
ls -lh /home/felo/work/content/tsvc_new/output/<slug>/images/featured-photo.jpg
If file is missing or < 1KB, report failure: "ERROR: featured-photo.jpg download failed — all three tiers exhausted."
Step 4 — Run generate-thumbnail.js:
cd /home/felo/work/content/tsvc_new/output/<slug>/images && node generate-thumbnail.js <slug>
Step 5 — Verify output:
ls /home/felo/work/content/tsvc_new/output/<slug>/images/output/<slug>-thumbnail*.png
When done, report: "Thumbnail complete: <slug>-thumbnail.png and <slug>-thumbnail-es.png (1200x630, stock photo background)"
Chart sub-Task prompt template (customize per chart — one sub-Task per chart, all spawned in parallel):
Generate two HTML chart files for The Startup VC.
Read /home/felo/work/content/tsvc_new/.claude/skills/create-visualizations/SKILL.md sections 3.2–3.6 for HTML format requirements (boilerplate, brand colors, constraints, chart-specific snippets).
Chart spec:
- Slug: <slug>
- Chart type: <chart.chart_type_hint>
- Filename suffix: <chart.filename_suffix>
- English title: <chart.title_en>
- Spanish title: <chart.title_es>
- English subtitle: <chart.subtitle_en>
- Spanish subtitle: <chart.subtitle_es>
- Data: <chart.data>
- Key insight (EN): <chart.key_insight_en>
- Key insight (ES): <chart.key_insight_es>
Files to create:
- /home/felo/work/content/tsvc_new/output/<slug>/images/html/<slug>-<chart.filename_suffix>-en.html
- /home/felo/work/content/tsvc_new/output/<slug>/images/html/<slug>-<chart.filename_suffix>-es.html
Requirements:
- Use exact boilerplate from SKILL.md section 3.3
- Insert chart-specific HTML from SKILL.md section 3.4 for the chart type
- Body width: exactly 600px
- FA CSS path: ../node_modules/@fortawesome/fontawesome-free/css/all.min.css
- No emoji — Font Awesome icons only
- .container class on outer div
- Logo in footer:
<img src="../logo.svg" alt="The Startup VC">(served locally via HTTP server) - Footer URL: www.thestartupvc.com
When done, report: "Chart <index> complete: <en-filename> and <es-filename>"
- Wait for ALL parallel sub-Task agents to complete before proceeding.
- Run Phase 3.7 validation (see section 3.7):
cd /home/felo/work/content/tsvc_new/output/<slug>/images && node validate-html.js --html-dir html/- Fix any FAIL items by editing the relevant HTML files directly
- Re-run validate-html.js after fixes until all checks pass
- Run Phase 3.7 screenshot (see section 3.7):
cd /home/felo/work/content/tsvc_new/output/<slug>/images && node screenshot.js - Verify all PNGs exist:
ls /home/felo/work/content/tsvc_new/output/<slug>/images/output/ - Report back to main session: list of all PNG filenames generated (one per line)
Wait for the subagent to complete and return the PNG list. The subagent streams its output directly to the conversation.
After the subagent completes, continue to Phase 4 (article insertion) using the PNG list returned by the subagent.
Important: Do NOT generate any HTML files inline in the main session. All HTML generation happens inside the subagent. The main session's only Phase 3 action is spawning the subagent and waiting for its result.
3.1 Setup: Copy Tooling Files
Run this as a single chained Bash command — all steps execute in the same shell, guaranteeing the directory exists before npm runs:
mkdir -p output/<slug>/images/html output/<slug>/images/output \
&& ([ ! -f output/<slug>/images/screenshot.js ] && cp .claude/skills/create-visualizations/screenshot.js output/<slug>/images/screenshot.js || true) \
&& ([ ! -f output/<slug>/images/validate-html.js ] && cp .claude/skills/create-visualizations/validate-html.js output/<slug>/images/validate-html.js || true) \
&& ([ ! -f output/<slug>/images/generate-thumbnail.js ] && cp .claude/skills/create-visualizations/generate-thumbnail.js output/<slug>/images/generate-thumbnail.js || true) \
&& ([ ! -f output/<slug>/images/package.json ] && cp .claude/skills/create-visualizations/package.json output/<slug>/images/package.json || true) \
&& ([ ! -f output/<slug>/images/logo.svg ] && cp .claude/skills/create-visualizations/logo.svg output/<slug>/images/logo.svg || true) \
&& cd output/<slug>/images \
&& ([ ! -d node_modules ] && npm install || true) \
&& ([ ! -d node_modules/.cache/ms-playwright ] && npx playwright install chromium || true)
Critical: Do NOT split this into separate Bash tool calls. The Bash tool does not persist working directory between calls. If mkdir and cd run in separate tool calls, the cd will fail because the shell state is reset. This single-command approach is the only reliable pattern.
npx playwright install chromium must run after npm install — Playwright stores Chromium in node_modules/.cache/ms-playwright/.
3.2 Filename Convention
| Chart type | Filename suffix |
|------------|----------------|
| Horizontal bar | bars |
| Stats dashboard | stats |
| Timeline / process | timeline |
| Donut / progress | donut |
| Country cards | cards |
Full filename: <slug>-<type>-en.html (English) and <slug>-<type>-es.html (Spanish)
If multiple charts of the same type: append number — bars-1-en, bars-1-es.
Thumbnail: <slug>-thumbnail.png (EN) and <slug>-thumbnail-es.png (ES) — generated directly by generate-thumbnail.js, no HTML file involved
3.3 HTML Template Structure
Every chart HTML file MUST use this exact boilerplate. Replace only the placeholder content.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[Visualization Title]</title>
<!-- CRITICAL: path must be exactly ../node_modules/... — do NOT change this path -->
<link rel="stylesheet" href="../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<style>
:root {
--primary: #00C9A7;
--primary-dark: #00b396;
--success: #10B981;
--text: #333333;
--text-secondary: #666666;
--text-muted: #7f8c8d;
--border: #e8e8e8;
--bg-white: #ffffff;
--bg-section: #f5f5f5;
--accent: #e6faf6;
}
* { box-sizing: border-box; }
body {
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: transparent;
margin: 0;
padding: 0;
width: 600px;
}
.container {
background: var(--bg-white);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
color: #ffffff;
padding: 24px 28px;
text-align: center;
}
.header h1 {
font-size: 20px;
font-weight: 700;
margin: 0 0 6px;
color: #ffffff;
line-height: 1.3;
}
.header .subtitle {
font-size: 13px;
opacity: 0.9;
margin: 0;
}
.content {
padding: 24px 28px;
}
.key-insight {
background: var(--accent);
border-left: 3px solid var(--primary);
padding: 14px 18px;
border-radius: 0 8px 8px 0;
margin-top: 20px;
font-size: 13px;
color: var(--text);
line-height: 1.5;
}
.key-insight strong {
color: var(--primary-dark);
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 28px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-muted);
}
.footer-url {
font-weight: 600;
letter-spacing: 0.02em;
}
.footer-logo img {
height: 22px;
opacity: 0.75;
}
/* Add chart-specific styles below */
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>[Chart Title]</h1>
<p class="subtitle">[Subtitle — data source and year, e.g. "Source: 2024 Latin America VC Report"]</p>
</div>
<!-- Visualization area -->
<div class="content">
<!-- INSERT CHART HTML HERE -->
<!-- Key insight callout (required) -->
<div class="key-insight">
<p><strong>Key insight:</strong> [One concrete takeaway sentence — e.g. "Colombia's operating costs are 47% lower than Brazil's, making it the most cost-efficient market in the region."]</p>
</div>
</div>
<!-- Footer (required) -->
<div class="footer">
<span class="footer-url">www.thestartupvc.com</span>
<div class="footer-logo">
<img src="../logo.svg" alt="The Startup VC">
</div>
</div>
</div>
</body>
</html>
3.4 Chart-Specific HTML Snippets
Insert these snippets into the visualization area of the boilerplate. Add accompanying CSS inside the <style> block.
Horizontal Bar Chart
/* Bar chart styles */
.chart-area { margin-bottom: 8px; }
.bar-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.bar-label {
width: 130px;
font-size: 13px;
font-weight: 500;
color: var(--text);
flex-shrink: 0;
padding-right: 12px;
}
.bar-track {
flex: 1;
background: var(--bg-section);
border-radius: 4px;
height: 30px;
overflow: hidden;
}
.bar-fill {
height: 30px;
border-radius: 4px;
background: linear-gradient(90deg, var(--primary-dark), var(--primary));
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 10px;
min-width: 40px;
transition: none;
}
.bar-value {
font-size: 12px;
font-weight: 700;
color: #ffffff;
white-space: nowrap;
}
<!-- Horizontal bar example (max 5 bars) -->
<div class="chart-area">
<div class="bar-row">
<div class="bar-label">[Country/Item]</div>
<div class="bar-track">
<div class="bar-fill" style="width: 85%;">
<span class="bar-value">$XX,XXX</span>
</div>
</div>
</div>
<!-- Repeat for up to 5 bars -->
</div>
Bar width calculation: Use the highest value as 95% and scale others proportionally. The bar with the widest fill is visually dominant.
Stats Dashboard
/* Stats dashboard styles */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 8px;
}
.stat-card {
background: var(--bg-section);
border-radius: 10px;
padding: 18px 16px;
text-align: center;
border: 1px solid var(--border);
}
.stat-icon {
font-size: 22px;
color: var(--primary);
margin-bottom: 10px;
}
.stat-number {
font-size: 26px;
font-weight: 700;
color: var(--text);
line-height: 1;
margin-bottom: 6px;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
margin-bottom: 4px;
}
.stat-context {
font-size: 11px;
color: var(--text-muted);
}
<!-- Stats dashboard (max 4 stat cards, 2x2 grid) -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon"><i class="fa-solid fa-chart-line"></i></div>
<div class="stat-number">$4.2B</div>
<div class="stat-label">Total VC Funding 2024</div>
<div class="stat-context">+38% vs 2023</div>
</div>
<!-- Repeat for up to 4 cards -->
</div>
Timeline / Process
/* Timeline styles */
.timeline {
position: relative;
padding-left: 32px;
margin-bottom: 8px;
}
.timeline::before {
content: '';
position: absolute;
left: 10px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
margin-bottom: 18px;
display: flex;
align-items: flex-start;
gap: 14px;
}
.timeline-item:last-child { margin-bottom: 0; }
.timeline-dot {
position: absolute;
left: -26px;
width: 14px;
height: 14px;
background: var(--primary);
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 0 0 2px var(--primary);
flex-shrink: 0;
margin-top: 2px;
}
.timeline-content { flex: 1; }
.timeline-step {
font-size: 12px;
font-weight: 700;
color: var(--primary-dark);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 2px;
}
.timeline-label {
font-size: 14px;
font-weight: 600;
color: var(--text);
margin-bottom: 2px;
}
.timeline-detail {
font-size: 12px;
color: var(--text-secondary);
}
<!-- Timeline (max 5 items) -->
<div class="timeline">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="timeline-step">Step 1</div>
<div class="timeline-label">[Step Name]</div>
<div class="timeline-detail">[Duration or key detail]</div>
</div>
</div>
<!-- Repeat for up to 5 steps -->
</div>
Country Comparison Cards
/* Country cards styles */
.country-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-bottom: 8px;
}
.country-card {
background: var(--bg-section);
border-radius: 10px;
padding: 16px 14px;
text-align: center;
border: 1px solid var(--border);
}
.country-card.highlight {
border-color: var(--primary);
background: var(--accent);
}
.country-flag {
font-size: 20px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.country-name {
font-size: 13px;
font-weight: 700;
color: var(--text);
margin-bottom: 10px;
}
.country-stat {
font-size: 22px;
font-weight: 700;
color: var(--primary-dark);
line-height: 1;
margin-bottom: 4px;
}
.country-label {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.country-note {
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
padding-top: 8px;
margin-top: 8px;
}
<!-- Country comparison (max 3 countries) -->
<!-- Use class="country-card highlight" for the recommended/best option -->
<div class="country-cards">
<div class="country-card highlight">
<div class="country-flag"><i class="fa-solid fa-location-dot"></i></div>
<div class="country-name">[Country]</div>
<div class="country-stat">$XX</div>
<div class="country-label">[Metric Label]</div>
<div class="country-note">[Distinguishing detail]</div>
</div>
<!-- Repeat for 2-3 countries total -->
</div>
Donut Chart
/* Donut chart styles */
.donut-wrapper {
display: flex;
align-items: center;
gap: 28px;
justify-content: center;
margin-bottom: 8px;
}
.donut {
width: 180px;
height: 180px;
border-radius: 50%;
/* conic-gradient: each segment = percentage of 360deg */
background: conic-gradient(
var(--primary) 0% [SEGMENT_1_%],
var(--success) [SEGMENT_1_%] [SEGMENT_1+2_%],
var(--border) [SEGMENT_1+2_%] 100%
);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.donut-center {
width: 108px;
height: 108px;
border-radius: 50%;
background: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.donut-main-value { font-size: 24px; font-weight: 700; color: var(--text); }
.donut-main-label { font-size: 11px; color: var(--text-muted); }
.donut-legend { display: flex; flex-direction: column; gap: 10px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.legend-dot {
width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
}
.legend-value { font-weight: 700; color: var(--text); }
.legend-label { color: var(--text-secondary); }
<!-- Donut chart (max 3 segments) -->
<!-- Calculate conic-gradient percentages from the data values -->
<div class="donut-wrapper">
<div class="donut">
<div class="donut-center">
<div class="donut-main-value">[Total]</div>
<div class="donut-main-label">[Unit]</div>
</div>
</div>
<div class="donut-legend">
<div class="legend-item">
<div class="legend-dot" style="background: var(--primary);"></div>
<div>
<span class="legend-value">[Value 1]</span>
<span class="legend-label"> [Label 1]</span>
</div>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--success);"></div>
<div>
<span class="legend-value">[Value 2]</span>
<span class="legend-label"> [Label 2]</span>
</div>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--border);"></div>
<div>
<span class="legend-value">[Value 3]</span>
<span class="legend-label"> [Label 3]</span>
</div>
</div>
</div>
</div>
3.5 Thumbnail Spec
The thumbnail is always generated by generate-thumbnail.js — it is NOT a user-selectable option and does NOT use an HTML template.
What generate-thumbnail.js produces:
- 1200×630 PNG (EN):
output/<slug>-thumbnail.png - 1200×630 PNG (ES):
output/<slug>-thumbnail-es.png
Compositing layers (bottom to top):
- Stock photo background —
featured-photo.jpg, resized to 1200×630 with Sharpfit: cover, position: centre - Dark semi-transparent overlay box — 800×dynamic height, centered, fill
#0e0d13opacity 0.90, rx=6 - TSVC teal accent bar — width proportional to font size, centered horizontally, fill
#00C9A7, height 4px, rx=2 - White uppercase tagline text — dynamically sized (NBS
calcLayoutalgorithm), centered, Arial/Helvetica/sans-serif, bold - TSVC logo — bottom-left corner, 180px wide, 28px padding
Tagline sources:
- EN:
output/<slug>/thumbnail-tagline.txt(written by write-article Stage 4.6) - ES:
output/<slug>/thumbnail-tagline-es.txt(written by write-article Stage 4.6) - Fallback: if files are missing, the script derives a tagline from the slug
Run location: Script is copied to output/<slug>/images/ at Phase 3.1 setup time. Run from that directory:
cd output/<slug>/images && node generate-thumbnail.js <slug>
3.6 Spanish Variants
Immediately after generating each English HTML file, create a Spanish variant. Do this during template generation, not as a separate pass — the chart structure and data are already in context.
What to translate per chart:
| Element | Translate? |
|---------|-----------|
| <h1> chart title | Yes |
| .subtitle source line | Yes (e.g. "Fuente: …") |
| Chart labels (axis, legend, stat labels, timeline steps) | Yes |
| .key-insight sentence | Yes |
| Data values (numbers, %, $) | No — identical |
| Footer URL www.thestartupvc.com | No |
| TSVC logo | No |
| HTML structure / CSS | No |
Process:
- Write
<slug>-<type>-en.html - Immediately write
<slug>-<type>-es.htmlwith translated text strings — same HTML structure, same data values, Spanish labels
Note: The thumbnail ES variant (<slug>-thumbnail-es.png) is handled automatically by generate-thumbnail.js — do not create a thumbnail HTML file.
Translation quality: Chart strings are 2–10 words each — short enough to translate accurately without full article context. Use natural Spanish business vocabulary (e.g., "Tasa de éxito" not "Porcentaje de triunfo").
After generating all charts, html/ should contain both -en.html and -es.html files. The screenshot.js will screenshot all of them in a single Playwright session.
3.7 Validate HTML
Run the validator before screenshotting. Fix all FAIL items before proceeding.
cd output/<slug>/images && node validate-html.js --html-dir html/
Common FAIL causes and fixes:
| FAIL | Fix |
|------|-----|
| Wrong FA path | Must be exactly ../node_modules/@fortawesome/fontawesome-free/css/all.min.css |
| Emoji found | Replace with <i class="fa-solid fa-[icon]"></i> — see icon-reference in skill dir |
| Missing .container class | Add class="container" to the outer div |
| Body width not 600px | Ensure width: 600px on body in CSS |
3.7 Screenshot
cd output/<slug>/images && node screenshot.js
The screenshot.js script:
- Reads all
.htmlfiles fromhtml/subdirectory - Starts a local HTTP server (required for relative node_modules path to work)
- Launches Chromium with WSL-safe flags (
--no-sandbox,--disable-setuid-sandbox) - Screenshots only the
.containerelement (not full page) - Saves PNGs to
output/subdirectory - Filenames match:
<slug>-bars-en.html→<slug>-bars-en.png
Verify each PNG was created:
ls output/<slug>/images/output/
If a PNG is missing, check the console output from screenshot.js for errors.
Phase 4: Article Insertion
Requirement: VIZ-04
4.1 Insert Chart References into 04-final.md
PNG list: Use the PNG filenames returned by the Phase 3 subagent. Do not re-scan the output directory — use the reported list.
For each generated chart (NOT the thumbnail), insert an image reference into output/<slug>/04-final.md.
Where to insert: After the first paragraph under the relevant H2 section identified in Phase 2.
Format:

*Caption: one sentence under 15 words*
Alt text rules:
- Descriptive — describes what the chart shows
- Not keyword-stuffed
- Examples: "Bar chart comparing operating costs by country in Latin America" / "Stats dashboard showing Latin America VC funding metrics in 2024"
Caption rules:
- Factual and concise
- Under 15 words
- Describes what the image shows (not marketing copy)
- Examples: "Colombia's operating costs are the lowest among major Latin American markets." / "Latin American VC deal volume reached $4.2 billion in 2024."
Process for each chart:
- Read
output/<slug>/04-final.md - Find the H2 section identified in Phase 2 for this chart
- Find the end of the first paragraph under that H2 (first blank line after the opening paragraph text)
- Insert the markdown image block + caption on a new line after that paragraph
- Use the Edit tool to make the change
4.2 Write metadata.json
Write output/<slug>/images/metadata.json after all insertions complete:
{
"thumbnail": "images/output/<slug>-thumbnail.png",
"charts_en": [
"images/output/<slug>-bars-en.png"
],
"charts_es": [
"images/output/<slug>-bars-es.png"
]
}
List all generated chart PNGs separated by language. The thumbnail field uses the EN PNG path. The ES thumbnail (<slug>-thumbnail-es.png) is uploaded by upload-es.js automatically. This file is used by Phase 4 (WordPress/WPML upload) to identify all images — charts_en for the English post, charts_es for the Spanish WPML translation.
4.3 Confirm Completion
Report:
[N] charts generated, 1 thumbnail generated, [N] image references inserted into 04-final.md.
Generated files:
- output/<slug>/images/output/<slug>-thumbnail.png
- output/<slug>/images/output/<slug>-bars-en.png
[... list all PNGs ...]
Image references inserted:
- After "[H2 section heading]" in 04-final.md — <slug>-bars-en.png
[... list all insertions ...]
Important Implementation Notes
Node.js Tooling Location
The screenshot.js, validate-html.js, and package.json source files live in .claude/skills/create-visualizations/. They are copied into output/<slug>/images/ on first use (see Phase 3.1). This means:
- Tooling changes are made once in the skill directory
- Each article gets its own isolated copy
node_modulesand Chromium binary are per-article (underoutput/<slug>/images/node_modules/)
WSL Playwright Requirements
Always pass these flags in chromium.launch():
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
Without these, Chromium crashes immediately in WSL2 with SIGTRAP or BUS_ADRERR.
Local HTTP Server Requirement
The HTML files reference ../node_modules/@fortawesome/fontawesome-free/css/all.min.css. This relative path only resolves correctly when served via HTTP, not via file:// URLs. The screenshot.js starts a local HTTP server and serves output/<slug>/images/ as the root. The HTML files are accessed at http://127.0.0.1:[port]/html/<file>.html.
One HTML File Per Chart Type
Do NOT create a parameterized template. Create separate HTML files per visualization. This makes validation and debugging simpler, and matches the existing skill pattern.
Icon Reference
Use <i class="fa-solid fa-[name]"></i> format. Common icons for TSVC charts:
| Concept | Icon |
|---------|------|
| Cost / money | fa-dollar-sign |
| Growth / trend | fa-arrow-trend-up |
| Market / chart | fa-chart-line |
| Investment | fa-coins |
| Company | fa-building |
| Startup | fa-rocket |
| People / team | fa-users |
| Country / location | fa-location-dot |
| Globe | fa-earth-americas |
| Calendar / timeline | fa-calendar-days |
| Percent / rate | fa-percent |
| Award | fa-award |
For the full icon list, see .claude/skills/create-visualizations/references/icon-reference.md.
Pitfalls to Avoid
- Using old brand colors —
#28DF8Dis wrong. Use#00C9A7as--primary. - Emoji in HTML — Use Font Awesome icons. Emoji render as blank squares in headless Chrome.
- External CDN — No Google Fonts, no CDN Font Awesome. All assets must be local.
- Full-page screenshot — Use
container.screenshot()notpage.screenshot({ fullPage: true }). networkidle0— Playwright uses'networkidle'(not'networkidle0'which is puppeteer).- Data from article prose — Always pull numbers from
02-research/files, not from04-final.md. - Exceeding density limits — Split instead of shrink. Max 5 bars, 3 cards, 4 stats, 5 timeline, 3 donut.
- Thumbnail as an option — The thumbnail is always generated. Do not list it in the Phase 2 options.
- Skipping validation — Run
validate-html.jsbeforescreenshot.js. Fix all FAIL items first. - Wrong FA icon names — Use FA7 canonical names (e.g.
fa-location-dotnotfa-map-marker-alt).
Content Repurposer
Content
Transforms a single piece of content into platform-adapted publications.
SEO Blog Post Writer
Content
Writes SEO-optimized blog posts with proper structure and keywords.
YouTube Script Writer
Content
Writes engaging YouTube scripts with hooks, structure, and retention.