# Markdown To Pdf

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-vlebert-markdown-to-pdf-agent-skill-markdown-to-pdf`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [vlebert](https://agentstack.voostack.com/s/vlebert)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [vlebert](https://github.com/vlebert)
- **Source:** https://github.com/vlebert/markdown-to-pdf-agent-skill/tree/main/skills/markdown-to-pdf

## Install

```sh
agentstack add skill-vlebert-markdown-to-pdf-agent-skill-markdown-to-pdf
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# markdown-to-pdf

LaTeX-free PDF generation pipeline:

```
note.md ──[Pandoc + Lua filters]──▶ HTML ──[WeasyPrint + CSS]──▶ PDF
```

Pandoc converts the content (Markdown → HTML) and injects the frontmatter into
the template. WeasyPrint handles layout exclusively: pagination, headers/footers,
cover page, styles. Everything is bundled in this skill (template, Lua filters,
CSS, Docker image): no external dependency.

The skill stays **agnostic**: standard Markdown converts cleanly, and the
Obsidian transformations are only applied when the corresponding syntax is
present.

## 1. Prerequisite: the Docker image

Before generating, **make sure the image exists** — do not rely on session memory
(each session starts cold). The source of truth is Docker:

```bash
bash scripts/build-image.sh
```

This script is **idempotent**: it runs `docker image inspect markdown-to-pdf:latest`
and **returns immediately if the image already exists** (it does not rebuild). It
only builds (a few minutes) if the image is missing. Running it systematically
before a generation is therefore safe and nearly free.

Manual equivalent to decide: `docker image inspect markdown-to-pdf:latest`
(exit 0 = present, nothing to do; exit ≠ 0 = needs building).

When to rebuild with `--force`: **only if the `Dockerfile` changes** (WeasyPrint
version, fonts). Templates, CSS and Lua filters are mounted at runtime
(`-v …:/skill`), so modifying them **requires no rebuild**.

The image produces `markdown-to-pdf:latest` (Pandoc + WeasyPrint + fonts: system
Arial/Verdana/… + Google/open-source Inter, Roboto, Montserrat, etc.).

## 2. Prepare the frontmatter BEFORE generating

Inspect the note's YAML frontmatter. The keys feed the template; an incomplete
frontmatter yields a degraded PDF (no cover page, empty headers).

If useful keys are missing, **add them to the note** (without overwriting what's
there):

- at minimum `title`;
- as needed: `subtitle`, `author`, `date`, `lang`;
- for a cover page / table of contents: `titlepage`, `titlepage-logo`,
  `logo-width`, `titlepage-rule-color`, `toc`, `toc-depth`, `toc-title`.

Infer values from the content and context when obvious (e.g. `title` = H1 heading
or file name, `date` = today). **Ask the user for confirmation when in doubt**
rather than guessing silently: whether a cover page is wanted, which logo to use,
author, subtitle. See the full key table in `references/weasyprint-notes.md`.

## 3. Choose the Lua filters based on content

Only add `--lua-filter` for syntax actually present in the note (preserves
agnosticity — pure Markdown undergoes no Obsidian transformation):

| Filter | Add if the note contains… |
|---|---|
| `assets/lua/callouts.lua` | callouts `> [!TYPE]` |
| `assets/lua/embeds.lua` | image embeds `![[image.png]]`, `![[image.png\|200]]`, `![[image.png\|Caption\|200]]` |
| `assets/lua/mdlinks.lua` | text wikilinks `[[note]]` / `[[note\|alias]]` |
| `assets/lua/images.lua` | standard Markdown images with width in the caption `` |
| `assets/lua/headers_shift.lua` | the "H1 = title" convention to shift (H2→H1, etc.) — otherwise do NOT add it |

Detect quickly via grep on the note (e.g. `grep -E '> \[!' note.md` for callouts,
`grep -E '!\[\[' note.md` for image embeds, `grep -E '\[\[' note.md` for
wikilinks).

**Order matters**: load `embeds.lua` BEFORE `mdlinks.lua`. `embeds.lua` converts
`![[image]]` into images; otherwise `mdlinks.lua` would see the inner `[[…]]` and
treat it as a text link.

`embeds.lua` **resolves the image path itself** by searching for it by name under
the mounted folder (see step 5), including in any subfolder: it reproduces
Obsidian's behavior (finding an attachment wherever it is) and makes
`--resource-path` unnecessary for these embeds, regardless of the attachments
convention. The segments after `|` are classified automatically: number → width
(`px`), `WxH` → width+height, text → **caption**. An image alone in its paragraph
with a caption becomes a `` with `` (styled in the template's
CSS). `mdlinks.lua` renders text wikilinks as a plain label (alias if present,
otherwise the note name) — no dead link in the PDF.

## 4. Choose the PDF location

- **Default (agnostic)**: next to the source `.md` file.
- **Obsidian vault detected** (a `.obsidian/` folder exists when walking up the
  tree from the note): place the PDF in the attachments folder used by the note —
  infer it from the note's existing embeds (e.g. `_attachments`, `attachments/`).
  If relevant, insert a wikilink embed/link to the PDF in the source note
  (`![[my-doc.pdf]]`). "If relevant": judge by context, ask when in doubt.

## 5. Generate the PDF

Mount two volumes: the **root folder** (for the note AND its attachments) and the
skill folder (template/CSS/filters). Choose the mounted root based on step 4:

- **Obsidian vault**: mount the **vault root** on `/data` and reference the note
  by its relative path (e.g. `projects//quote.md`). This way `embeds.lua`
  can find any attachment in the vault, regardless of its storage convention;
- **outside a vault**: mount the folder containing the note (and its images).

```bash
SKILL_DIR="/path/to/this-skill"     # this skill's folder (markdown-to-pdf)
ROOT_DIR="/path/to/root"            # vault root, or the note's folder
NOTE="projects/.../note.md"         # note path RELATIVE to ROOT_DIR

docker run --rm \
  -v "$ROOT_DIR:/data" \
  -v "$SKILL_DIR:/skill:ro" \
  --workdir /data \
  --user "$(id -u):$(id -g)" \
  markdown-to-pdf:latest \
  "$NOTE" \
  --from markdown+lists_without_preceding_blankline \
  --pdf-engine=weasyprint \
  --template=/skill/assets/templates/document/template.html \
  --css=/skill/assets/templates/document/style.css \
  --lua-filter=/skill/assets/lua/callouts.lua \
  --lua-filter=/skill/assets/lua/embeds.lua \
  --lua-filter=/skill/assets/lua/mdlinks.lua \
  --number-sections \
  --toc --toc-depth=3 \
  --embed-resources \
  -o "projects/.../note.pdf"
```

Notes:

- Only include the `--lua-filter` flags selected in step 3, in order (callouts,
  embeds, mdlinks, images, headers_shift).
- No need for `--resource-path` for `![[…]]` embeds: `embeds.lua` resolves the
  path by searching. Only add it for standard Markdown images
  `` whose path is not resolved otherwise.
- **Heading numbering**: `--number-sections` is enabled by default (headings `1`,
  `2`, `2.1`…). Pandoc does not read this from the frontmatter; it's a CLI flag.
  Remove it for unnumbered headings (see step 6 for adjusting).
- **Table of contents**: add `--toc` (+ `--toc-depth=N`) if the note wants a
  table of contents; omit it otherwise. The title/depth can also come from the
  frontmatter (`toc-title`, `toc-depth`) via the template.
- `--embed-resources` inlines images and CSS into the intermediate HTML
  (self-contained PDF).
- The warning `Ticker: poll failed: Interrupted system call` is benign.

## 6. After generation: offer adjustments

Once the PDF is produced, don't stop there: present the result then **ask the
user whether anything needs adjusting**, explicitly listing the available levers
(they may not know them). If an adjustment is requested, change the relevant
flag/key and **regenerate**.

Levers to offer:

- **Heading numbering** — enabled by default; remove `--number-sections` for
  headings without numbers.
- **Table of contents** — presence (`--toc`), depth (`--toc-depth=N`), title
  (`toc-title` in the frontmatter).
- **Cover page** — enable/disable (`titlepage`), logo (`titlepage-logo`,
  `logo-width`), accent color (`titlepage-rule-color`).
- **Template / visual identity** — switch template (`--template` + `--css`), hence
  font, colors, margins (see step 7).
- **Header/footer metadata** — `title`, `author`, `date`, `lang` (frontmatter).

For frontmatter-driven levers, edit the note; for CLI flags (numbering, toc),
adjust the command and regenerate.

## 7. Create a new template

A template = a visual identity, i.e. a pair `template.html` + `style.css`. The Lua
filters are shared (generic Obsidian transformations) and need not be duplicated.

For a new template:

1. Copy `assets/templates/document/` to `assets/templates//`.
2. Adapt `template.html`: cover page structure, running elements
   (headers/footers), exposed `$key$` frontmatter variables.
3. Adapt `style.css`: colors (`--accent`…), fonts, `@page` margins,
   callout/table styles. Many Google/open-source fonts are bundled in the image
   (Inter, Roboto, Montserrat, Merriweather, JetBrains Mono…): usable directly by
   name in `font-family`. Full list and how to add new fonts:
   `references/weasyprint-notes.md`.
4. Point `--template` and `--css` to the new folder at generation time.

**Remember the template for this project.** A custom template only has value if
it gets reused. Since each session starts cold, after creating it write the
association in the `CLAUDE.md` (or `AGENTS.md`) **at the root of the user's
vault/site** — not in this repo: absolute path of the template folder, and the
associated options (cover page, toc, logo). Example: "PDF export: template
`…/assets/templates/acme/`, `titlepage` enabled, logo `assets/logo.png`". If it
doesn't exist, create it. This way a future session reuses the right template
without asking again.

The separation of content (Pandoc + Lua) and layout (template + CSS) is the
guiding principle: appearance is changed without touching conversion. For layout
pitfalls (running elements, cover page, tables, WeasyPrint 63 limitations), see
`references/weasyprint-notes.md`.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [vlebert](https://github.com/vlebert)
- **Source:** [vlebert/markdown-to-pdf-agent-skill](https://github.com/vlebert/markdown-to-pdf-agent-skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-vlebert-markdown-to-pdf-agent-skill-markdown-to-pdf
- Seller: https://agentstack.voostack.com/s/vlebert
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
