make-poster
Generate an HTML conference poster from a paper and project website, printable to PDF
git clone https://github.com/ethanweber/posterskill
T=$(mktemp -d) && git clone --depth=1 https://github.com/ethanweber/posterskill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/make-poster" ~/.claude/skills/ethanweber-posterskill-make-poster && rm -rf "$T"
.claude/skills/make-poster/SKILL.mdConference 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/
contains the paper source. Read the mainoverleaf/
file and any files it.tex
s.\input{}
contains example posters showing the user's preferred visual style. Read/view ALL files in this folder to match their design language.references/
is the generated output — a self-contained website. All figures and assets live alongsideposter/
so relative paths just work.index.html
Inputs
- Paper source - Located in
. Ask the user whichoverleaf/
file is the main one to read (e.g.,.tex
,paper.tex
). Then read it and any files itmain.tex
s.\input{} - 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
. View all files there and match their style.references/ - 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):
to download the raw bytes.page.request.get(url) - 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/
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:
— defines each card's content (title, color, JSX body)CARD_REGISTRY
— defines column structure and card orderingDEFAULT_LAYOUT
— institutional logos for the headerDEFAULT_LOGOS- React state manages layout, with localStorage persistence
exposes functions for programmatic controlwindow.posterAPI
Key things to customize:
- Update
with the paper's content (each section is a card)CARD_REGISTRY - Update
with the aspect-ratio-optimized column assignmentsDEFAULT_LAYOUT - Update
with the user's institutional logosDEFAULT_LOGOS - Update
(start at 1.3, user can adjust with A-/A+ buttons)DEFAULT_FONT_SCALE - Update the header (title, authors, affiliations, conference badge, QR codes)
- Update
and@page { size: WIDTHmm HEIGHTmm; }
for the poster dimensionsbody { width: WIDTHmm; height: HEIGHTmm; } - Update
fit() function with the same dimensionsposterAPI
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:
with<table><thead>...</thead><tbody>...</tbody></table>
on winning cellsclassName="best" - Equation card:
(escape backslashes in JSX)<div className="eq">{'$LaTeX equation$'}</div>
Critical CSS rules for zero whitespace:
- Images MUST use
(NOT max-width/max-height — those prevent upscaling)width:100%; height:100%; object-fit:contain - Each column must always have one card with
(flex:1) that fills remaining spacegrow: true - The
function ensures this automatically — if no card has grow, the last card gets itisCardGrow()
Viewport scaling:
- Use
on body to center and fit the poster to any browser viewporttranslate() + scale()
is criticaltransform-origin: top left
must set@media print
for correct print resolutiontransform: none !important
returns SCALED values — always divide bygetBoundingClientRect()
in resize handlerscurrentScaleRef.current
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,
on images, auto-grow cards, and the Playwright optimizer. Iterate until waste is minimal.width:100%; height:100%; object-fit:contain - 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
so the A-/A+ buttons work. Start withcalc(Xpt * var(--font-scale))
and let the user adjust.--font-scale: 1.3 - Print-optimized CSS.
hides all edit UI and sets@media print
.transform: none !important
sets exact dimensions.@page - Posterskill QR. Always include a QR code linking to
in the header with the label "Poster made with my Claude skill".https://github.com/ethanweber/posterskill - No acknowledgements footer. Keep the poster clean — no footer by default.
- Logos in header. Download institutional logos from the author's website, save to
, and list them inposter/logos/
. They're auto-inverted to white via CSS filter.DEFAULT_LOGOS - 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
— don't referenceposter/
paths in the HTML.overleaf/ - 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
(not curl — websites often redirect).page.request.get() - Measure aspect ratios with
and assign to columns accordingly.sips -g pixelWidth -g pixelHeight
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
inDEFAULT_LOGOS
to matchindex.html - 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:
— swap two cards (works across columns)swapCards(id1, id2)
— move a card to a specific positionmoveCard(cardId, targetColId, position)
— set column width (null for flex)setColumnWidth(colId, widthMm)
— set explicit card height (null to reset)setCardHeight(cardId, heightMm)
— set global font scalesetFontScale(scale)
— measure total whitespace in figure containersgetWaste()
— get current layout with rendered dimensionsgetLayout()
— get full serializable configgetConfig()
— restore defaultsresetLayout()
— trigger download of poster-config.jsonsaveConfig()
— copy config JSON to clipboardcopyConfig()