# Carousel

> Use this skill to turn a blog post, or any source article, into a finished social carousel for LinkedIn and Instagram. Trigger whenever someone asks to make a carousel, a slide post, a document post, a swipe post, or to "turn this blog into social", or asks to repurpose, atomize, or break a post into slides. Also use when asked to restyle or re-cut an existing carousel, change its angle, or regenerate it at a different length. Produces 1080x1350 PNGs, a LinkedIn PDF, captions, alt text, and a shareable HTML review page. Do not use for single-image graphics, video scripts, or plain social captions with no slides.

- Skill: `moihars/carousel` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add moihars/carousel`
- Raw SKILL.md: https://api.skillmd.com/api/skills/moihars/carousel/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: moihars (https://skillmd.com/u/moihars)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/moihars/carousel

---


# Carousel

Turns a blog post into a posting-ready carousel: images for Instagram, a PDF
document post for LinkedIn, captions, alt text, and an HTML review page for
sign-off.

> **Setup:** everything brand-specific lives in `brand.json` at the skill root.
> Palette, logo, type, character budgets, and voice rules are all read from
> there. Fill it in once. Do not edit the scripts to change brand values.
> See the README for the full field reference.

## Load the voice skill first, if you have one

Every word on a slide is customer-facing copy. If your brand has a voice skill,
load it before writing any headline, body line, or caption. This skill governs
structure and layout; the voice skill governs language. Where they seem to
conflict, the voice skill wins on wording.

The `voice_rules` block in `brand.json` is the machine-enforceable subset of
that: the rules worth failing a build over. It pairs with a written voice guide
rather than replacing one.

## The pipeline

Blog → **angle briefing (stop here)** → card script → preflight → render → review page.

The card script is the pivot. Everything downstream reads from it, so all
editing happens there, never in the rendered output.

### Step 1. Read the source and brief the angles. Then stop.

Read the post. Pull out what actually carries a slide: statistics with named
sources, case studies, customer quotes, and list structures.

Most posts support several carousels. Propose **two or three angles** and stop
for a decision. Do not pick one silently. For each angle give:

- the hook it opens on
- why it earns attention, in one line
- its weakness, honestly stated

See `references/angles.md` for how to construct these and a worked example.

### Step 2. Write the card script

Once an angle is chosen, write `card_script.json`. Full schema, the seven card
types, and the per-slot character budgets are in `references/card-types.md`.

Length is **6 to 8 slides**, chosen by topic complexity, not by how much
material exists. A simple topic is 6. A data-heavy one with a case study is 8.
Going longer costs completion rate. The exact range is configurable in
`brand.json` under `deck`.

Cutting is normal and expected. Material that does not fit moves into the
caption, where the "read the full post" work belongs anyway. Record what was
cut and why in `meta.cut_for_length`, so the tradeoff stays visible instead of
looking like an oversight.

Every deck opens on a `cover` and closes on a `cta`. Slide 1 is the feed
thumbnail before it is anything else, so the hook has to survive at thumbnail
size.

### Step 3. Preflight

```bash
python3 scripts/preflight.py card_script.json
```

This must exit clean before rendering. It enforces the per-slot character
budgets and the voice rules from `brand.json`, and it catches things the eye
misses: banned constructions, em dashes outside attribution lines, deck length,
undefined or repeated themes, alt-text count, and captions over the platform
limits.

Fix the copy. Never widen a budget to make an error disappear. The budgets are
derived from the template, so a bust means the slide would have shrunk its own
type to stay inside the box.

### Step 4. Render

```bash
python3 scripts/render_carousel.py card_script.json out/
```

Produces the 1080×1350 PNGs and the LinkedIn PDF. Watch stdout for autofit
warnings: any slide reporting a scale below 1.00 had copy too long for its
slot, and the fix is shorter copy, not smaller type.

### Step 5. Build the review page

```bash
python3 scripts/build_review.py card_script.json out/ out/carousel-review.html "headlines: DM Sans 800"
```

One self-contained file with images and fonts embedded, so it works offline and
can be sent to a reviewer as a single attachment. It shows the swipeable deck,
every caption with a live character count, and the per-slide alt text. Share
this for approval rather than a folder of PNGs.

## Posting

- **LinkedIn** — upload the PDF as a document post. Use the LinkedIn caption.
- **Instagram** — upload the PNGs in filename order. Use the Instagram caption.
- **Alt text** — one entry per slide, in `card_script.json`. Not optional.
  Preflight fails if the count does not match the deck.

## Design system

Set in `brand.json`. Do not restyle per deck; consistency across the feed is the
point.

**Palette.** Themes are named background/foreground/accent sets. Alternate slide
themes so the swipe has rhythm; preflight warns on two identical backgrounds in
a row.

**Type.** One family throughout by default. `headline_family` and
`headline_scale` exist for brands whose display face sets optically larger or
smaller than the body face at the same px value.

**Logo.** Placed automatically on every slide, including the cover. The variant
is computed from background luminance and the contrast of the coloured mark
against that background, so full colour is used wherever it is legible and mono
is used where it is not. On a slide whose background *is* the brand accent, the
mark would score near 1.0 contrast and disappear, so that slide correctly falls
back to mono. Because it is computed rather than a lookup table, adding a new
background colour needs no logo work.

## Environment

Needs `playwright` with chromium, `img2pdf`, and `Pillow`. Fonts and logos ship
in `assets/`, so there is no network dependency at render time.

```bash
pip install playwright img2pdf Pillow && python3 -m playwright install chromium
```

