SKILL: notion-media-generator
TRIGGER PHRASES
/media, generate media, create carousel, make instagram slides, make whop images,
create media for my template, generate promo images, build media kit, marketing images,
or any Notion URL provided with intent to market the template.
WHAT THIS SKILL PRODUCES
- 5 Instagram carousel slides — 1080×1350px logical canvas, rendered at device_scale_factor=3 → 3240×4050px actual PNG
- 5 Whop listing images — 1920×1080px logical canvas, rendered at device_scale_factor=3 → 5760×3240px actual PNG
Total: 10 high-resolution PNGs, all written to a single output folder: D:\Claude\.claude\automations\notion\output. Rendered via Playwright chromium. Pure HTML/CSS — no image files, no external assets.
PHASE 0 — READ THE NOTION PAGE COMPLETELY
Do not generate a single word of copy until this phase is complete.
notion-fetch the URL provided.
Also search for child databases, subpages, and linked views.
Read every block — don't skim.
REQUIRED EXTRACTIONS — record ALL of these explicitly before Phase 1
Template identity
- Exact name (with emoji if present in the page icon or title)
- Price (exact:
$X) - Tagline or one-liner (pull verbatim from page if present)
- Category (content, productivity, finance, fitness, etc.)
Structure — from the actual Notion page
- Every database name
- Every field/property name and type (Title, Select, Date, URL, Text, etc.)
- Every view name and what it filters or sorts on
- Status options (e.g. Idea / In Progress / Posted / Scrapped) — exact labels
Sample data — read actual rows
- Pull 4–6 verbatim entries from the template's real sample rows
- For each row: record its Title, key status field, and 1-2 other notable fields
- These rows go directly into the mock-database images — do NOT invent fictional data
Problem & audience
- The exact problem the template solves (their words, not marketing language)
- Who the buyer is: role, hobby, niche, platform they post on / use
- What they were doing before (the broken system: Notes app, memory, spreadsheet, DMs)
- The one action the template makes effortless that was painful before
Differentiators
- Why this vs a free template?
- Why this vs a spreadsheet or Notes app?
- The "power move" — the single habit or check the template enables
Price anchor
- What does one saved unit of time/money/mistake avoided cost vs the template price?
- (e.g. "one skipped duplicate post pays for this 20× over")
CTA mechanics
- How does the buyer get it? (DM keyword / link in bio / direct URL)
- DM keyword if applicable (e.g. "NOTEBOOK")
PHASE 1 — AUDIENCE INTELLIGENCE
Read this section fully. It determines copy tone and slide structure.
Instagram — Notion template audience
Demographics: Hobby creators, students, side-hustlers, productivity seekers, 18–34. Follows @notionhq, @aliabdaal, #notionsetup, #productivitytips, #studygram. Buys Notion templates because they want systems, not because they love Notion.
What stops the scroll on slide 1: Not aspiration. Not product features. Their private embarrassment. The moment they lost something, forgot something, did something twice. Specific failure > abstract pain.
- ✅ "you already posted that." ← specific, accusatory, true
- ✅ "that idea you had in the shower is gone." ← moment they recognize
- ❌ "tired of disorganized content?" ← too vague, sounds like an ad
- ❌ "Introducing My Content Notebook" ← ignored instantly
IG carousel algorithm signals:
- Save rate drives reach — carousels that teach/show something get saved. Every slide must feel "I'll need this later"
- Swipe-through rate — slide 2 must be earned by slide 1. Not with a cliffhanger gimmick — with genuine continuation of the story
- DM keyword in caption drives comments → comments signal to algorithm → more reach
- Lowercase tone = creator peer, not brand. This audience trusts lowercase.
- Specific numbers ("21st", "$9", "3 rows", "10 seconds") are more believable than vague claims
Words that kill IG carousels for this audience:
introducing, features include, learn more, click the link, this template will help you, synergy, streamline, any ALL CAPS body text, any corporate phrasing.
Carousel narrative arc that performs:
Slide 1: Recognition hit — their secret failure, named precisely
Slide 2: The story — lived timeline of the exact moment the failure happened
Slide 3: The reveal — "here's what the actual system looks like" (real data visible)
Slide 4: The objection killed — what the skeptic says + why they're wrong
Slide 5: The price reveal + CTA — justified, never apologized for
Whop Discover — Notion template audience
Demographics: Active buyers. They searched "notion template" or browsed the discover page. Not casually scrolling — actively evaluating. 70% have already bought at least one Notion template. They're price-aware ($5–$25 range is normal) and scan-read listings.
How buyers scan a Whop listing:
- Cover thumbnail at ~300px in the grid → if headline isn't readable, they don't click
- After clicking → gallery images viewed left-to-right in 3–5 seconds each
- Description skim → price check → buy or bounce
What converts each gallery slot:
- Image 1 (cover): Single benefit claim + product name. Must be legible at 300px. No clutter.
- Image 2 (what's inside): Specific, named features with descriptions of why they matter — not a feature list
- Image 3 (the actual product): Real database rows/UI mock. This is the highest-converting image. Buyers fear buying a hollow template. Show them it's real and pre-filled.
- Image 4 (how it works): 3-step workflow. Kills the "is this complicated" fear.
- Image 5 (value anchor): Price justification. Specifics. What they stop doing/losing. Kills the "$9 is a lot for a template" hesitation.
Whop copy rules:
- Amber eyebrow label → signals section type at a glance
- Pill tag top-right → confirms category (e.g. "notion template")
- Binding dots → consistent brand identity across all 5 images
- Price badge bottom-right → visible on every image, normalizes the price by repetition
PHASE 2 — COPY PLANNING
Write ALL 10 slide/image copy blocks BEFORE generating any HTML. Print them clearly. Do not proceed to Phase 3 until copy is drafted.
IG CAROUSEL — 5 SLIDES
SLIDE 1 — HOOK
- 1–2 lines. Lowercase. Weight: light/thin.
- Names the exact failure state. Not their aspiration.
- No product name. No solution hint. Just the mirror.
- Max 12 words on a single line.
- Format examples:
you forgot you already posted that.that idea you had on tuesday is gone.you followed up three weeks late.
SLIDE 2 — LIVED STORY Format: 3–5 staccato lines narrating the exact scenario, then 2 smaller lines reframing it.
[specific event, present tense]
[next beat, specific date or number]
[consequence]
[another beat]
[reframe line 1 — dim/smaller]
[reframe line 2 — dim/smaller]
Example:
you had the idea at 11pm.
you typed it into notes.
it's been six days.
you haven't opened notes once.
that's not you being bad at this.
that's not having a system.
SLIDE 3 — PRODUCT GLIMPSE
- Small label at top:
the actual [database name]— lowercase, dim - Table with 3 real rows from the template's sample data:
- Columns: Title | Key Field | Value/Status
- Dividers: thin 1px lines
- Bold Title, normal weight for other columns
- 2 lines below table (dim): the single sentence that explains why this table solves it
- Keep it simple — the "wait, that's it?" reaction is the goal
SLIDE 4 — OBJECTION CRUSHER
- Opens with skeptic's objection in quotes — small, dim, italicized feel
- Main body: respond in their voice, not defensively
- Differentiator: 1-2 lines about what's different about this specific template
- Small line: proof / use-before-buy moment
SLIDE 5 — PRICE + CTA Format:
[value anchor — 1 dim line]
$[PRICE]
[once. yours forever. no subscription.] ← dim, small
[TEMPLATE NAME] ← medium, line break
——
[template description, 2-3 lines] ← dim
$X · one time · [CTA like "link in bio"] ← dim/small
[DM KEYWORD or action in bold] ← bold, white
WHOP — 5 IMAGES
IMAGE 1 — COVER
- Amber eyebrow:
[TEMPLATE NAME IN CAPS] - Tag pill:
notion template - Hero headline (2 lines, bold, 90–104px): captures core benefit in 6–10 words
- Sub (1–2 lines, dim): who it's for + what overhead they eliminate
- Bottom left:
[emoji] [platform list]e.g.📓 For YouTube · Instagram · TikTok · Blog - Bottom right price badge:
$X · lifetime access
IMAGE 2 — WHAT'S INSIDE
- Eyebrow:
WHAT'S INSIDE - Tag:
[N] database · [N] views(exact counts from the template) - Headline: short, specific claim about what the user gets
- 3 feature cards (one per main view/database):
- Emoji matching the view icon
- View name as card title
- 2–3 sentence description of WHY it matters (benefit, not feature definition)
- Brand + price badge
IMAGE 3 — THE ACTUAL DATABASE
- Eyebrow:
THE ACTUAL DATABASE - Tag: exact database name from Notion
- Headline:
No blank page. [N] sample [items] already inside. - Table with 4 real rows:
- Columns derived from actual database properties
- Status column uses colored pills matching the real status options
- Pill colors: use the token values from Phase 3
- Brand + price badge
IMAGE 4 — HOW IT WORKS
- Eyebrow:
HOW IT WORKS - Tag:
[X] min setup - Headline: single sentence making the workflow sound effortless
- 3 steps with
01/02/03in amber mono,→arrows between columns:- Each step: emoji + step name (bold) + 2-line description
- Brand + price badge
IMAGE 5 — WHY $[PRICE]
- Eyebrow:
WHY $[PRICE] - Tag:
one time · no subscription - Headline: the value equation in one bold sentence
- 3 value bullets using
→(not bullet points or dashes):- Each: specific thing the buyer stops doing/losing
- Grounded in extracted data (e.g. "stop searching Notes app for old ideas")
- 1 objection crusher line (the "you could just use..." rebuttal)
- Brand + price badge
PHASE 3 — EXACT DESIGN SYSTEM
Copy these tokens exactly. Do not adjust colors, sizes, or layout proportions without explicit instruction.
COLOR TOKENS
Background: #141413 (not black — has warm brown undertone)
Text primary: #F0F0EC (warm off-white, NOT pure white)
Text body: #A2A098 (medium warm gray — secondary copy)
Text dim: #6F6E69 (muted gray — labels, mono tags, small print)
Card bg: #1B1B1A (slightly lighter than bg — Whop cards only)
Card border: #2C2C2A (subtle separation)
Table divider: #343330 (row separator lines, thin 1px)
Accent amber: #E7B24C (WHOP ONLY — eyebrow labels, price badge bg, step numbers)
Binding dot fill: #3A3A38
Binding dot bdr: #56564F
Status pill colors (IMAGE 3 only):
Idea: bg #3d3620 text #E7B24C
In Progress: bg #1f2c3d text #7fb0e0
Posted: bg #1f3320 text #7fce8a
Scrapped: bg #3a2323 text #e08a8a
(adjust status names/colors to match the template's actual status options)
TYPOGRAPHY
Primary: 'Liberation Sans', Arial, sans-serif ← available in Playwright env
Mono: 'Liberation Mono', monospace ← used for tags, labels, price badge
Never use Google Fonts. Never use @import. Network is blocked in render env.
IG CAROUSEL SPECS (1080×1350 canvas, device_scale_factor=3)
Background: #141413 full bleed
Left margin: 83px
Right margin: 83px
Max text width: ~840px
Vertical center: flex column, justify-content: center (slight upward bias with padding-bottom ~50px)
NO amber anywhere on IG slides
NO binding dots on IG slides
NO feature cards or decorative borders on IG slides
NO background textures, gradients, or shadows
Slide-specific font sizes (at 1080px logical canvas):
| Slide | Element | Size | Weight | Color |
|---|---|---|---|---|
| 1 | Hook text | 58–62px | 300 | #F0F0EC |
| 2 | Story lines (primary) | 42–46px | 300 | #F0F0EC |
| 2 | Reframe lines | 34–36px | 300 | #A2A098 |
| 2 | Gap between blocks | 40px | — | — |
| 3 | Table label | 22px | 400 | #6F6E69 |
| 3 | Table title (row) | 32px | 700 | #F0F0EC |
| 3 | Table other cols | 32px | 400 | #A2A098 |
| 3 | Footer insight | 28–30px | 300 | #A2A098 |
| 4 | Objection quote | 22px | 300 | #6F6E69 |
| 4 | Response body | 42–46px | 300 | #F0F0EC |
| 4 | Differentiator | 30–32px | 300 | #A2A098 |
| 5 | Value anchor | 32px | 300 | #A2A098 |
| 5 | Price (the number) | 150–170px | 700 | #F0F0EC |
| 5 | Terms line | 28px | 300 | #A2A098 |
| 5 | Product name | 44–48px | 700 | #F0F0EC |
| 5 | Divider rule | width 60px, height 3px | — | #3A3A38 |
| 5 | Description | 30px | 300 | #A2A098 |
| 5 | CTA / DM keyword | 36–38px | 700 | #F0F0EC |
Line heights: story/body = 1.3–1.4. Hook = 1.15–1.2.
WHOP SPECS (1920×1080 canvas, device_scale_factor=3)
Binding strip: position:absolute; left:64px; top:0; bottom:0; width:10px
Binding dot: 11×11px, border-radius:50%, bg:#3A3A38, border:1px solid #56564F
Dot count: 12 dots, justify-content:space-evenly
Content frame: left:150px, right:110px, top:90px, bottom:90px
display:flex; flex-direction:column; justify-content:space-between
Element sizing:
| Element | Size | Weight | Color | Notes |
|---|---|---|---|---|
| Eyebrow label | 26px | 700 | #E7B24C | letter-spacing:4px, text-transform:uppercase |
| Tag pill | 22px | 400 | #6F6E69 | mono font, border 1px #3A3A38, radius:100px, padding:8px 18px |
| Hero headline | 90–104px | 700 | #F0F0EC | letter-spacing:-2px, line-height:1.05 |
| Sub text | 36–40px | 400 | #A2A098 | line-height:1.5, max-width:960px |
| Card bg | — | — | #1B1B1A | border:1px solid #2C2C2A, radius:16px, padding:36px 30px |
| Card emoji | 44px | — | — | |
| Card title | 30px | 700 | #F0F0EC | |
| Card desc | 22px | 400 | #A2A098 | line-height:1.5 |
| Step number | 24px | 400 | #E7B24C | mono font |
| Step title | 32px | 700 | #F0F0EC | |
| Step desc | 22px | 400 | #A2A098 | line-height:1.5 |
| Arrow (→) | 36px | 400 | #3A3A38 | |
| Table header | 18px | 400 | #6F6E69 | mono, letter-spacing:1px, UPPERCASE |
| Table row | row padding:26px 34px | border-bottom:1px solid #262624 | ||
| Table title cell | 26px | 700 | #F0F0EC | |
| Table other cells | 22px | 400 | #A2A098 | |
| Status pill | 20px | 700 | per status | radius:100px, padding:6px 16px |
| Brand watermark | 26px | 400 | #8f8d86 | bottom-left |
| Price badge | 28–32px | 700 | bg:#E7B24C, text:#141413 | mono, padding:14px 26px, radius:8px |
| Value bullets (→) | 28–32px | 400 | #A2A098 | line-height:1.6 |
PHASE 4 — HTML GENERATION
File naming and directory
There is exactly ONE working directory for this skill — HTML sources and final PNGs both live here. Never create media_gen, outputs, or media_kit subfolders.
mkdir -p "D:\Claude\.claude\automations\notion\output"
Files (HTML build files; PNGs render to the same folder, same basename, .png extension):
D:\Claude\.claude\automations\notion\output\ig_1_hook.html
D:\Claude\.claude\automations\notion\output\ig_2_story.html
D:\Claude\.claude\automations\notion\output\ig_3_product.html
D:\Claude\.claude\automations\notion\output\ig_4_objection.html
D:\Claude\.claude\automations\notion\output\ig_5_cta.html
D:\Claude\.claude\automations\notion\output\whop_1_cover.html
D:\Claude\.claude\automations\notion\output\whop_2_inside.html
D:\Claude\.claude\automations\notion\output\whop_3_database.html
D:\Claude\.claude\automations\notion\output\whop_4_howto.html
D:\Claude\.claude\automations\notion\output\whop_5_value.html
No copy step. No second folder. If any script in this skill references media_gen, outputs, or media_kit, that is a bug — replace it with output.
CROSS-CONTAMINATION RULE (hard rule — this is the #1 cause of visibly broken renders)
Each HTML file's <body> contains only that slide's own copy — nothing else. Never leave in, paste, or reuse a headline/paragraph/line that belongs to a different slide. This exact mistake is what produces the broken renders seen previously: slide 1's hook text rendering stacked on top of slide 2's story lines, or a Whop headline overlapping its own tag pill/price badge because two content blocks occupy the same space.
Before rendering, for every HTML file, search it for the unique opening words of every OTHER slide's Phase-2 copy block. If any match is found, delete the stray block before proceeding — it does not belong there.
IG BASE TEMPLATE
<!DOCTYPE html><html><head><meta charset="utf-8">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html,body {
width:1080px; height:1350px;
background:#141413; overflow:hidden;
font-family:'Liberation Sans', Arial, sans-serif;
color:#F0F0EC;
}
.wrap {
position:absolute; left:83px; right:83px;
top:0; bottom:50px; /* slight upward bias */
display:flex; flex-direction:column; justify-content:center;
}
</style></head>
<body><div class="wrap">
<!-- SLIDE CONTENT HERE -->
</div></body></html>
WHOP BASE TEMPLATE
<!DOCTYPE html><html><head><meta charset="utf-8">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html,body {
width:1920px; height:1080px;
background:#141413; overflow:hidden;
font-family:'Liberation Sans', Arial, sans-serif;
color:#F0F0EC;
}
.binding {
position:absolute; left:64px; top:0; bottom:0; width:10px;
display:flex; flex-direction:column; justify-content:space-evenly; align-items:center;
}
.dot { width:11px; height:11px; border-radius:50%; background:#3A3A38; border:1px solid #56564f; }
.frame {
position:absolute; left:150px; right:110px; top:90px; bottom:90px;
display:flex; flex-direction:column;
}
.top-row { display:flex; justify-content:space-between; align-items:flex-start; flex:0 0 auto; }
.eyebrow { font-size:26px; letter-spacing:4px; text-transform:uppercase; color:#E7B24C; font-weight:700; }
.tag { font-family:'Liberation Mono',monospace; font-size:22px; color:#6f6e69;
border:1px solid #3A3A38; padding:8px 18px; border-radius:100px; white-space:nowrap; }
.mid {
flex:1 1 auto; display:flex; flex-direction:column; justify-content:center;
gap:36px; min-height:0; overflow:hidden;
}
.bottom-row { display:flex; justify-content:space-between; align-items:flex-end; flex:0 0 auto; }
.brand { font-size:26px; color:#8f8d86; }
.price-badge { font-family:'Liberation Mono',monospace; font-size:28px; color:#141413;
background:#E7B24C; padding:14px 26px; border-radius:8px; font-weight:700; white-space:nowrap; }
</style></head>
<body>
<div class="binding">
<span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
</div>
<div class="frame">
<div class="top-row">
<div class="eyebrow">EYEBROW</div>
<div class="tag">tag text</div>
</div>
<div class="mid">
<!-- MID CONTENT GOES HERE, AND ONLY HERE.
This is the ONLY place headline/cards/table/steps are placed.
Never position headline/body text with position:absolute — it will
overlap .top-row or .bottom-row. Everything flows inside .mid. -->
</div>
<div class="bottom-row">
<div class="brand">[emoji] Template Name</div>
<div class="price-badge">$X · lifetime access</div>
</div>
</div>
</body></html>
Why this structure matters: the frame no longer uses justify-content:space-between across three flex children of wildly different content length. Only .mid stretches (flex:1), and it centers its own children vertically with a fixed gap. This is what was broken in the previous render: a bare space-between on .frame spread .top-row, the mid content, and .bottom-row across the full 900px frame height with equal-feeling gaps regardless of how much content was in the middle — producing huge dead space on short slides (Image 3, 4, 5-style layouts) and, when a headline was instead positioned with position:absolute, direct overlap with the eyebrow/tag/price badge.
IG SLIDE 3 — TABLE PATTERN
<div style="font-size:22px;color:#6F6E69;margin-bottom:28px;">the actual [database name]</div>
<div style="border-top:1px solid #343330;">
<!-- Row template: -->
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;align-items:center;
padding:22px 0;border-bottom:1px solid #343330;">
<span style="font-size:32px;font-weight:700;color:#F0F0EC;">[Title]</span>
<span style="font-size:30px;color:#A2A098;">[Key Field]</span>
<span style="font-size:30px;color:#A2A098;text-align:right;">[Value]</span>
</div>
<!-- repeat for each row -->
</div>
<div style="margin-top:36px;font-size:28px;color:#A2A098;line-height:1.4;">
[insight line 1]<br>[insight line 2]
</div>
WHOP IMAGE 3 — TABLE PATTERN
The table sits inside .mid as its ONLY child (mid is display:flex; flex-direction:column; justify-content:center) — the table itself must be one single block element (display:block, not a flex item spread by a parent space-between). Header and every row are children inside that one block, stacked by normal document flow, each with its own border-bottom — never let header/rows become siblings of .top-row/.bottom-row, and never let them be justify-content:space-between children of .frame directly. That mistake is what produced the previous bug: a big empty gap between the header row and the first data row, and the data rows floating far down the canvas.
<div class="mid">
<div style="background:#1B1B1A;border:1px solid #2C2C2A;border-radius:16px;overflow:hidden;">
<!-- header row -->
<div style="display:grid;grid-template-columns:[match real columns];
padding:20px 34px;background:#202020;
font-family:'Liberation Mono',monospace;font-size:18px;
letter-spacing:1px;color:#6F6E69;text-transform:uppercase;">
<span>TITLE</span><span>[FIELD]</span><span>STATUS</span><span>[FIELD]</span>
</div>
<!-- data rows — 4 rows from real sample data, stacked with NO gap between them -->
<div style="display:grid;grid-template-columns:[same];align-items:center;
padding:24px 34px;border-bottom:1px solid #262624;">
<span style="font-size:24px;font-weight:700;color:#F0F0EC;">[Row Title]</span>
<span style="font-size:22px;color:#A2A098;">[Field Value]</span>
<span>
<span style="display:inline-block;font-size:20px;font-weight:700;
padding:6px 16px;border-radius:100px;
background:[pill-bg];color:[pill-text];">[Status] [emoji]</span>
</span>
<span style="font-size:22px;color:#A2A098;">[Field Value]</span>
</div>
<!-- repeat for rows 2-4, last row has border-bottom:none -->
</div>
</div>
WHOP IMAGE 2 — FEATURE CARD GRID PATTERN
Every card MUST include emoji + title + description. A card with only a title (no description) is a render failure — the previous run produced exactly this by leaving descriptions out or clipping them with an undersized fixed height. Cards use min-height, never a fixed height, so description text is never silently clipped.
<div class="mid" style="justify-content:center;">
<div style="font-size:44px;font-weight:700;color:#F0F0EC;line-height:1.15;margin-bottom:44px;max-width:1400px;">
[headline]
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:28px;">
<div style="background:#1B1B1A;border:1px solid #2C2C2A;border-radius:16px;padding:36px 30px;min-height:260px;">
<div style="font-size:44px;margin-bottom:16px;">[emoji]</div>
<div style="font-size:30px;font-weight:700;color:#F0F0EC;margin-bottom:14px;">[view name]</div>
<div style="font-size:22px;color:#A2A098;line-height:1.5;">[2-3 sentence description — REQUIRED, never omit]</div>
</div>
<!-- repeat for card 2, card 3 -->
</div>
</div>
WHOP IMAGE 4 — STEPS PATTERN
Same rule: each step needs number + emoji + title + 2-line description, all present, laid out inside .mid, never spread by an outer space-between.
<div class="mid" style="flex-direction:row;justify-content:space-between;align-items:flex-start;">
<div style="max-width:400px;">
<div style="font-family:'Liberation Mono',monospace;font-size:24px;color:#E7B24C;margin-bottom:12px;">01</div>
<div style="font-size:32px;font-weight:700;color:#F0F0EC;margin-bottom:14px;">[emoji] [step name]</div>
<div style="font-size:22px;color:#A2A098;line-height:1.5;">[line 1]<br>[line 2]</div>
</div>
<div style="font-size:36px;color:#3A3A38;padding-top:40px;">→</div>
<!-- repeat step block + arrow for steps 2 and 3 (no arrow after step 3) -->
</div>
PHASE 5 — RENDERING
HTML source and PNG output live in the same single folder — OUTPUT_DIR below. There is no separate build directory.
from playwright.sync_api import sync_playwright
import os
OUTPUT_DIR = r"D:\Claude\.claude\automations\notion\output"
os.makedirs(OUTPUT_DIR, exist_ok=True)
jobs = [
# (html_file, output_png, canvas_width, canvas_height)
("ig_1_hook.html", "ig_1_hook.png", 1080, 1350),
("ig_2_story.html", "ig_2_story.png", 1080, 1350),
("ig_3_product.html", "ig_3_product.png", 1080, 1350),
("ig_4_objection.html", "ig_4_objection.png", 1080, 1350),
("ig_5_cta.html", "ig_5_cta.png", 1080, 1350),
("whop_1_cover.html", "whop_1_cover.png", 1920, 1080),
("whop_2_inside.html", "whop_2_inside.png", 1920, 1080),
("whop_3_database.html", "whop_3_database.png", 1920, 1080),
("whop_4_howto.html", "whop_4_howto.png", 1920, 1080),
("whop_5_value.html", "whop_5_value.png", 1920, 1080),
]
with sync_playwright() as p:
browser = p.chromium.launch()
for html_name, png_name, w, h in jobs:
page = browser.new_page(
viewport={"width": w, "height": h},
device_scale_factor=3 # 3, not 2 — see note below. Higher DPI = crisper text/edges.
)
page.goto("file:///" + os.path.join(OUTPUT_DIR, html_name).replace("\\", "/"))
page.wait_for_timeout(150) # let webfont/layout settle before capture
page.screenshot(path=os.path.join(OUTPUT_DIR, png_name))
page.close()
print(f"rendered: {png_name}")
browser.close()
print("All done.")
device_scale_factor=3, not 2: the previous output looked visibly soft/blurry compared to the examples. Rendering at 3x internal resolution (e.g. the IG canvas rasterizes at 3240×4050 before final delivery) produces noticeably crisper text edges and pill borders, matching the sharpness of examples/. This is the main lever for the "quality has degraded" complaint — always render at 3x, never 1x, never 2x.
PHASE 6 — QUALITY CHECKS
After rendering, view each PNG before proceeding. Fix-then-re-render for any failure.
IG slide checks
- Left edge text ≥83px from canvas left
- No text touches or clips the right edge (max-width enforced)
- Nothing bleeds below 1350px
- No amber color anywhere (#E7B24C must NOT appear on any IG slide)
- No binding dots on IG slides
- Slide 1 headline reads in under 2 seconds — if longer, shorten it
- Slide 3 table rows show REAL sample data (not placeholder lorem ipsum)
- Slide 5 price number is large and dominant — the biggest element on the slide
Whop image checks
- Binding dots visible on left edge
- Amber on eyebrow AND price badge
- Headline legible when mentally squinted to 300px (thumbnail test)
- Image 3 table uses REAL property names from the template — not invented columns
- Image 3 status pills match the EXACT status option names from the template
- Price badge shows the CORRECT extracted price
- All 5 images share the same visual grammar (binding, eyebrow, price badge, brand)
Overlap / contamination / whitespace (the three failures seen previously)
- No two text blocks visually overlap anywhere in the image — eyeball every corner, especially top-row vs mid-content and mid-content vs bottom-row
- Zero cross-contamination: no slide's HTML contains another slide's headline/story/copy (re-run the grep check from Phase 4 if unsure)
- No single unexplained gap taller than ~15% of canvas height between two content blocks — if present, the layout is using
space-betweenwhere it should use a centered.midwrapper; fix per Phase 4 pattern - Every feature card shows emoji + title + a non-empty description — a title-only card is a failure
- Table header sits directly above row 1 with no gap larger than the row padding itself
Both
- Output PNG file size > 150KB (too small = render failure or under-rendered/blank canvas)
- Background pixel sampled from corner = rgb(20, 20, 19) — not pure black
- device_scale_factor=3 used in all renders — text/edges must look crisp, not soft, when zoomed in
- All 10 files exist in
D:\Claude\.claude\automations\notion\outputand nowhere else
PHASE 7 — OUTPUT
No copy step. The PNGs already landed in D:\Claude\.claude\automations\notion\output in Phase 5 — that is the final delivery location. Just enumerate them:
import os
OUTPUT_DIR = r"D:\Claude\.claude\automations\notion\output"
files = [
"ig_1_hook.png", "ig_2_story.png", "ig_3_product.png",
"ig_4_objection.png", "ig_5_cta.png",
"whop_1_cover.png", "whop_2_inside.png", "whop_3_database.png",
"whop_4_howto.png", "whop_5_value.png",
]
paths = [os.path.join(OUTPUT_DIR, f) for f in files]
for p in paths:
assert os.path.exists(p) and os.path.getsize(p) > 150_000, f"missing or too small: {p}"
print("All 10 verified in", OUTPUT_DIR)
Call present_files with all 10 paths (all under D:\Claude\.claude\automations\notion\output).
Then print this usage guide:
Instagram carousel — post order: Slides 1→5. Slide 1 is what shows in grid. Mirror slide 1's hook in your caption first line. Add DM keyword at end of caption AND as first comment (boosts reach).
Whop listing — upload order:
whop_1_cover.png → hero thumbnail (shown in discover grid). whop_2 through whop_5 → gallery in order. Whop allows 5 listing images maximum.
ANTI-PATTERNS
Copy — never do these
- Slide 1 must NOT name the product
- Never open with "introducing"
- Never use "features include:" anywhere in any image
- Never write "[Name] is a Notion template that..."
- Never apologize for the price — state it, justify it, move on
- Never exceed 12 words on an IG hook (slide 1)
- Never put the price before slide 4 in the IG carousel
- IG slides must be ALL LOWERCASE — no shouted words, no sentence-case titles
Design — never do these
- Background =
#141413not#000000 - Text primary =
#F0F0ECnot#ffffff - Amber
#E7B24Cis WHOP-ONLY — never use in IG slides - Binding dots are WHOP-ONLY — never add to IG slides
- No Google Fonts — network is blocked during render
- No background images, gradients, or box-shadow
- No border-radius on text blocks — only on cards (16px) and pill tags (100px)
- No more than 3 feature cards side-by-side (3-column grid max)
Design — layout, never do these (added after prior render broke on these exactly)
- Never put
justify-content:space-betweenon.framedirectly with variable-length mid content — it spreads content into huge empty gaps. Use the.midwrapper pattern (Phase 4). - Never
position:absolutea headline or body text block — it will overlap the eyebrow/tag row or the brand/price row. Everything flows inside.mid. - Never let a table's header and rows become siblings of
.top-row/.bottom-row— they must be nested inside one block container inside.mid. - Never render a feature card with a title but no description — description is mandatory, card uses
min-heightnotheight. - Never leave another slide's copy inside a file — one file, one slide's content, no exceptions (see Cross-Contamination Rule, Phase 4).
Technical — never do these
- Never use
device_scale_factor=1or2— always 3 (sharper text/edges; this is the main quality-vs-examples gap) - Never write HTML or PNG files anywhere except
D:\Claude\.claude\automations\notion\output— nomedia_gen, nooutputs\media_kit, no other subfolder - Never skip viewing rendered images before delivering — check every one against the Overlap/contamination/whitespace checklist in Phase 6
- Never fabricate sample data — read actual rows from the Notion page
- Never assume status names — extract them from the template's real Select options
EXECUTION CHECKLIST
[ ] 1. notion-fetch the Notion page URL
[ ] 2. Extract all REQUIRED EXTRACTIONS — print them explicitly
[ ] 3. Draft all 10 copy blocks (Phase 2) — print before building
[ ] 4. Confirm copy with user (or proceed after 5 seconds if no objection)
[ ] 5. mkdir -p "D:\Claude\.claude\automations\notion\output" (the ONLY folder used)
[ ] 6. Generate all 10 HTML files directly into that folder (Phase 4), using the .mid wrapper
pattern — no position:absolute text, no space-between on .frame
[ ] 7. Grep each HTML file for other slides' copy — delete any cross-contamination found
[ ] 8. Write render script (device_scale_factor=3) and execute (Phase 5) — PNGs land in the
same folder as the HTML
[ ] 9. view each PNG — run the full Phase 6 checklist (overlap, whitespace, contamination,
card descriptions, table spacing, sharpness) — fix and re-render any failure
[ ] 10. Verify all 10 PNGs exist in D:\Claude\.claude\automations\notion\output and are >150KB
[ ] 11. present_files with all 10 paths
[ ] 12. Print usage guide