skills/capabilities/create-html-carousel/SKILL.md
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 skillsauth add athina-ai/goose-skills create-html-carouselInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 upload
Ask 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 install
After generating HTML slides:
node screenshot-slides.js carousel-name
This 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.
content-media
Takes an existing screen recording or demo video and adds professional zoom/pan effects synchronized to the narration. Uses transcript-driven zoom targeting and Remotion for rendering. Optionally replaces audio with a soundtrack.
tools
Repurposes long-form video (podcasts, interviews, talks) into short-form vertical clips for Instagram Reels, TikTok, and YouTube Shorts. Handles transcription, moment selection, clip extraction, speaker-tracked reframing (16:9 to 9:16), and animated captions.
development
Creates talking head videos from any source material (docs, changelogs, blog posts, notes, transcripts). Produces multi-scene videos with avatar narration over screenshots/images using HeyGen v2 API. Supports Quick Shot and Full Producer modes.
tools
Generates Instagram-ready product reels from any e-commerce product page URL. Scrapes product images, classifies by type, generates AI-animated clips via Higgsfield API, creates text overlays with style presets, and composes a 15-20 second reel with music. Supports model-based and product-only reels.