Conference Poster Generator (HTML)
Generate a professional HTML poster. User notes: $ARGUMENTS
The poster is a React-based interactive editor — a single self-contained HTML file in the poster/ directory. No build step needed (React/Babel loaded via CDN). The user can visually adjust the layout in their browser, then export the config back to Claude for further changes.
Project folder structure
<project>/
├── overleaf/ # Paper source from Overleaf
│ ├── paper.tex
│ ├── figures/
│ └── ...
├── references/ # Reference posters for style matching
│ └── (any format: pdf, png, jpg, html, pptx, ...)
├── poster/ # GENERATED: self-contained poster website
│ ├── index.html # The poster (React app)
│ ├── poster-config.json # Layout config (columns, card order, heights, font scale)
│ ├── logos/ # Institution logos
│ ├── teaser.png # Copied/converted figures
│ ├── qr.png # Project page QR code
│ ├── qr-posterskill.png # Posterskill QR code
│ └── ...
└── .claude/skills/make-poster/
overleaf/contains the paper source. Read the main.texfile and any files it\input{}s.references/contains example posters showing the user's preferred visual style. Read/view ALL files in this folder to match their design language.poster/is the generated output — a self-contained website. All figures and assets live alongsideindex.htmlso relative paths just work.
Inputs
- Paper source - Located in
overleaf/. Ask the user which.texfile is the main one to read (e.g.,paper.tex,main.tex). Then read it and any files it\input{}s. - Project website - Ask the user for the URL if not already known. Fetch with WebFetch to extract author info, hosted images, and links.
- Reference posters - Auto-discovered from
references/. View all files there and match their style. - Author website (optional) - Fetch with WebFetch and extract design signals (color palette, typography, logos). Download institutional logos from the author's site using Playwright (curl may fail due to redirects):
page.request.get(url)to download the raw bytes. - Formatting requirements - Ask for poster dimensions, orientation, number of columns.
- Git repo (optional) - Ask the user if they have a GitHub repo to push the poster to.
If the user doesn't specify formatting, ask them before proceeding. Don't assume defaults for dimensions, orientation, or column count.
Process
Step 0: Analyze style references
Look in references/ for any PDF, PNG, or image files. Convert PDFs to PNGs (sips -s format png on macOS). View each one and note the visual style — layout, colors, typography, card styles, figure placement. Match the reference style — don't default to a dark theme if the reference is light, etc.
Step 1: Extract content from paper source
Ask the user which .tex file to read. Extract: title, authors, affiliations, abstract (2-3 sentences), key method, results (tables + figures), key equations (1-2 max), conclusion.
Step 2: Fetch the project website
Use WebFetch to get author names, affiliations, figure URLs, project URL for QR, links to code/arxiv/video.
Step 3: Gather assets into poster/
Figures: Copy from overleaf/figures/, converting PDFs to PNGs at high resolution:
sips -s format png input.pdf --out poster/output.png -Z 3000
Website images: Download higher-quality images from the project website using Playwright (not curl — many sites redirect):
resp = page.request.get(url)
with open('poster/filename.png', 'wb') as f:
f.write(resp.body())
Logos: Download institutional logos from the author's personal website using Playwright. Save to poster/logos/. The template auto-inverts them to white for the header.
QR codes: Generate and save:
curl -sL -o poster/qr.png "https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=PROJECT_URL"
curl -sL -o poster/qr-posterskill.png "https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=https://github.com/ethanweber/posterskill"
Step 4: Measure image aspect ratios
This is critical for eliminating whitespace. Measure every image:
sips -g pixelWidth -g pixelHeight poster/*.png poster/*.jpg
Then assign images to columns based on aspect ratio:
- Wide images (>2:1 ratio, e.g. teaser, architecture): put in the widest column
- Square images (~1:1 ratio): put in narrow columns
- Portrait images (<1:1 ratio): put in the narrowest column
This prevents the #1 whitespace problem: wide images in narrow cells (or vice versa) leaving huge gaps.
Step 5: Generate the poster HTML
Use the template at ${{CLAUDE_SKILL_DIR}}/template.html as a starting point. The template is a React app with:
Architecture:
CARD_REGISTRY— defines each card's content (title, color, JSX body)DEFAULT_LAYOUT— defines column structure and card orderingDEFAULT_LOGOS— institutional logos for the header- React state manages layout, with localStorage persistence
window.posterAPIexposes functions for programmatic control
Key things to customize:
- Update
CARD_REGISTRYwith the paper's content (each section is a card) - Update
DEFAULT_LAYOUTwith the aspect-ratio-optimized column assignments - Update
DEFAULT_LOGOSwith the user's institutional logos - Update
DEFAULT_FONT_SCALE(start at 1.3, user can adjust with A-/A+ buttons) - Update the header (title, authors, affiliations, conference badge, QR codes)
- Update
@page { size: WIDTHmm HEIGHTmm; }andbody { width: WIDTHmm; height: HEIGHTmm; }for the poster dimensions - Update
posterAPIfit() function with the same dimensions
Card content patterns:
- Figure card:
<div className="fig"><div className="fig-wrap"><img src="file.png" alt="..." /></div><div className="cap"><b>Caption title.</b> Description.</div></div> - Text card:
<div className="hl"><p>Highlight text</p></div><ul><li>Point 1</li></ul> - Table card:
<table><thead>...</thead><tbody>...</tbody></table>withclassName="best"on winning cells - Equation card:
<div className="eq">{'$LaTeX equation$'}</div>(escape backslashes in JSX)
Critical CSS rules for zero whitespace:
- Images MUST use
width:100%; height:100%; object-fit:contain(NOT max-width/max-height — those prevent upscaling) - Each column must always have one card with
grow: true(flex:1) that fills remaining space - The
isCardGrow()function ensures this automatically — if no card has grow, the last card gets it
Viewport scaling:
- Use
translate() + scale()on body to center and fit the poster to any browser viewport transform-origin: top leftis critical@media printmust settransform: none !importantfor correct print resolutiongetBoundingClientRect()returns SCALED values — always divide bycurrentScaleRef.currentin resize handlers
Step 6: Auto-optimize layout with Playwright
After generating, use Playwright to measure whitespace and find optimal column widths:
from playwright.sync_api import sync_playwright
import os
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={'width': 3200, 'height': 2260})
page.goto('file://' + os.path.abspath('poster/index.html'))
page.wait_for_load_state('networkidle')
page.wait_for_timeout(3000)
# Measure whitespace
waste = page.evaluate('window.posterAPI.getWaste()')
print(f"Total waste: {waste['total']}px")
for d in waste['details']:
print(f" {d['card']}: H={d['wasteH']} W={d['wasteW']} ({d['pct']}%)")
# Try different column widths to minimize waste
best_waste = waste['total']
best_c1, best_c3 = 300, 230
for c1 in range(200, 350, 10):
for c3 in range(160, 280, 10):
page.evaluate(f'window.posterAPI.setColumnWidth("col1", {c1})')
page.evaluate(f'window.posterAPI.setColumnWidth("col3", {c3})')
page.wait_for_timeout(30)
w = page.evaluate('window.posterAPI.getWaste().total')
if w < best_waste:
best_waste = w
best_c1, best_c3 = c1, c3
# Apply best and screenshot
page.evaluate(f'window.posterAPI.setColumnWidth("col1", {best_c1})')
page.evaluate(f'window.posterAPI.setColumnWidth("col3", {best_c3})')
page.wait_for_timeout(500)
page.screenshot(path='/tmp/poster_screenshot.png')
# Also try swapping cards between columns
page.evaluate('window.posterAPI.swapCards("cardA", "cardB")')
# ... measure waste again ...
browser.close()
Then read /tmp/poster_screenshot.png to visually inspect. Iterate multiple times — take screenshots, fix issues, re-screenshot until the poster has minimal blank space.
After finding optimal values, bake them into DEFAULT_LAYOUT, DEFAULT_CARD_HEIGHTS, etc. in the HTML.
Step 7: Generate PDF and verify
page.pdf(
path='poster/poster.pdf',
width='841mm', height='594mm', # match poster dimensions
margin={'top':'0','right':'0','bottom':'0','left':'0'},
print_background=True
)
Convert the PDF to PNG and read it to verify it renders at full resolution:
sips -s format png poster/poster.pdf --out /tmp/poster_pdf_check.png -Z 3000
Step 8: Open and iterate with user
Open the poster in the browser:
open poster/index.html
Explain the editing controls to the user:
- Preview — toggle edit UI off to see exactly how it will print
- A-/A+ — adjust font size globally
- Drag column dividers (vertical blue bars) — resize columns left/right
- Drag row dividers (horizontal blue bars) — resize cards up/down within columns
- Click-to-swap — click one card's diamond handle (turns orange), then click another's to swap them
- Move/insert — click a card's handle, then click a dashed orange drop zone to move it there
- Save — downloads
poster-config.json - Copy Config — copies layout JSON to clipboard
- Reset — restore defaults
Proactively suggest improvements: After showing the first draft, suggest specific changes:
- "The model architecture card has some whitespace — try dragging the row divider above it down to give it less space"
- "The completion figure might look better in column 2 since it's wider — try clicking its diamond, then clicking a drop zone in column 2"
- "You might want to bump the font size with A+ a few times"
Encourage the feedback loop: Tell the user:
Try rearranging the poster in your browser! When you're happy with the layout, click Copy Config in the top-right toolbar and paste it here — I'll bake those changes into the defaults so they persist.
- Save — download
poster-config.json - Copy Config — copy layout JSON to clipboard to paste to Claude
- Reset — restore defaults
When the user pastes a config JSON, update DEFAULT_LAYOUT, DEFAULT_CARD_HEIGHTS, DEFAULT_FONT_SCALE, and DEFAULT_LOGOS in the HTML to match. Also write it to poster-config.json.
Step 9: Push to GitHub (optional)
If the user provides a GitHub repo URL:
cd poster
git init
git remote add origin <REPO_URL>
git add .
git commit -m "Poster: <paper title>"
git push -u origin main
Important guidelines
- No blank space. This is the #1 priority. Use aspect-ratio-aware column assignment,
width:100%; height:100%; object-fit:containon images, auto-grow cards, and the Playwright optimizer. Iterate until waste is minimal. - Keep text minimal. Posters are visual — bullet points, not paragraphs. 2-minute understanding.
- Match the reference style. If the reference poster is light/clean, don't use a dark theme. Match the overall aesthetic.
- Font scaling. All text sizes use
calc(Xpt * var(--font-scale))so the A-/A+ buttons work. Start with--font-scale: 1.3and let the user adjust. - Print-optimized CSS.
@media printhides all edit UI and setstransform: none !important.@pagesets exact dimensions. - Posterskill QR. Always include a QR code linking to
https://github.com/ethanweber/posterskillin the header with the label "Poster made with my Claude skill". - No acknowledgements footer. Keep the poster clean — no footer by default.
- Logos in header. Download institutional logos from the author's website, save to
poster/logos/, and list them inDEFAULT_LOGOS. They're auto-inverted to white via CSS filter. - Self-contained. No build step, no npm, no server. Single HTML file with CDN dependencies. Works when opened directly as
file://. - Equations. Use KaTeX (loaded via CDN). Escape backslashes in JSX strings:
{'$\\mathcal{E}$'}.
Figure handling
- Always copy needed figures into
poster/— don't referenceoverleaf/paths in the HTML. - Convert PDFs to PNGs at high resolution:
sips -s format png input.pdf --out poster/output.png -Z 3000 - Download website images via Playwright's
page.request.get()(not curl — websites often redirect). - Measure aspect ratios with
sips -g pixelWidth -g pixelHeightand assign to columns accordingly.
User workflow for config updates
The user can adjust the poster in their browser and share changes back:
- User clicks "Copy Config" in the toolbar
- User pastes the JSON in chat
- Claude updates
DEFAULT_LAYOUT,DEFAULT_CARD_HEIGHTS,DEFAULT_FONT_SCALE,DEFAULT_LOGOSinindex.htmlto match - Claude also writes the config to
poster-config.json - User refreshes and clicks "Reset" to load new defaults
Programmatic API (window.posterAPI)
Available in the browser console or via Playwright:
swapCards(id1, id2)— swap two cards (works across columns)moveCard(cardId, targetColId, position)— move a card to a specific positionsetColumnWidth(colId, widthMm)— set column width (null for flex)setCardHeight(cardId, heightMm)— set explicit card height (null to reset)setFontScale(scale)— set global font scalegetWaste()— measure total whitespace in figure containersgetLayout()— get current layout with rendered dimensionsgetConfig()— get full serializable configresetLayout()— restore defaultssaveConfig()— trigger download of poster-config.jsoncopyConfig()— copy config JSON to clipboard