Goose-skills create-html-carousel
Create LinkedIn carousel posts as high-quality PNG images. Design informational multi-slide posts like "5 AI GTM workflows" with consistent styling, then automatically screenshot each slide at LinkedIn's optimal 1080x1080px format.
git clone https://github.com/gooseworks-ai/goose-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/gooseworks-ai/goose-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/capabilities/create-html-carousel" ~/.claude/skills/gooseworks-ai-goose-skills-create-html-carousel && rm -rf "$T"
skills/capabilities/create-html-carousel/SKILL.mdLinkedIn Carousel Creator
Create stunning LinkedIn carousel posts as PNG images. This skill generates styled HTML slides optimized for square format (1080×1080px), then automatically screenshots each slide for direct upload to LinkedIn.
Core Philosophy
- LinkedIn-First Design — Square format (1080×1080px), optimized for mobile feed viewing
- Informational Content — Tips, workflows, lists, frameworks (not presentations)
- Consistent Styling — Reuse proven design systems from frontend-slides
- Automated Export — Generate HTML → Screenshot → PNG files ready for LinkedIn
- Viewport Perfect — Every slide must fit exactly in 1080×1080px without scrolling
LinkedIn Carousel Specs
Format: Square (1080×1080px)
- Aspect ratio: 1:1
- File format: PNG (recommended) or JPG
- File size: Under 10MB per image
- Max slides: 10 images per carousel
- Ideal slide count: 5-8 slides (best engagement)
Content Structure:
- Cover slide — Hook + title + your brand
- Content slides — One key point per slide (3-6 slides)
- Closing slide — CTA / summary / follow prompt
When to Use This Skill
Use for LinkedIn carousel posts like:
- "5 AI GTM workflows you should be using"
- "How to build X: A step-by-step guide"
- "7 mistakes founders make with Y"
- "The complete framework for Z"
- "Before & After: How we 10x'd our metrics"
NOT for:
- Long-form presentations (use frontend-slides)
- Video content
- Single-image posts
Workflow Overview
1. Content Input → User provides topic/outline 2. Style Selection → Choose visual style (or preview options) 3. HTML Generation → Create 1080×1080px HTML slides 4. Screenshot → Auto-capture each slide as PNG 5. Delivery → Folder of PNG files ready for LinkedIn upload
Phase 1: Content Discovery
Step 1.1: Get Topic & Structure
Ask the user:
Question 1: What's the topic?
- Header: "Topic"
- Question: "What's the main topic of this carousel?"
- (Free text input)
Question 2: Content Type
- Header: "Format"
- Question: "What type of post is this?"
- Options:
- "Numbered list" — "5 ways to...", "7 mistakes...", "3 steps to..."
- "How-to guide" — Step-by-step tutorial or process
- "Framework" — Concept explanation with structure
- "Before/After" — Transformation or case study
- "Insights/Tips" — Collection of advice or learnings
Question 3: Slide Count
- Header: "Length"
- Question: "How many slides?"
- Options:
- "Short (5-6)" — Quick, punchy (best for mobile scrolling)
- "Medium (7-8)" — Standard carousel length
- "Long (9-10)" — Maximum LinkedIn allows
Question 4: Branding Handle
- Header: "Brand"
- Question: "What handle or name should appear on each slide?"
- (Free text input — e.g., "@yourhandle", "Acme Inc", or leave blank for none)
Question 5: Content Ready?
- Header: "Content"
- Question: "Do you have the content written?"
- Options:
- "Yes, I have all content" — Paste it in
- "I have bullet points" — Need light formatting
- "Just the topic" — Need help outlining
If user has content, ask them to share it.
Content Density Rules for LinkedIn
Each slide should be scannable in 2-3 seconds on mobile:
| Slide Type | Max Content |
|---|---|
| Cover | Title (1 line) + subtitle (1 line) + branding |
| List item | Number/icon + heading (2 lines max) + body (3 lines max) |
| Framework | Diagram/visual + 2-4 labels |
| Quote/Stat | 1 large stat or quote + context |
| CTA | 1 action + visual element |
If content exceeds limits: Break into multiple slides or simplify.
Phase 2: Style Selection
Users can choose styles two ways:
Option A: Direct Selection (Faster)
Show preset picker:
Question: Pick a Style
- Header: "Style"
- Question: "Which visual style works best for your content?"
- Options:
- "Bold Signal" — High-contrast card on dark, confident
- "Dark Botanical" — Elegant dark with soft abstract shapes
- "Notebook Tabs" — Editorial cream paper with colorful tabs
- "Pastel Geometry" — Friendly pastels with decorative pills
- "Neon Cyber" — Futuristic tech aesthetic
- "Split Pastel" — Playful two-tone split design
(See STYLE_PRESETS.md for full details on each style)
Option B: Guided Discovery
If user isn't sure, ask:
Question: Audience & Tone
- Header: "Vibe"
- Question: "Who's your audience and what tone?"
- Options:
- "Professional/Corporate" → Recommend: Bold Signal, Dark Botanical
- "Creative/Playful" → Recommend: Split Pastel, Pastel Geometry
- "Technical/Dev-focused" → Recommend: Neon Cyber, Terminal Green
- "Elegant/Premium" → Recommend: Dark Botanical, Paper & Ink
Then generate 2-3 preview slides and let user pick.
Phase 3: Generate HTML Carousel
File Structure
All carousel files (HTML source and PNG exports) are saved to the shared assets directory.
[carousel-name]/ ├── index.html # Full carousel (all slides) ├── slides/ │ ├── slide-01.html # Individual slide pages │ ├── slide-02.html │ └── ... └── exports/ ├── slide-01.png # Screenshots (generated in Phase 4) ├── slide-02.png └── ...
HTML Architecture for 1080×1080px
CRITICAL: LinkedIn carousel slides are SQUARE (1:1 ratio), not widescreen.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Slide 01</title> <!-- Fonts --> <link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=..." /> <style> /* =========================================== LINKEDIN CAROUSEL: SQUARE FORMAT Fixed 1080×1080px for screenshot =========================================== */ :root { /* Fixed size for LinkedIn */ --slide-width: 1080px; --slide-height: 1080px; /* Colors (from chosen preset) */ --bg-primary: #0a0f1c; --text-primary: #ffffff; --accent: #00ffcc; /* Typography - scaled for square format */ --title-size: 72px; --subtitle-size: 36px; --body-size: 28px; --small-size: 20px; /* Spacing */ --slide-padding: 80px; --content-gap: 40px; /* Animation */ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: var(--slide-width); height: var(--slide-height); overflow: hidden; } body { font-family: var(--font-body); background: var(--bg-primary); color: var(--text-primary); display: flex; flex-direction: column; justify-content: center; align-items: center; padding: var(--slide-padding); } /* Content container */ .slide-content { width: 100%; max-width: 100%; display: flex; flex-direction: column; gap: var(--content-gap); } /* Typography hierarchy */ h1 { font-size: var(--title-size); font-weight: 800; line-height: 1.1; margin-bottom: 20px; } h2 { font-size: var(--subtitle-size); font-weight: 700; line-height: 1.2; } p, li { font-size: var(--body-size); line-height: 1.4; } /* List styling */ ul { list-style: none; } li { padding-left: 40px; position: relative; margin-bottom: 20px; } li::before { content: "→"; position: absolute; left: 0; color: var(--accent); font-weight: bold; } /* Number badge (for list items) */ .number { font-size: 120px; font-weight: 900; color: var(--accent); opacity: 0.15; position: absolute; top: -40px; left: -20px; z-index: 0; } /* Branding footer */ .brand { position: absolute; bottom: var(--slide-padding); right: var(--slide-padding); font-size: var(--small-size); opacity: 0.7; } /* =========================================== STYLE-SPECIFIC OVERRIDES Inject preset styles here =========================================== */ /* ... preset-specific CSS ... */ </style> </head> <body> <div class="slide-content"> <!-- Slide content goes here --> <h1>Your Title Here</h1> <p>Your content here</p> </div> <div class="brand">@yourbrand</div> </body> </html>
Content Slide Templates
Cover Slide:
<div class="slide-content"> <h1>5 AI GTM Workflows<br />You Should Be Using</h1> <p>Scale your outbound without scaling your team</p> </div> <div class="brand">@yourhandle</div>
Numbered Item (e.g., Slide 2/6):
<div class="slide-content"> <div class="number">01</div> <h2>Signal-Based Outbound</h2> <p> Monitor job postings, funding announcements, and tech stack changes to find companies actively solving your problem. </p> </div> <div class="brand">@yourhandle • 1/5</div>
Framework Slide:
<div class="slide-content"> <h2>The GTM Engineering Stack</h2> <div class="framework-grid"> <div class="box">Research</div> <div class="box">Personalization</div> <div class="box">Outreach</div> <div class="box">Tracking</div> </div> </div> <div class="brand">@yourhandle • 3/5</div>
CTA Slide:
<div class="slide-content"> <h2>Want more like this?</h2> <p>Follow me for more tips and workflows.</p> <div class="cta">Hit that follow button →</div> </div> <div class="brand">@yourhandle</div>
Phase 4: Screenshot Generation
After generating HTML, automatically capture screenshots.
Using Playwright (Recommended)
Create a Node.js script to screenshot each slide:
// screenshot-slides.js const { chromium } = require("playwright"); const path = require("path"); const fs = require("fs"); async function screenshotSlides(slidesDir, outputDir) { const browser = await chromium.launch(); const page = await browser.newPage(); // Set viewport to LinkedIn carousel size await page.setViewportSize({ width: 1080, height: 1080 }); // Ensure output directory exists if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Find all HTML files in slides directory const slideFiles = fs .readdirSync(slidesDir) .filter((f) => f.endsWith(".html")) .sort(); console.log(`Found ${slideFiles.length} slides to screenshot`); for (const slideFile of slideFiles) { const slidePath = path.join(slidesDir, slideFile); const outputName = slideFile.replace(".html", ".png"); const outputPath = path.join(outputDir, outputName); console.log(`Capturing ${slideFile}...`); await page.goto(`file://${path.resolve(slidePath)}`); // Wait for fonts and animations await page.waitForTimeout(500); // Take screenshot await page.screenshot({ path: outputPath, type: "png", fullPage: false, }); console.log(`✓ Saved ${outputName}`); } await browser.close(); console.log("\n✨ All slides captured!"); } // Usage const carouselName = process.argv[2]; if (!carouselName) { console.error("Usage: node screenshot-slides.js <carousel-name>"); process.exit(1); } const slidesDir = path.join(__dirname, carouselName, "slides"); const outputDir = path.join(__dirname, carouselName, "exports"); screenshotSlides(slidesDir, outputDir);
Installation
The skill directory needs these dependencies:
{ "name": "linkedin-carousel-screenshots", "version": "1.0.0", "private": true, "dependencies": { "playwright": "^1.40.0" } }
First time setup:
cd /path/to/skills/create-html-carousel npm install
Running Screenshot Script
After generating HTML slides:
node screenshot-slides.js carousel-name
This will:
- Open each slide HTML in a headless browser
- Set viewport to 1080×1080px
- Wait for fonts/animations to load
- Capture PNG screenshot
- Save to
[carousel-name]/exports/
Phase 5: Delivery
After screenshots are generated, present to user:
✨ Your LinkedIn carousel is ready! 📁 Location: /assets/carousel-name/ **Slides:** - 6 HTML slides in slides/ folder - 6 PNG images in exports/ folder (1080×1080px) **Preview:** Open index.html to see all slides with navigation. **Upload to LinkedIn:** 1. Create new post on LinkedIn 2. Click "Add media" 3. Upload all PNGs from exports/ folder in order 4. Add your post copy 5. Publish! **File sizes:** - slide-01.png: 234 KB ✓ - slide-02.png: 198 KB ✓ - slide-03.png: 256 KB ✓ (All under 10MB limit) Want to make any changes to the slides?
Style Adaptation for Square Format
All styles from frontend-slides work for carousels, but require these adjustments:
Typography Scaling
Square format has less horizontal space, so scale fonts:
| Element | Presentation (16:9) | Carousel (1:1) |
|---|---|---|
| Title | clamp(2rem, 6vw, 5rem) | 72px (fixed) |
| Subtitle | clamp(1.25rem, 3vw, 2.5rem) | 36px (fixed) |
| Body | clamp(0.875rem, 1.5vw, 1.125rem) | 28px (fixed) |
| Small | clamp(0.75rem, 1vw, 0.875rem) | 20px (fixed) |
Why fixed sizes? We're targeting a single export size (1080×1080px), not responsive web viewing.
Layout Adjustments
Vertical space is precious:
- Reduce top/bottom padding (80px instead of 4rem)
- Tighter line-height (1.2-1.4 instead of 1.5-1.6)
- Fewer list items per slide (max 3-4)
- Smaller decorative elements
Mobile-first mindset:
- Most LinkedIn users view on phones
- Text must be readable at thumbnail size
- High contrast is critical
- Bold, simple layouts beat intricate designs
Content Best Practices
Hook Formula (Cover Slide)
Strong hooks for LinkedIn carousels:
- Number + Promise: "5 workflows that 10x'd our outbound"
- Contrarian: "Stop doing X. Do this instead."
- Before/After: "How we went from X to Y in 30 days"
- Question: "Why are only 3% of founders doing this?"
- Curiosity gap: "The GTM strategy nobody talks about"
Body Slides (Items 2-9)
Each slide should:
- One clear point — Don't cram multiple concepts
- Visual hierarchy — Large number/icon + heading + body
- Concrete, not abstract — "Use job postings to find intent" not "Leverage signals"
- Scannable — 2-3 second read time max
Closing Slide
Always include a CTA:
- "Follow for more [topic]"
- "Repost if this helped"
- "Comment your biggest takeaway"
- "DM me if you want the full playbook"
Avoid:
- "Link in comments" (often gets buried)
- "Check out my website" (feels salesy)
- No CTA at all (wasted opportunity)
Troubleshooting
Fonts Not Loading in Screenshots
Symptom: Screenshots show default system fonts
Solution:
- Use web-safe fonts (Arial, Georgia) OR
- Add
before screenshotawait page.waitForLoadState('networkidle') - Increase wait timeout:
await page.waitForTimeout(1000)
Screenshots Are Blurry
Symptom: Text looks fuzzy or low-res
Solution:
- Set device scale factor in Playwright:
await page.setViewportSize({ width: 1080, height: 1080, deviceScaleFactor: 2, // Retina-quality });
Content Overflows the Slide
Symptom: Text or elements cut off in screenshot
Solution:
- Reduce font sizes
- Decrease padding
- Split into multiple slides
- Simplify content (fewer bullets, shorter text)
Colors Look Different in Export
Symptom: PNG colors don't match HTML preview
Solution:
- Ensure browser color profile matches sRGB
- Use hex colors, avoid CSS filters that may render differently
- Test screenshot script before generating all slides
Preset Quick Reference
| Preset | Best For | Vibe |
|---|---|---|
| Bold Signal | Confident, high-impact | Professional |
| Dark Botanical | Elegant, premium | Sophisticated |
| Notebook Tabs | Editorial, organized | Friendly-professional |
| Pastel Geometry | Friendly, approachable | Playful |
| Neon Cyber | Tech, innovation | Futuristic |
| Split Pastel | Creative, fun | Energetic |
See STYLE_PRESETS.md for complete styling details.
Related Skills
- frontend-slides — For full presentations (not carousels)
- personalized-email — For outreach content to pair with LinkedIn posts
- deep-web-research — For researching topics/stats for carousel content
Example Session Flow
- User: "Create a LinkedIn carousel about 5 AI GTM workflows"
- Skill asks: content type, slide count, have content ready?
- User provides bullet points of the 5 workflows
- Skill asks: style preference
- User picks "Bold Signal"
- Skill generates 7 HTML slides (cover + 5 workflows + CTA)
- Skill runs screenshot script automatically
- Skill delivers folder with HTML + PNG exports
- User uploads PNGs to LinkedIn and publishes
Total time: 5-10 minutes from idea to ready-to-publish carousel.