Reel covers
Every reel gets its own cover. It's built from your real photos and set in your real type, and it keeps the text inside the part Instagram shows on the profile grid.
Config: set your brand here
Edit these once before the first run.
| setting | where | default |
|---|---|---|
| accent color | --brand in scripts/cover.html | #3b82f6 |
| headline font | --display-font in scripts/cover.html | Anton (free, Google Fonts) |
| subline font | .sub in scripts/cover.html | Montserrat Bold (free) |
| default subline | your covers | e.g. "from a Home Cook", or none |
| photo folder | photos_dir in library.json | <PATH_TO_YOUR_COVER_PHOTOS> |
| Canva photo folder (optional) | canva_folder in library.json | <YOUR_CANVA_FOLDER_ID> |
| Canva cover master doc (optional) | reference for your current style | <YOUR_CANVA_COVER_MASTER_DOC_ID> |
| Canva scratch design (optional) | used to export photos out of Canva | <YOUR_CANVA_SCRATCH_DESIGN_ID> |
| cover log | COVER_LOG env var for pick.py | drafts/social/cover-log.json in the project root |
| where finished covers go | step 8 | <YOUR_VIDEO_FOLDER>/<Video> YYYY-MM-DD/Covers/ |
| banned words | hooks.md rules | your list (e.g. an old offer name) |
Built for Instagram. The cover can ride on a normal IG + TikTok + Shorts post as the video thumbnail (for example Metricool's videoThumbnailUrl). Only the Instagram grid matters for the design. Never replace a cover a post already has.
Never AI-generate the image. Backgrounds are your photos, and layouts are copied from your covers. The code only arranges them.
Default house style (change to taste)
Reference only your newest covers in your master doc. Older styles you have moved on from don't count.
- a bright, neutral photo of you with a relevant prop (laptop, camera, phone, product), with plain wall where the text goes
- the headline in white display font, ALL CAPS every time, centered, 2 or 3 lines, fairly small
- an optional subline in Montserrat Bold, normal upper and lowercase
- default: no shadow, no outline, no band or darkening behind the text, no accent-colored text
- no labels, no slanted type, no script or cursive fonts, no big-number or series layouts
- the only extra: a plain accent-colored hand-drawn arrow, now and then
- nothing under the reel icon: Instagram's grid puts it in the top-right corner of every tile. render.mjs warns if anything sits at x over 760 and y under 540.
Headline rules, hook types and labels are in hooks.md. Read it before writing a headline.
Files
scripts/cover.html | the one layout: headline + optional subline, plus arrow / inset / avatar extras. Brand config at the top. |
scripts/render.mjs | renders 1080x1920 JPGs + both proofs, and warns on text outside the safe zone or files over 8MB |
scripts/pick.py | shortlists photos by topic, blocks the last 9 used, and prints recent layouts and hook types |
library.json | every photo, tagged (pose, props, where the empty space is, focus point). Ships with an example; build your own. |
cover log (COVER_LOG) | every cover shipped. This is what keeps them unique |
photos_dir | your full-res photos (local only, keep them out of git) |
scripts/ needs puppeteer-core (npm i puppeteer-core in scripts/, or symlink scripts/node_modules to another skill's install) plus Google Chrome and ffmpeg. render.mjs points at the default macOS Chrome path; change executablePath if yours differs.
Run it
1. Know the reel. You need each reel's transcript, caption and opening text card. Name the one promise the reel makes.
2. Headline. Pick a hook type that isn't the last cover's type (pick.py prints it). Write 3 or 4 options, 3 to 8 words each, and pick the strongest. It must not repeat the opening text card word for word.
3. Photo.
python3 .claude/skills/reel-covers/scripts/pick.py "<topic words>" --space top
Then look at the top 3 or 4 before choosing (build a contact sheet with ffmpeg). Match the prop to the topic: laptop for email, systems or website; camera or product for gear; you mid-talk for sales and mindset. For customer stories, use an inset or avatar of the customer (with permission).
4. Layout. Not the same as the last cover's (see pick.py). Put the text in the photo's empty space (space), set focus so the face stays clear, and set y so the text block sits between 300 and 1250.
5. Render.
OUT="<work>/covers" node .claude/skills/reel-covers/scripts/render.mjs covers.json
{"covers":[{"id":"3-the-exact-close","img":"<PATH_TO_YOUR_COVER_PHOTOS>/<file>.jpg",
"data":{"head":"the exact close I use on sales calls","sub":"from a Home Cook","y":560,"headroom":280}}],
"history":["<last few shipped cover files from the cover log, newest first>"]}
Expand ~ to a real path; render.mjs does not. A non-zero exit means a warning, so fix it before sharing.
6. Check it yourself first. Open _proof.png and _grid-phone.png:
- Is the headline readable in the grid proof (130px wide)? If not, cut words.
- Is the face clear of the text in both the full cover and the grid crop?
- Next to the other covers in the grid, does it look different (photo, layout, text position) but clearly from the same set?
7. Gate. Show the user _grid-phone.png and _proof.png, with each headline and one alternative headline. Ask: "Do these covers stop the scroll?" Swap photos or words on request and re-render. It takes seconds.
8. After approval:
- Copy each JPG to
<YOUR_VIDEO_FOLDER>/<Video> YYYY-MM-DD/Covers/, named like the reel (3. the exact close.jpg). - Append one entry per cover to the cover log:
{"date":"YYYY-MM-DD","video":"<video-slug>","reel":"3. the exact close","photo":"<file>","layout":"stack","hook_type":"how","head":"the exact close I use on sales calls","file":"<abs path to the Covers jpg>"} - Hand the files to your scheduling step, which uploads each one and attaches it to the Instagram post.
Uniqueness rules
- No photo used in the last 9 covers.
pick.pyenforces this. - Never the same layout or hook type twice in a row.
- Rotate the subline: none, centered, or offset right and tilted (
subdx,subrot). - Vary the text position (top, middle, left or right-aligned) based on the photo. Don't always center it at the top.
- If the library runs thin (every good photo used recently), say so and suggest a new photo session rather than breaking the rule.
Layout lessons
- The 9:16 crop puts the face high. Most photos are 2:3. Scaled to 1920 tall they lose the sides, not the top, so a face that looks low in the original sits around y 600 to 800. The most common failure is text sitting on the subject's hair. Look at every proof for that before anything else.
- Fix it with
headroom, not by shrinking the text."headroom": 150-350pushes the photo down and stretches the plain wall above the subject. It is invisible on plain studio walls. Don't use it on busy outdoor backgrounds (trees and sky smear). - Headline size: auto is 104px for 4 words or fewer, 84px for longer, shrinking to fit 3 lines.
- Drop the subline if it would touch the head. It's texture, not the message.
- Placing text with the reel icon in mind: either start the block at y 560 or lower (push the photo down with
headroomso it clears the head), or keep it high and usealign: left, right: 320. Mix both across a batch. - A 3:4 grid crop hides nothing above y 240. Keep text starting at 280 or lower (render.mjs warns).
Adding photos
From a local folder: md5-check against photos_dir for dupes, copy in as <name>-<k>__local<MMDD>.jpg, then tag.
From Canva (optional). Canva won't download images out of an uploads folder directly. A working route is a scratch design used only for exports:
list-folder-itemson<YOUR_CANVA_FOLDER_ID>, and diff the asset ids againstlibrary.json(keep the id in each filename after__).read-designon<YOUR_CANVA_SCRATCH_DESIGN_ID>withopen_transaction, then oneedit-designcall with anadd_page(2400x3600, title = asset id) for each new photo. Read the new page ids, then one call with aninsert_fillper page (full page,top 0, left 0).commit.export-designas PDF,export_quality: pro, thenpdfimages -j -f <first> -l <last> -p all.pdf ex/img. The PDF embeds the original JPEGs untouched. JPG export works too, but gives one URL per page.- Save each as
<name>__<assetid>.jpgintophotos_dir. Several photos can share a Canva name, so the id keeps them apart. Then tag them.
If the same photo was uploaded twice, keep one copy ok and mark the other "ok": false, "note": "duplicate of N", or the no-repeat rule can pick the same picture twice.
Tag each new file in library.json by looking at it:
{"file":"photo_-13.jpg","pose":"smiling at laptop","props":["laptop","desk"],"topics":["email","follow","systems","website"],
"cap":false,"space":["top"],"focus":"50% 55%","ok":true}
space is where the text can go without touching the face. Mark blurry, dark or awkward shots "ok":false instead of deleting them.
Related
video-reels: the opening text card, which the cover headline must not repeat- a scheduling skill (optional): uploads the covers and schedules the reels