docsmith — make a PDF
Turn a markdown source into a polished, on-brand PDF. Each run renders one template branded as one company; every visual — diagram, chart, or image — is hand-written raw SVG embedded into that output (no diagramming library, no image generation, no external image host).
PLUGIN_DIR below is the folder that contains scripts/build.py and
assets/templates/. Resolve it once at the start by locating scripts/build.py:
- In Claude Code,
${CLAUDE_PLUGIN_ROOT}is authoritative (the plugin root, two levels up from this SKILL.md). - When this skill was installed standalone via
npx(the universal~/.agents/skills/make-pdf/bundle, e.g. on OpenClaw/Hermes/Codex),scripts/andassets/sit right beside this SKILL.md — soPLUGIN_DIRis this file's own directory.
Always-apply quality checklist (document-style PDFs)
For long-form documents — anything built with the handbook template (reports,
guides, research, handbooks) — these seven checks are part of a finished PDF, not
optional polish. A reader notices their absence immediately (a blank page, a
title-less cover, a wall of unexplained jargon, dead citation text). Apply them by
default; only skip one if the user opts out or it genuinely doesn't fit the
content. (Slide decks — corporate-deck/claudecode-deck/kawaii-storybook — are
exempt from the glossary; the rest still help.)
- No blank pages. The handbook builds digital-first (
oneside+openany), so each chapter opens on the next available page and thebookclass never inserts the filler pages (header + folio only) it would otherwise use to force chapters onto a recto. There is nothing to strip — the reader never hits an empty sheet. (A doc that will actually be printed-and-bound can opt back into recto openings withoverrides.classoptions: [twoside, openright]in its front-matter.) - Use callouts/quotes/plain-English where they help. The handbook ships
::: note·::: tip·::: warning·::: plain("In Plain English") ·::: pullquote(a large navy quote) ·::: do/::: dont·::: cheatsheet. Add a::: plainbox wherever a section leans on jargon so a non-expert can follow; pull a memorable sentence into a::: pullquote; flag traps with::: warning. These break up dense prose and are the difference between a wall of text and a guide. - Add SVG diagrams to aid comprehension. A roadmap, pipeline, comparison, or "how it works" flow lands far better as a picture. Author at least the key one(s) as hand-written raw SVG (Step 5). A timeline/roadmap and an architecture pipeline are the highest-leverage diagrams for most reports.
- Make every link clickable and uniformly light-blue. Author every external
source as a markdown link with descriptive text —
[Amazon Ads MCP beta](https://…), not a bare URL or plain text. The handbook template now renders all hyperlinks — TOC entries, internal cross-refs, external URLs, and citations — in one uniform light blue (linkblue#2F80ED, vialinkcolor/toccolor/urlcolor/citecolor), so a proper markdown link becomes an href-styled citation and the TOC reads as clickable too (no more black internal links). Bare URLs do NOT auto-link in pandoc — they render as dead text, so always use[label](url). - Cover (page 1) must carry the title. The cover is
[logo] → COMPANY → title → subtitle → author → date + version, drawn bytitlepage.tex. It pullstitle/subtitle/versionfrom the source front-matter — so if those are missing the cover renders blank/title-less. Confirm the front-matter has at leasttitle:(recommend asubtitle:andversion:too) before building; if the source has none, propose a title/subtitle and add them (Step 1). - Author/colophon (page 2).
titlepage.tex's\dsauthorpageprints a colophon (company, author, email, website, copyright, confidentiality) from the chosen org profile automatically — nothing to author, but verify the chosen org's profile fields are filled so the page isn't sparse. - Glossary for jargon-heavy docs. End long/technical documents with a short
## Glossary— a two-column term/meaning table. It makes the document usable by readers outside the immediate domain and is cheap to add.
Tight-list gotcha: in pandoc, a bullet/numbered list must be preceded by a
blank line. A bold lead-in immediately followed by - item (no blank line)
collapses into one run-on paragraph. Ensure a blank line before every list.
Step 0 — first-run config
The profile lives at $DOCSMITH_HOME/profile.yaml (default ~/.docsmith/profile.yaml)
and drives identity + branding for every document. If it does not exist, create it
with the portable, dependency-free setup script (works in any agent — it does
not rely on AskUserQuestion, and it also makes the ~/.docsmith/{template,cache,logo}
dirs):
python3 PLUGIN_DIR/scripts/setup_profile.py # interactive: prompts each field, loops over orgs
The script is the canonical writer — the same one the npx installer runs at
install time, so the YAML shape stays identical everywhere. (If you already know
the org details, you may instead gather company/author/… up front — in Claude
Code via AskUserQuestion — and pipe them in non-interactively:
echo '[{"company":"Acme Corp","author":"Docs Team"}]' | python3 PLUGIN_DIR/scripts/setup_profile.py --json --mode overwrite.)
Tell the user where the profile landed.
The profile is a YAML list of self-contained org objects — each entry is one
organization make-pdf can brand a document as, picked per run by company. Every
entry carries company, author, email, logo, wordmark, website,
default_confidentiality, and copyright, e.g.:
- company: "Acme Corp"
author: "Docs Team"
email: "docs@acme.example"
logo: "~/.docsmith/logo/acme.png" # this org's own logo
wordmark: "" # text fallback when no logo
website: ""
default_confidentiality: "Confidential" # Public/Internal/Confidential/Restricted; "" = none
copyright: "© 2026"
A per-document front-matter or --profile/--company/--logo always overrides
these.
The logo has to read at footer size. A deck footer renders it ~40px tall
(the handbook cover/colophon small too), so prefer a square-ish SVG or a crisp
transparent PNG. A dark, busy, or wide-aspect logo collapses into an unreadable
blob at that size — if a build's footer logo looks wrong, suspect the asset before
the CSS. (Legacy DICT profiles — a single flat org, or a
top-level company list paired with a logos: map — are still read by
build.py for back-compat, but new profiles should use the list-of-orgs form.)
Step 1 — read the source
Read the source markdown. Parse its YAML front-matter:
template:→ the default target. (templates: [..]from older sources is still read; since one template builds per run, treat its first entry as the default.)- title/subtitle/date/version etc. → document metadata. If the user named a template in their request, prefer that.
Cover check (checklist #5): the cover pulls title (and ideally subtitle,
version) from front-matter. If the source has no title:, the cover renders
title-less — propose a title + subtitle and add a front-matter block before
building. While here, also plan the document-style checklist: are there sections
that need a ::: plain explainer, a concept that wants a diagram (Step 5), bare
URLs that should become [label](url) links, and (for long docs) a ## Glossary?
Enrich a loose outline — don't just transcribe it. A source that arrives as a bare content dump (a list of slide topics, raw meeting notes, a training-module outline) becomes a far stronger deck once you add the structural slides a reader expects but the outline omits: a titled cover, an agenda / learning-objectives slide near the front, a closing slide, and — when the content is one part of a series — a roadmap slide that places it in the arc. These aren't padding; they orient the audience and are most of what separates a finished deck from a pasted bullet list. Plan the additions and briefly surface them to the user before building, so they can redirect up front rather than discover them in the finished PDF. (When the user says something like "add any loose info before execution," this is what they're asking for.)
Decide the output folder once, up front. Author the source .md, its
diagrams/ subfolder, and the built .pdf together in ONE folder you've agreed on
with the user (e.g. ~/project/<topic>/<part>/). Diagrams embed by ABSOLUTE path
(Step 5), so picking a home after the fact means moving every file and hand-editing
every SVG path — cheap to avoid, tedious to undo. If you don't know where it should
live, ask before authoring rather than defaulting to a scratch dir and relocating
later.
Step 2 — doctor
Run python3 "$PLUGIN_DIR/scripts/doctor.py". If a required tool is missing,
surface the install hint and stop.
Step 3 — choose ONE template (HITL)
List the available templates (ls "$PLUGIN_DIR/assets/templates"). For each one,
read its one-line style summary from assets/templates/<name>/template.yaml (the
description: field) so the chooser shows what each template looks like, not just
its name. Use AskUserQuestion with multiSelect: false — "Render to which
template?" — passing each template as an option (label = template name, description
= its template.yaml description), with the front-matter default pre-selected.
Exactly one template is built per run. (Skip the prompt only if the user already
named exactly one template.)
Step 4 — choose ONE company (HITL)
The profile is a LIST of org objects so one identity can brand many orgs. Read
~/.docsmith/profile.yaml and use AskUserQuestion with multiSelect: false —
"Brand this document as which company?" — offering each org entry's company as
an option (pre-select the first). A document brands exactly one company, so this
is single-select. (Skip the prompt only if the profile has a single org, or the
user already named one.)
Pass the chosen name to build.py in Step 6 as --company. You do not resolve
a logo here — build.py looks up the chosen org in the profile list and pulls
that org's own logo, author, email, etc. automatically. (Only pass
--logo if the user explicitly wants to override the org's logo for this one
document.)
Identity beyond the logo — override per document in front-matter. The company
is the org-level identity, but author, email, copyright, etc. can be set in
the source front-matter to win over the profile for THIS document — front-matter
beats profile. This matters because on a deck the footer is auto-composed from
logo · company · author · copyright, so putting author: "Anggraeni Wisono" in
the front-matter is how you credit a specific presenter without editing the shared
profile. (On the handbook the same fields feed the page-2 colophon.) So when a user
says "brand as Acme but the author is Jane," keep --company Acme and add
author: "Jane" to the front-matter rather than hunting for a CLI flag.
Step 5 — diagrams, charts & images (all hand-written raw SVG)
Every visual is hand-written raw SVG — flow diagrams, bar/line charts, data
viz, timelines, comparison graphics, and illustrative or decorative art alike.
There is no other image path: no d2, no Mermaid, no image generation, no fetching
from an image host or R2 bucket. If the content calls for a picture, you author it
as plain XML (<rect>, <line>, <text>, <path>, <polygon>, <circle>) with
manual coordinates — which is what keeps every output self-contained and on-brand.
Keep the .svg files in the diagrams/ subfolder of the agreed output folder
(Step 1) and embed each via a markdown image with an ABSOLUTE path:
{width=80%}
There is no pre-render step and no diagram manifest: the handbook (pandoc+tectonic)
auto-converts SVG→PDF via rsvg-convert, and decks (marp) embed SVG via Chrome.
(Absolute paths are required — the build runs from a temp dir, so relative image
paths won't resolve. The image alt text becomes the figure caption.)
Give every SVG an explicit width and height, not just a viewBox. The
handbook backend (rsvg-convert) can size from a viewBox alone, but decks embed
the SVG as an HTML <img>, and Chrome collapses a size-less <img> to zero
height — the slide builds "successfully" with the graphic simply gone, no error
raised. Setting both attributes
(<svg ... viewBox="0 0 980 360" width="980" height="360">) renders identically on
both backends, so it's the safe default everywhere.
Add diagrams by default (checklist #3): for an explanatory document, don't ship
pure prose — author at least the key diagram(s). The highest-leverage ones are a
roadmap/timeline and an architecture/pipeline flow; comparisons and
"how it works" loops also land far better as a picture. Validate each SVG before
embedding with rsvg-convert -f pdf -o /tmp/x.pdf diagrams/x.svg — a malformed
SVG fails the build.
Match the active template's palette so a diagram reads as part of the page, not
a pasted foreign asset. The navy brand palette (navy #003060, amber #E0821A,
violet #5A3A8A, green #1A7A3A) fits the handbook and corporate-deck;
claudecode-deck wants its warm editorial tones (clay #B85838, cream #F0EEE6,
peach #F5E6DA, ink #262620); kawaii-storybook wants soft pastels. When unsure,
read the chosen template's assets/templates/<name>/design-system.md for its exact
tokens before authoring.
On claudecode-deck (the "claude"/Claude brand), SVGs do NOT use a white
background. Its split/stack figures sit directly on the cream wash with no
white card behind them, so author every SVG with a transparent ground and fill
shapes from the cream/surface/peach tokens (never #fff), letting ink/clay strokes
do the definition. A white panel behind a diagram reads as a pasted foreign asset
against this warm editorial page.
kawaii-storybook uses this same SVG flow for art, not just diagrams. Beyond
emoji mascots, author a hand-written raw SVG hero/character and embed it as the
hero of a path/figure/split/cover slide (;
add <!-- _class: figure bare --> to drop the white card so the character sits on
the wash), and paint a full-bleed storybook background with marp's native
directive (, or ![bg right:40%] /
![bg opacity:.3] to keep text legible). Same authoring as a handbook diagram —
hand-written SVG, validate with rsvg-convert, embed by ABSOLUTE path; here use the
soft pastel palette instead of the navy brand colours. Decks also support
<aside class="callout tip"> callouts (blank line around the inner content) and
styled fenced code blocks — see references/authoring-guide.md.
concept-deck is the SVG-first tech-doc deck — author from its SVG-template design system.
Each concept is normally one full-canvas SVG (figure full), so the SVG is the slide.
Before authoring, read assets/templates/concept-deck/icons.md — a two-mode system:
(A) flat black-outlined pastel concept-card diagrams (pipelines, loops, layered systems,
multi-panel composites) and (B) isometric illustration for the cover + hero scenes
(flat-shaded 3-tint cuboids — the ByteByteGo course-cover look). Copy a starting point from
assets/templates/concept-deck/svg-templates/ (iso-cover.svg, iso-objects.svg) or the
worked examples in examples/concept-deck/diagrams/, then edit. Ink is black #0A0A0A; the
electric-blue signal #3FA9F5 is an accent only — for text on the near-white field
(eyebrow, em, title word, page number) use the deepened #1A6AAE, never the bright signal.
Step 6 — build the template
Build the one chosen template inline — run build.py directly and verify the
output. A single render is one deterministic command, so do NOT spawn a subagent
for it: a subagent only adds latency and token cost for no benefit. Pass the
--company chosen in Step 4 (build.py resolves that org's logo/author/etc. from
the profile):
python3 "$PLUGIN_DIR/scripts/build.py" --in "$SOURCE" --out "$OUT" \
--template "$TEMPLATE" \
--company "$COMPANY"
OUT defaults to <source-dir>/<source-stem>.<template>.pdf. Add --logo "$LOGO"
only to override the chosen org's own logo, and --profile "$PROFILE" only if the
user supplied one. The script prints OK <path> (<pages> pages, <size>); confirm
the PDF exists, has ≥1 page, and — for decks — is 1440 x 810 pts.
Fan out to subagents ONLY for a genuine multi-template run. If the user asked
for several templates from one source in the same run, spawn one
template-builder subagent per template so the renders run in parallel (each
gets SOURCE, PLUGIN_DIR, its TEMPLATE, an OUT, the chosen --company, and
optional PROFILE/--logo). For one template — the common case — build inline as
above; never spawn a subagent just to run a single build.
Step 7 — check links, then report
For handbook builds, run the internal link-integrity check on the finished PDF (the handbook builds digital-first, so there are no blank/filler pages to strip — see checklist #1):
python3 "$PLUGIN_DIR/scripts/check_links.py" "$OUT"
It walks every page's link annotations plus the document outline/bookmarks (incl.
the TOC), and verifies each internal link/cross-ref/bookmark resolves to a real
in-range page; external URLs are validated for syntax only (no network). It exits
non-zero on a broken/mismatched internal link. If it reports a FAIL, surface the
listed links and fix the source (a bad [text](#anchor) ref, a renamed
heading, a malformed citation) before calling the PDF done — don't ship a
handbook with a dead internal link or TOC entry. (External-syntax WARNs flag
empty/placeholder URLs like a bare https:// — fix those too. If pypdf is
missing offline the check skips with a warning and exits 0, never blocking a
build.)
Optional — external 404 check (ask first). The default check is network-free. After the internal check passes, OFFER the user a live external-link check — ask via AskUserQuestion: "Also check external URLs for 404s? (needs network, may be slow)" with options Run it / Skip. Only if they choose to run it:
python3 "$PLUGIN_DIR/scripts/check_links.py" "$OUT" --external
This HEAD/GET-probes every external URL: a 404/410 is a FAIL (a genuinely dead link — fix or drop it); other 4xx/5xx and unreachable/offline URLs are WARNs (so a flaky network never blocks the build). Skipping is always fine — it is not part of the default gate.
Then report the output PDF with its final page count and size. If the build
failed, surface its error. Mention that the authoring conventions live in
references/authoring-guide.md.
Before declaring done, sanity-check the document-style checklist held: cover has a
title, citations and the TOC render as light-blue links (not dead text / not black),
key diagrams are present, and (for long docs) there's a glossary. If you can, render
a couple of pages to PNG (pdftoppm -png -r 90 -f N -l N "$OUT" /tmp/check) and
eyeball the cover + a content page — image review catches a title-less cover or
links that aren't the uniform light blue that text checks miss.
For an SVG-heavy deck, eyeball the actual SVG slides, not just the cover. Because a size-less SVG collapses silently (Step 5), a deck can build with a missing diagram and no error to warn you — so render each slide that embeds an SVG to PNG and confirm the graphic is present and on-palette before declaring done. Count the slide numbers carefully when picking pages: the structural slides you added in Step 1 (cover, agenda, closing, roadmap) shift every downstream page offset, so the "diagram slide" is rarely the page number you'd guess from the source order. Glance at the footer while you're there: the logo, company·author line, and page number should render the same on every slide class — a layout class can hide or shift that shared chrome without raising an error, and a poor logo asset shows up as a blurry blob at footer size.
Authoring quick reference
- Front-matter selects
template(s)+ metadata (title/subtitle/versionfeed the cover);author:overrides the profile and feeds the deck footer / handbook colophon;overrides:tweaks tokens per-doc. - Output: co-locate the source
.md,diagrams/, and built.pdfin one agreed folder up front — SVGs embed by absolute path, so relocating later means rewriting every path. - Visuals (diagrams, charts, images — all of them): hand-written raw SVG (plain XML — /////, manual coordinates; no d2/Mermaid/image-gen/R2 host). Give each an explicit
width+height(not justviewBox) or Chrome collapses it to nothing on decks. Embed via{width=80%}; validate withrsvg-convertfirst, and match the active template's palette. - Handbook callouts:
::: note/::: tip/::: warning/::: plain("In Plain English") /::: do/::: dont/::: cheatsheet/::: pullquote. - Citations & links: write external sources as markdown links
[label](url)so they render in the uniform light blue (linkblue#2F80ED) + stay clickable; TOC entries and internal cross-refs are the same blue now (bare URLs become dead text). Always put a blank line before a list. - Long docs: end with a
## Glossaryterm/meaning table; after building, run the internal link-integrity checkscripts/check_links.py "$OUT"(Step 7). The handbook builds digital-first (oneside+openany) so there are nobook-class blank pages to strip. - Decks: separate slides with
---; pick a layout per slide with<!-- _class: kpi -->(cover, kpi, split, quote, versus, statement, closing, …).kawaii-storybookaddspath(+accept/reject/caution),laws,scorecard,flow,scenarios,roadmap,figure(+bare), renders emoji 🐻🦊🦉🐹 as mascots, and — via the same hand-written SVG flow as the handbook — supports SVG hero/character art, full-bleedscenes,<aside class="callout tip">callouts, and styled fenced code blocks. Seereferences/authoring-guide.mdfor the full contract andreferences/adding-a-template.mdto add a new template.