create-html-carousel

$npx mdskill add gooseworks-ai/goose-skills/create-html-carousel

Generate LinkedIn-ready carousel images from HTML content.

  • Creates square PNG images optimized for mobile feeds.
  • Depends on frontend-slides design systems for styling.
  • Executes by converting HTML to screenshots at 1080x1080px.
  • Delivers ready-to-upload files directly to LinkedIn.
SKILL.md
.github/skills/create-html-carouselView on GitHub ↗
---
name: create-html-carousel
description: 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.
---

# LinkedIn 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

1. **LinkedIn-First Design** — Square format (1080×1080px), optimized for mobile feed viewing
2. **Informational Content** — Tips, workflows, lists, frameworks (not presentations)
3. **Consistent Styling** — Reuse proven design systems from frontend-slides
4. **Automated Export** — Generate HTML → Screenshot → PNG files ready for LinkedIn
5. **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:**

1. **Cover slide** — Hook + title + your brand
2. **Content slides** — One key point per slide (3-6 slides)
3. **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.**

```html
<!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:**

```html
<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):**

```html
<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:**

```html
<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:**

```html
<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:

```javascript
// 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:

```json
{
  "name": "linkedin-carousel-screenshots",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "playwright": "^1.40.0"
  }
}
```

First time setup:

```bash
cd /path/to/skills/create-html-carousel
npm install
```

### Running Screenshot Script

After generating HTML slides:

```bash
node screenshot-slides.js carousel-name
```

This will:

1. Open each slide HTML in a headless browser
2. Set viewport to 1080×1080px
3. Wait for fonts/animations to load
4. Capture PNG screenshot
5. 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:

1. **One clear point** — Don't cram multiple concepts
2. **Visual hierarchy** — Large number/icon + heading + body
3. **Concrete, not abstract** — "Use job postings to find intent" not "Leverage signals"
4. **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:**

1. Use web-safe fonts (Arial, Georgia) OR
2. Add `await page.waitForLoadState('networkidle')` before screenshot
3. Increase wait timeout: `await page.waitForTimeout(1000)`

### Screenshots Are Blurry

**Symptom:** Text looks fuzzy or low-res

**Solution:**

1. Set device scale factor in Playwright:
   ```javascript
   await page.setViewportSize({
     width: 1080,
     height: 1080,
     deviceScaleFactor: 2, // Retina-quality
   });
   ```

### Content Overflows the Slide

**Symptom:** Text or elements cut off in screenshot

**Solution:**

1. Reduce font sizes
2. Decrease padding
3. Split into multiple slides
4. 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

1. User: "Create a LinkedIn carousel about 5 AI GTM workflows"
2. Skill asks: content type, slide count, have content ready?
3. User provides bullet points of the 5 workflows
4. Skill asks: style preference
5. User picks "Bold Signal"
6. Skill generates 7 HTML slides (cover + 5 workflows + CTA)
7. Skill runs screenshot script automatically
8. Skill delivers folder with HTML + PNG exports
9. User uploads PNGs to LinkedIn and publishes

Total time: 5-10 minutes from idea to ready-to-publish carousel.
More from gooseworks-ai/goose-skills