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.
npx gooseworks install --all # then, in Claude Code, Cursor, or Codex: /gooseworks use the create-html-carousel skill
Deprecated: This skill is superseded by
goose-graphics. Seeskills/composites/goose-graphics/(install withnpx goose-skills install goose-graphics). The carousel format is one of seven formats in the newer skill and supports 36 style presets plus image sourcing and PNG export. This skill is retained for one release cycle before removal.
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.
Format: Square (1080×1080px)
Content Structure:
Use for LinkedIn carousel posts like:
NOT for:
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 uploadAsk the user:
Question 1: What's the topic?
Question 2: Content Type
Question 3: Slide Count
Question 4: Branding Handle
Question 5: Content Ready?
If user has content, ask them to share it.
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.
Users can choose styles two ways:
Show preset picker:
Question: Pick a Style
(See STYLE_PRESETS.md for full details on each style)
If user isn't sure, ask:
Question: Audience & Tone
Then generate 2-3 preview slides and let user pick.
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
└── ...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>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>After generating HTML, automatically capture screenshots.
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);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 installAfter generating HTML slides:
node screenshot-slides.js carousel-nameThis will:
[carousel-name]/exports/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?All styles from frontend-slides work for carousels, but require these adjustments:
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.
Vertical space is precious:
Mobile-first mindset:
Strong hooks for LinkedIn carousels:
Each slide should:
Always include a CTA:
Avoid:
Symptom: Screenshots show default system fonts
Solution:
await page.waitForLoadState('networkidle') before screenshotawait page.waitForTimeout(1000)Symptom: Text looks fuzzy or low-res
Solution:
await page.setViewportSize({
width: 1080,
height: 1080,
deviceScaleFactor: 2, // Retina-quality
});Symptom: Text or elements cut off in screenshot
Solution:
Symptom: PNG colors don't match HTML preview
Solution:
| 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.
Total time: 5-10 minutes from idea to ready-to-publish carousel.
Full video production sequence with script review, actual ingredient choices, controlled generation, editing, evidence-based quality review, polish, captions and delivery. A host binding supplies project storage, authentic approvals, provider access and billing.
Build a vox-pop street interview video ad. An interviewer with a handheld mic asks passers-by one question about the brand's product, they give blunt wrong guesses, one gives the real answer, and the cut lands on a branded end card. Generates the takes through the GooseWorks fal proxy (Seedance 2.0 with native voice), then grades, re-cuts, captions and gates them locally. Use for the street-interview format.
Write the words of a short-form video ad (voiceover, dialogue, chat bubbles, on-screen lines) the way performance creative teams do instead of from a blank page. Builds the script from the buyers' own words, the beat sheet of an ad that already works and three deliberately different angles, filters them with a rule check and a second non-Claude model, and takes the strongest into the review with the other two as one-line swaps. Use it in every video ad run before any paid step, and whenever the user asks to write, rewrite or improve a video ad script or says a script sounds generic or AI-written.
Teach your coding agent to build fast, cheap value prop video ads from a brand's logo and product images, using HTML instead of a video model.
An iMessage video ad shows a text conversation on an iPhone screen. Here is how a single skill teaches Claude to build one, with sound and an end card.