pdf-onepager
Produces high-craft, print-safe, AI-citation-ready one/two-page PDFs from
HTML + print CSS rendered by WeasyPrint — not a design tool, not a flattened
image. You edit a content HTML file; gen.py renders it to a tagged PDF in both
US Letter and A4 plus a PNG preview; verify.py gates the citation/utility
checklist before you ship.
Why this approach: the asset's whole value is that its text is selectable, its metadata is set, and its source URL is live in the document — the citation infrastructure that makes ChatGPT, Perplexity, and Google AI Overview cite it. WeasyPrint emits tagged PDF/UA from clean HTML, and CSS handles the layout math (centering, columns, page breaks) for you.
It works for any brand out of the box: a neutral default preset plus two
worked examples (feedbackpulse, enpstools). Make your own by copying one
file. Paths below are relative to the skill directory (where this file lives).
Setup (one-time)
cd <skill-dir> # the folder containing this SKILL.md
./setup.sh # installs native deps hint, builds .venv, smoke-renders
setup.sh is idempotent. It needs system libraries for WeasyPrint; if they are
missing it prints the exact install line for your OS:
- macOS:
brew install pango cairo gdk-pixbuf libffi poppler - Debian/Ubuntu:
sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 libffi-dev poppler-utils
If the skill is installed read-only via the plugin marketplace, copy gen.py,
verify.py, setup.sh, requirements.txt, styles/, templates/, fonts/
into your project (or clone the repo) and run there, so the .venv and out/
have a writable home.
Run (agent path) — render + verify
cd <skill-dir>
# Neutral default brand (Atkinson Hyperlegible + indigo), Letter + A4 + preview:
./.venv/bin/python gen.py templates/starter.html
# A real example, branded, one flag to swap brand:
./.venv/bin/python gen.py templates/example-sbi.html --brand feedbackpulse
# QA against the citation checklist (exit 0 = ship-able):
./.venv/bin/python verify.py out/starter.pdf --source yourdomain.com/your-source-page
gen.py writes <stem>.pdf (Letter), <stem>-a4.pdf (A4), and
<stem>-preview.png (page-1 render) into --out (default out/). Open the
preview and actually look at it — verify.py checks structure, not whether the
layout looks good.
verify.py FAILs (blocking) on: missing Title/Author/Subject/Keywords/Creator,
no /Lang, untagged PDF (no StructTreeRoot), any page without selectable text,
or the --source URL missing from the text. WARNs on file size over 600 KB.
Authoring a NEW asset
- Copy the starter, keep the structure, replace every
[bracketed]placeholder:cp templates/starter.html templates/<your-slug>.html - Set the metadata in
<head>—<title>,<meta name="author|description| keywords|generator">, and<html lang>. WeasyPrint reads these into the PDF properties; they are how AI engines identify the document. Do not leave blank. - Set the running-footer URL — the one per-document override:
body { string-set: docsource "yourdomain.com/the-page"; }. Put the same URL in the masthead link and the.docfootso the citation appears three times. - Use the components in
styles/base.css:.definition(tinted intro),.framework/.col(equal columns with rules),.callout-key(the single loudest element — make it the memorable hook),.worksheet/.fill(dotted fill-in lines),.cols2,.checklist,.page-2(back of the one-pager). - Render + verify + LOOK. If page 1 overflows, adjust margins/font-size in
the HTML's
<style>or base.css; WeasyPrint reflows, so don't fight pixels. - Manual AI test (required, can't be automated): drag the PDF into both ChatGPT and Claude and ask "What is this document and where can I read more?" The answer must name the topic and surface your source URL. If not, the metadata or footer URL is wrong — fix and re-render.
- Ship to a STABLE URL. Host at
/assets/downloads/<slug>.pdf(and-a4.pdf), publicly (not behind a form). When you update, replace the file at the same URL — never version the path, or backlinks and AI citations break. Optionally addDigitalDocumentJSON-LD on the page that links it.
Make it your brand
Only colors and fonts are brand-specific; layout is shared. To add a brand:
cp styles/brand-template.css styles/brand-acme.css # then fill in colors/fonts
./.venv/bin/python gen.py templates/starter.html --brand acme
brand-template.css is fully commented. The one rule that matters is contrast:
--accent is your vivid color for fills/rules; --accent-deep is for accent
text on white and must clear 4.5:1; --callout-bg/--on-callout is the loudest
block and must also clear 4.5:1 (if your brand color is too light for white text,
use a navy/near-black there — see brand-enpstools.css, where teal fails so the
callout is navy). Drop brand .ttf files into fonts/ and wire @font-face as
shown in the template.
What makes these good (encoded in base.css)
- One loud element.
.callout-keyis the highest-contrast block; everything else is calm. White background, no full-bleed color, no gradients/shadows — it must read as a tool, not a flyer. - Brand via presets, never hardcoded. Every color/font is a CSS variable in
styles/brand-*.css;base.cssconsumes them. - Contrast enforced by variable split (
--accentvs--accent-deep,--callout-bg/--on-callout) — see "Make it your brand". - No em dashes in copy. They read as an AI tell; write them out (period/comma/colon). The templates are already clean.
- Dual page size from one template.
base.csssets margins but not size;gen.pyinjects@page { size: letter | A4 }per build.
Gotchas
- Use the venv's python, every time. System
python3lacks WeasyPrint;./.venv/bin/pythonhas it. The #1 "module not found" cause. - Native libs are a real dependency. If
import weasyprintfails, you are missing the OS packages — re-run./setup.shand follow its install line. On macOS Homebrew,DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/libresolves rare dylib-lookup failures. - Fonts are bundled (OFL) in
fonts/and embedded+subsetted into the PDF, so output is identical on every machine. To use your own brand font, add the.ttfand wire@font-facein your brand CSS. Variable fonts work — declarefont-weight: 100 900on the@font-face(seebrand-feedbackpulse.css). pdf/ua-1needs a<title>and<html lang>. Both are in the templates; deleting them degrades the tagged-PDF output.- Keep it under 600 KB.
optimize_images=Trueis on; don't embed large rasters. The asset should be email-attachable and fast to crawl. - Put the key points as text on the download page too, so AI has the "answer key" without parsing the PDF.
References
- Example brand presets:
styles/brand-feedbackpulse.css(Product Bridge Blue) andstyles/brand-enpstools.css(navy/teal, the clearest contrast demo). - Sample output: regenerate any time with
gen.pyintoout/. - Bundled fonts:
fonts/with the OFL license text for each family. - Deeper PDF/UA validation (optional): run the file through veraPDF for a
formal accessibility conformance claim;
verify.pychecks the structural markers (tags, metadata, selectable text), not full PDF/UA conformance.