# Make PDF

> docsmith — make a PDF

- Skill: `labspangaea/make-pdf` (Agent Skill)
- Install (CLI): `npx skillmds@latest add labspangaea/make-pdf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/labspangaea/make-pdf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: labspangaea (https://skillmd.com/u/labspangaea)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/labspangaea/make-pdf

---


# 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/`
  and `assets/` sit **right beside this SKILL.md** — so `PLUGIN_DIR` is 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.)

1. **No blank pages.** The handbook builds digital-first (`oneside`+`openany`), so
   each chapter opens on the next available page and the `book` class 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
   with `overrides.classoptions: [twoside, openright]` in its front-matter.)
2. **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 `::: plain` box 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.
3. **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.
4. **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`, via
   `linkcolor`/`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)`.
5. **Cover (page 1) must carry the title.** The cover is `[logo] → COMPANY → title
   → subtitle → author → date + version`, drawn by `titlepage.tex`. It pulls
   `title`/`subtitle`/`version` from the source **front-matter** — so if those are
   missing the cover renders blank/title-less. Confirm the front-matter has at
   least `title:` (recommend a `subtitle:` and `version:` too) before building; if
   the source has none, propose a title/subtitle and add them (Step 1).
6. **Author/colophon (page 2).** `titlepage.tex`'s `\dsauthorpage` prints 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.
7. **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.:
```yaml
- 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:
    ![Caption](/abs/path/diagrams/funnel.svg){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 (`![Bara](/abs/diagrams/hero.svg)`;
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 (`![bg cover](/abs/diagrams/scene.svg)`, 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`/`version` feed 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 `.pdf` in 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 — <rect>/<line>/<text>/<path>/<polygon>/<circle>, manual coordinates; no d2/Mermaid/image-gen/R2 host). Give each an explicit `width`+`height` (not just `viewBox`) or Chrome collapses it to nothing on decks. Embed via `![Caption](/abs/diagram.svg){width=80%}`; validate with `rsvg-convert` first, 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 `## Glossary` term/meaning table; after building, run the internal link-integrity check `scripts/check_links.py "$OUT"` (Step 7). The handbook builds digital-first (`oneside`+`openany`) so there are no `book`-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-storybook` adds `path` (+ `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-bleed `![bg cover](/abs/scene.svg)` scenes, `<aside class="callout tip">` callouts, and styled fenced code blocks.
See `references/authoring-guide.md` for the full contract and
`references/adding-a-template.md` to add a new template.

