| name | make-poster |
| description | Generate an HTML conference poster from a paper and project website, printable to PDF |
| argument-hint | <any formatting notes, conference name, or instructions URL> |
| user-invocable | true |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep, WebFetch, Agent |
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 .tex file 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 alongside index.html so relative paths just work.
Inputs
- Paper source - Located in
overleaf/. Ask the user which .tex file 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 ordering
DEFAULT_LOGOS — institutional logos for the header
- React state manages layout, with localStorage persistence
window.posterAPI exposes functions for programmatic control
Key things to customize:
- Update
CARD_REGISTRY with the paper's content (each section is a card)
- Update
DEFAULT_LAYOUT with the aspect-ratio-optimized column assignments
- Update
DEFAULT_LOGOS with 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; } and body { width: WIDTHmm; height: HEIGHTmm; } for the poster dimensions
- Update
posterAPI fit() 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> with className="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 left is critical
@media print must set transform: none !important for correct print resolution
getBoundingClientRect() returns SCALED values — always divide by currentScaleRef.current in 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)
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']}%)")
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
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')
page.evaluate('window.posterAPI.swapCards("cardA", "cardB")')
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',
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:contain on 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.3 and let the user adjust.
- Print-optimized CSS.
@media print hides all edit UI and sets transform: none !important. @page sets exact dimensions.
- Posterskill QR. Always include a QR code linking to
https://github.com/ethanweber/posterskill in 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 in DEFAULT_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 reference overleaf/ 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 pixelHeight and 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_LOGOS in index.html to 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 position
setColumnWidth(colId, widthMm) — set column width (null for flex)
setCardHeight(cardId, heightMm) — set explicit card height (null to reset)
setFontScale(scale) — set global font scale
getWaste() — measure total whitespace in figure containers
getLayout() — get current layout with rendered dimensions
getConfig() — get full serializable config
resetLayout() — restore defaults
saveConfig() — trigger download of poster-config.json
copyConfig() — copy config JSON to clipboard