
Takes HTML templates and turns them into print-ready PDFs with proper pagination, brand colors, and budget tables. Built for grant proposals and program reports where you need letter-sized output that looks designed, not just exported. The interactive preview loop is the real workflow: you say "preview," it screenshots the current state, you request changes, repeat until done. Ships with CCM brand guidelines and a Democracy Day template. Handles the annoying parts like footer clearance rules and snap-confined Chromium paths. Uploads finished PDFs straight to Google Drive folders.
npx -y skills add jamditis/claude-skills-journalism --skill pdf-design --agent claude-codeInstalls into .claude/skills of the current project.
Create and edit professional PDF reports and funding proposals with live preview and iterative design.
During a design session, use these commands:
| Command | Action |
|---|---|
preview | Screenshot current state |
preview page N | Screenshot specific page |
show cover | Preview cover page |
show budget | Preview budget section |
regenerate | Create new PDF |
upload | Confirm and upload to a user-chosen destination |
done | Finish session |
Workflow:
Locate this installed SKILL.md and use its parent directory as
SKILL_DIR. Resolve templates/ and other bundled resources from that
directory. Do not assume that the skill is installed below a specific client
home directory.
Use the absolute SKILL.md location from the available-skills catalog. Its
parent directory is SKILL_DIR; resolve templates/ from there.
When a plugin invocation exposes CLAUDE_PLUGIN_ROOT, use that directory as
SKILL_DIR. For a legacy single-skill install under
~/.claude/skills/pdf-design, use the parent directory of the loaded
SKILL.md. Do not use the legacy path as the shared default.
Replace the installed-skill placeholder path before running this block. Run the block from the project directory and in one shell so the exit trap removes the disposable browser directory.
set -eu
PROJECT_DIR="$PWD"
SKILL_DIR="/absolute/path/to/installed/pdf-design"
BROWSER_BIN="${BROWSER_BIN:-chromium-browser}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/browser-profile/Default"
printf '%s\n' '{"profile":{"default_content_setting_values":{"javascript":2}}}' \
> "$WORK_DIR/browser-profile/Default/Preferences"
cp "$SKILL_DIR/templates/democracy-day-proposal.html" "$PROJECT_DIR/new-report.html"
cp "$PROJECT_DIR/new-report.html" "$WORK_DIR/new-report.html"
"$BROWSER_BIN" --headless=new --disable-gpu \
--user-data-dir="$WORK_DIR/browser-profile" \
--print-to-pdf="$WORK_DIR/output.pdf" \
--no-pdf-header-footer \
"file://$WORK_DIR/new-report.html"
cp "$WORK_DIR/output.pdf" "$PROJECT_DIR/output.pdf"
:root {
--ccm-red: #CA3553;
--ccm-black: #000000;
--ccm-gray: #666666;
--ccm-light: #e2e8f0;
}
:root {
--civic-navy: #1a2b4a;
--civic-blue: #2d4a7c;
--civic-gold: #c9a227;
--civic-red: #b31942;
}
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&family=Source+Sans+Pro:wght@300;400;600&display=swap" rel="stylesheet">
body {
font-family: 'Source Sans Pro', sans-serif;
font-size: 0.875rem;
line-height: 1.6;
}
h1, h2, h3 {
font-family: 'Montserrat', sans-serif;
}
@page { size: letter; margin: 0; }
.page {
width: 8.5in;
height: 11in;
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
page-break-after: always;
}
<div class="page cover">
<div class="cover-header">
<div class="cover-org">Center for Cooperative Media</div>
<h1 class="cover-title">Report title</h1>
<p class="cover-intro">Brief description.</p>
</div>
<div class="cover-footer">
<div class="cover-stats"><!-- Stats --></div>
<div class="cover-footer-right">
<div class="cover-date">February 2026</div>
<div class="cover-logo"><img src="..." alt="Logo"></div>
</div>
</div>
</div>
<div class="page content-page">
<div class="page-header">
<div class="page-header-title">Document title</div>
<div class="page-number">2</div>
</div>
<div class="page-body">
<!-- Content goes here -->
</div>
<footer class="page-footer">
<!-- Footer -->
</footer>
</div>
<table class="budget-table">
<thead>
<tr><th>Expense</th><th>Per year</th><th>Total</th></tr>
</thead>
<tbody>
<tr>
<td>Item<span class="item-desc">Details</span></td>
<td>$10,000</td>
<td>$20,000</td>
</tr>
</tbody>
<tfoot>
<tr><td>Total</td><td>$50,000</td><td>$100,000</td></tr>
</tfoot>
</table>
.page-body {
padding: 0.2in 0.65in 0.3in;
overflow: hidden;
}
.page-footer {
padding: 0 0.65in 0.5in;
border-top: 1px solid #e2e8f0;
font-size: 0.8rem;
}
Content must not touch or overlap the page footer. These rules apply to content pages, cover pages and special layouts may use different structures.
display: grid; grid-template-rows: auto 1fr auto on .page.page-body), footeroverflow: hidden to prevent text bleedingposition: absolute for footers, keep them in normal document flow as the third grid row.page-footer:empty { display: none; } so pages without footer content don't render a blank borderUse the quick-start browser workflow as the default. Keep the source HTML and final PDF in the project directory. Stage only the render input and temporary output in the disposable directory, then let the exit trap remove it.
Use this adapter only when a snap-packaged Chromium rejects the disposable
directory. In the quick-start browser workflow, replace the default WORK_DIR
assignment with this snap-confined path. All browser profile, input, and output
commands will then use the same allowed directory:
WORK_DIR="$(mktemp -d "$HOME/snap/chromium/common/pdf-work.XXXXXX")"
trap 'rm -rf "$WORK_DIR"' EXIT
Stage the render in $WORK_DIR, copy the finished PDF back to the project
directory, and let the exit trap remove only this render's files. Keep the
script-blocking browser profile setting from the default workflow.
# PDF to PNG
pdftoppm -png -f 1 -l 1 output.pdf preview
# Page count
pdfinfo output.pdf | grep Pages
set -eu
PROJECT_DIR="$PWD"
BROWSER_BIN="${BROWSER_BIN:-chromium-browser}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/browser-profile/Default"
printf '%s\n' '{"profile":{"default_content_setting_values":{"javascript":2}}}' \
> "$WORK_DIR/browser-profile/Default/Preferences"
cp "$PROJECT_DIR/template.html" "$WORK_DIR/template.html"
"$BROWSER_BIN" --headless=new --disable-gpu \
--user-data-dir="$WORK_DIR/browser-profile" \
--screenshot="$WORK_DIR/preview.png" \
--window-size=1275,1650 \
"file://$WORK_DIR/template.html"
cp "$WORK_DIR/preview.png" "$PROJECT_DIR/preview.png"
Upload only when the user explicitly requests it after reviewing the local PDF.
Do not read or parse raw OAuth token files. Do not search for credentials, silently choose a remote folder, or upload to a hard-coded destination. If no connected integration is available, stop after local generation and explain how the user can upload the PDF themselves.
These patterns were proven out in the NJ Public TV walkthrough deck (pdf-playground 1.3.0) and work just as well inside report and proposal pages. Drop them into any .page-body or .cover-footer.
A tight 0.95in × 0.08in red bar under the headline reads cleaner than a full-width border. Use for section headers inside content pages.
.section-header h2 {
font-family: 'Montserrat', sans-serif;
font-size: 22pt;
font-weight: 800;
color: var(--ccm-black);
line-height: 1.08;
}
.section-header h2::after {
content: '';
display: block;
width: 0.95in;
height: 0.08in;
background: var(--ccm-red);
margin-top: 0.14in;
}
Row of 3–4 big numbers with a short caption and a red left rule. Great for executive-summary numbers on a cover or intro page.
<div class="stats-strip">
<div class="stat"><div class="big">23,000+</div><div class="label">Students enrolled</div></div>
<div class="stat"><div class="big">$660M</div><div class="label">Annual operating budget</div></div>
<div class="stat"><div class="big">$2.3B</div><div class="label">Economic impact</div></div>
<div class="stat"><div class="big">252</div><div class="label">Acre main campus</div></div>
</div>
.stats-strip {
display: grid;
grid-template-columns: repeat(var(--stat-cols, 4), 1fr);
gap: 0.3in;
}
.stats-strip .stat {
padding: 0.05in 0 0.1in 0.24in;
border-left: 4px solid var(--ccm-red);
}
.stats-strip .big {
font-family: 'Montserrat', sans-serif;
font-size: 28pt;
font-weight: 800;
line-height: 1;
color: var(--ccm-black);
letter-spacing: -0.015em;
}
.stats-strip .label {
font-size: 9pt;
font-weight: 600;
margin-top: 0.1in;
color: var(--ccm-gray);
text-transform: uppercase;
letter-spacing: 0.05em;
}
For breaking one topic into three parallel facets with a dashed divider between columns.
.three-col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.3in;
}
.three-col > div + div {
padding-left: 0.3in;
border-left: 2px dashed #d9d9d9;
}
.three-col h3 {
font-size: 10pt;
font-weight: 800;
color: var(--ccm-red);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.12in;
}
Numbered cards with a red top rule, for parallel capabilities, themes, or commitments. Common on proposal executive summary pages.
<div class="four-col-tiles">
<div class="tile">
<div class="tile-num">Pillar 01</div>
<h3>Proven facilities and expertise</h3>
<p>Short 2–3 line description.</p>
</div>
<!-- 3 more tiles -->
</div>
.four-col-tiles {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.15in;
}
.four-col-tiles .tile {
border-top: 4px solid var(--ccm-red);
padding: 0.18in 0.15in 0.15in;
background: #f7f6f5;
}
.four-col-tiles .tile-num {
font-size: 8pt;
font-weight: 800;
color: var(--ccm-red);
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 0.08in;
}
.four-col-tiles .tile h3 {
font-size: 12pt;
font-weight: 800;
color: var(--ccm-black);
margin-bottom: 0.08in;
}
.four-col-tiles .tile p {
font-size: 9pt;
line-height: 1.38;
color: var(--ccm-gray);
margin: 0;
}
4-column grid of labeled tiles with a red left accent bar. Use for sponsor lists, letters of support, or advisory board rosters.
.partner-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.12in 0.18in;
}
.partner-grid .partner {
position: relative;
padding: 0.14in 0.15in 0.14in 0.22in;
background: #f7f6f5;
font-size: 9pt;
font-weight: 600;
line-height: 1.25;
color: var(--ccm-black);
}
.partner-grid .partner::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 0.06in;
background: var(--ccm-red);
}
Every block above was tightened based on real presentation feedback. Key principles:
If a page feels too crowded, reduce content, don't expand spacing.
Resolve templates/democracy-day-proposal.html from the installed skill directory.