# markdown2pdf — full documentation bundle Source: https://markdown2pdf.eu · Repository: https://github.com/woodyjon/markdown2pdf · License: MIT --- # markdown2pdf Convert Markdown to PDF — three ways, one engine. The same Rust crate ([`markdown2pdf-core`](https://github.com/woodyjon/markdown2pdf/tree/main/rust/crates/core)) drives every entry point: a [web playground](/docs/playground), a [command-line tool](/docs/cli), an [agent skill](/docs/skill) for Claude / Codex / Pi / your other preferred agent, and an [embeddable Rust API](/docs/embedding) for your own programs. ## Why another markdown-to-PDF tool Because most options are either: - **Browser print-to-PDF** — bloated, fragile pagination, fonts depend on the OS. - **Pandoc + LaTeX** — heavyweight install, slow first run. - **Headless Chrome wrappers** — same fragility as print, plus a 200 MB chromium dependency. `markdown2pdf` uses [Typst](https://typst.app/) as the layout engine. Typst is a modern typesetting system written in Rust, fast, and produces clean vector PDFs. We embed the fonts (Inter + JetBrains Mono) so the output is portable and consistent everywhere — same PDF whether you ran it on macOS, in CI, or in a browser tab. ## Pick your entry point | Want to… | Use | |------------------------------------------------|--------------------------------------| | Convert a markdown file once, no install | [Web playground](/docs/playground) | | Convert files from a script or terminal | [CLI](/docs/cli) | | Have your agent convert markdown for you | [Agent skill](/docs/skill) | | Generate PDFs from your own Rust program | [Rust crate](/docs/embedding) | ## Open source MIT-licensed. Source at [github.com/woodyjon/markdown2pdf](https://github.com/woodyjon/markdown2pdf). Issues, PRs, and feedback welcome. ## For LLMs If you're an AI agent looking for a machine-readable summary, fetch [`/llms.txt`](/llms.txt) (short index per the [llmstxt.org](https://llmstxt.org/) spec) or [`/llms-full.txt`](/llms-full.txt) (every doc page, concatenated as plain text). --- # Web playground The web playground at [markdown2pdf.eu](https://markdown2pdf.eu) is the simplest way to use markdown2pdf — paste markdown on the left, see the live preview on the right, click **Download PDF**. ## How it works Everything runs in your browser. No upload, no server processing, nothing stored. - The live preview is rendered with [`markdown-it`](https://github.com/markdown-it/markdown-it) (instant feedback, GitHub-flavored). - The PDF is generated by a WebAssembly build of the same Rust crate that powers the CLI ([`markdown2pdf-core`](https://github.com/woodyjon/markdown2pdf/tree/main/rust/crates/core)). It uses [Typst](https://typst.app/) as the layout engine, with [Inter](https://rsms.me/inter/) and [JetBrains Mono](https://www.jetbrains.com/lp/mono/) embedded for portable, vector-text output. - The WASM module (~23 MB) is lazy-loaded on first download and cached for subsequent clicks. ## Supported markdown The playground handles GitHub-flavored markdown: - Headings (`# h1` through `###### h6`) - **Bold**, *italic*, ~~strikethrough~~ - Links - Inline `code` and fenced code blocks with syntax highlighting - Tables - Ordered, unordered, and task lists - Blockquotes - Horizontal rules > Images are **not rendered** yet. For `![alt](url "title")` the PDF gets the *title* in italics; with no title it falls back to the literal word *image*. The Markdown alt text is currently dropped. Tracked in the repo as a planned feature. ## Privacy The page is fully static. Your markdown never leaves your browser. No analytics, no tracking, no cookies. The only network request after the initial page load is fetching the WASM blob from the CDN. ## Self-hosting The site is a pure static bundle (`bun run build` → `build/`). Drop it on any static host: Firebase Hosting, Cloudflare Pages, GitHub Pages, Netlify, etc. See the [README](https://github.com/woodyjon/markdown2pdf#deploy) for the Firebase setup used at markdown2pdf.eu. --- # Command-line tool `markdown2pdf` is a single self-contained binary. No fonts, no runtime dependencies — just one file you can copy anywhere. ## Install ### One-line installer (recommended) **macOS / Linux / WSL:** ```sh curl -fsSL https://markdown2pdf.eu/install.sh | sh ``` Detects your OS/arch (`uname`), downloads the matching archive from the latest [GitHub Release](https://github.com/woodyjon/markdown2pdf/releases), verifies its SHA256, and drops the `markdown2pdf` binary into `/usr/local/bin/` (falls back to `~/.local/bin/` if it can't write there without sudo). Flags: ```sh curl -fsSL https://markdown2pdf.eu/install.sh | sh -s -- --to ~/.local/bin curl -fsSL https://markdown2pdf.eu/install.sh | sh -s -- --version v0.1.1 curl -fsSL https://markdown2pdf.eu/install.sh | sh -s -- --no-sudo ``` **Windows (PowerShell):** ```powershell powershell -c "irm https://markdown2pdf.eu/install.ps1 | iex" ``` Downloads `markdown2pdf-x86_64-pc-windows-msvc.zip` from the latest release, verifies SHA256, extracts `markdown2pdf.exe` into `%LOCALAPPDATA%\Programs\markdown2pdf\`, and adds that directory to your user PATH. Optional environment variables (set before piping to `iex`): ```powershell $env:M2P_INSTALL_DIR = "$HOME\.local\bin" $env:M2P_VERSION = "v0.1.1" $env:M2P_NO_PATH = "1" # skip the PATH update irm https://markdown2pdf.eu/install.ps1 | iex ``` ### Manual download If you'd rather not pipe the script: grab the right archive from the [GitHub Releases](https://github.com/woodyjon/markdown2pdf/releases) page: - `markdown2pdf-aarch64-apple-darwin.tar.gz` — macOS Apple Silicon - `markdown2pdf-x86_64-unknown-linux-gnu.tar.gz` — Linux x86_64 - `markdown2pdf-x86_64-pc-windows-msvc.zip` — Windows x86_64 Intel Macs aren't covered by a prebuilt binary (GitHub-hosted Intel-Mac runners are too unreliable to release against). Build from source instead: `cargo install --git https://github.com/woodyjon/markdown2pdf markdown2pdf-cli`. Extract the archive and put the binary somewhere on your `PATH`: ```sh # macOS / Linux tar xzf markdown2pdf-*.tar.gz sudo mv markdown2pdf /usr/local/bin/ ``` Each release also publishes `SHA256SUMS` so you can verify the download: ```sh sha256sum -c SHA256SUMS --ignore-missing ``` ### Build from source Requires a Rust toolchain (1.89+): ```sh cargo install --git https://github.com/woodyjon/markdown2pdf markdown2pdf-cli ``` The binary lands in `~/.cargo/bin/markdown2pdf`. ### Via the agent skill If you only want PDFs through your coding agent (Claude, Codex, Pi, or another), install the [agent skill](/docs/skill) instead — just ask your agent to grab it from . The skill auto-fetches the binary into `~/.cache/markdown2pdf/` on first use. No manual install step. ## Usage ```text markdown2pdf [OPTIONS] [INPUT] Arguments: [INPUT] Input file. Reads stdin if omitted. Options: -o, --output Output PDF path. Writes to stdout if omitted. -t, --title Optional title metadata for the PDF. -h, --help Print help. -V, --version Print version. ``` ## Examples ```sh # File → file markdown2pdf README.md -o README.pdf # Pipe in, pipe out cat notes.md | markdown2pdf > notes.pdf # Set PDF metadata title markdown2pdf -t "Q1 Report" report.md -o report.pdf # Convert all .md files in a directory for f in *.md; do markdown2pdf "$f" -o "${f%.md}.pdf"; done ``` ## What you get PDFs come out with: - A4 paper size, sensible margins - Vector text (searchable, copy-pasteable, no rasterization) - [Inter](https://rsms.me/inter/) for body, [JetBrains Mono](https://www.jetbrains.com/lp/mono/) for code — both embedded in the binary - GitHub-style typography (headings, tables, lists, blockquotes) - Syntax-highlighted code blocks via Typst's built-in highlighter The same renderer drives the [web playground](/docs/playground) and the [Rust crate](/docs/embedding). --- <!-- url: https://markdown2pdf.eu/docs/skill --> # Agent skill A drop-in [agent skill](https://docs.claude.com/en/docs/claude-code/skills) so any coding agent — Claude (Code, the API, claude.ai), Codex, Pi, or your other preferred agent — can convert Markdown to PDF on request. The skill is self-contained: a single SKILL.md with trigger conditions, install logic, and usage examples. The agent reads it on demand and runs the CLI when the user asks for a PDF. **The user does not need to install the CLI separately.** The skill auto-fetches the latest `markdown2pdf` binary into `~/.cache/markdown2pdf/` on first use — no sudo, no PATH change, no manual download. If `markdown2pdf` is already on the user's PATH, the skill uses that one and skips the download. ## Install the skill ### Just ask your agent (recommended) Most modern coding agents know how to install a skill from a GitHub repo. The simplest path is to tell yours: > Install the markdown2pdf skill from <https://github.com/woodyjon/markdown2pdf> The agent will fetch the skill folder (`skills/markdown2pdf/`) and drop it into the right place for its runtime — `~/.claude/skills/` for Claude Code, the equivalent skills directory for Codex, Pi, and others. After that, ask it to *"convert this README to PDF"* and the skill takes over. This works for any agent that supports skills. The skill folder follows the standard `SKILL.md` format, so it is portable across runtimes. ### Claude Code — via `/plugin` The repo is also a [Claude Code plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces), so inside Claude Code you can install through the plugin manager: ```text /plugin marketplace add woodyjon/markdown2pdf /plugin install markdown2pdf@markdown2pdf ``` The first line registers the GitHub repo as a marketplace; the second installs the `markdown2pdf` plugin, which bundles this skill. Run `/plugin` on its own for the interactive manager (Discover / Installed / Marketplaces tabs), and `/plugin marketplace update` to pull a newer version later. You can also pre-trust the marketplace and enable the plugin for a project by committing this to `.claude/settings.json`: ```json { "extraKnownMarketplaces": { "markdown2pdf": { "source": { "source": "github", "repo": "woodyjon/markdown2pdf" } } }, "enabledPlugins": { "markdown2pdf@markdown2pdf": true } } ``` ### Manual copy (any agent) If your agent can't fetch the skill itself, drop the folder in by hand: ```sh mkdir -p ~/.claude/skills cd ~/.claude/skills curl -L https://github.com/woodyjon/markdown2pdf/archive/refs/heads/main.tar.gz \ | tar xz --strip-components=2 markdown2pdf-main/skills/markdown2pdf ``` Replace `~/.claude/skills` with whichever skills directory your agent reads from. Or clone the whole repo and symlink: ```sh git clone https://github.com/woodyjon/markdown2pdf.git ln -s "$(pwd)/markdown2pdf/skills/markdown2pdf" ~/.claude/skills/markdown2pdf ``` ### Project-level (Claude Code) ```sh mkdir -p .claude/skills cp -R path/to/markdown2pdf/skills/markdown2pdf .claude/skills/ ``` ### Anthropic API / Claude Agent SDK Upload the skill folder via the [Skills API](https://docs.claude.com/en/api/agent-skills). The folder follows the standard `SKILL.md` + supporting files format. ## How it picks the right binary When the agent triggers the skill and `markdown2pdf` isn't on the user's `PATH`, the embedded snippet in `SKILL.md`: 1. Checks `command -v markdown2pdf` — if found, uses it. 2. Checks `~/.cache/markdown2pdf/markdown2pdf` — if cached, uses it. 3. Otherwise: - Detects platform via `uname -s` / `uname -m` - Maps to a release target (`aarch64-apple-darwin`, `x86_64-unknown-linux-gnu`, `x86_64-pc-windows-msvc`) - Downloads `https://github.com/woodyjon/markdown2pdf/releases/latest/download/markdown2pdf-<target>.tar.gz` - Verifies SHA256 against the release's `SHA256SUMS` file - Extracts the binary into `~/.cache/markdown2pdf/` The `releases/latest/download/<asset>` URL always redirects to the current latest non-prerelease, so the skill never has to query the GitHub API or pin a version. ## Permanent install (optional) If the user expects to use `markdown2pdf` outside the skill — in their own shell scripts, terminal, CI — they can install it system-wide once. **macOS / Linux / WSL:** ```sh curl -fsSL https://markdown2pdf.eu/install.sh | sh ``` Writes to `/usr/local/bin/` (or `~/.local/bin/` if it can't write there without sudo). **Windows (PowerShell):** ```powershell powershell -c "irm https://markdown2pdf.eu/install.ps1 | iex" ``` Writes to `%LOCALAPPDATA%\Programs\markdown2pdf\` and adds it to the user PATH. Both installers run the same logic the skill uses internally, but install to a permanent location instead of the per-user cache. After this, both the shell and the skill find the binary on `PATH` and the cache fallback is never used. ## What it does When the user asks for a PDF — *"convert this README to PDF"*, *"make a PDF report from these notes"*, *"export this to PDF"* — the agent: 1. Resolves or downloads the binary (one-time `~/.cache/markdown2pdf/` setup). 2. Locates or creates the source markdown. 3. Runs `markdown2pdf input.md -o output.pdf`. 4. Reports the output path. The skill knows the CLI flags, file formats, and common patterns (stdin/stdout piping, batch conversion, title metadata). It does not need the network after the first download, doesn't talk to the web playground, and works fully offline. ## Why a skill (and not just a tool call) Skills let the agent pick up the capability without you wiring it into your prompt every time. Once installed, the agent triggers the skill whenever a markdown→PDF request comes up — across projects, across sessions. You don't need to remember the install command, the binary location, or the CLI flags. --- <!-- url: https://markdown2pdf.eu/docs/embedding --> # Embed in Rust The PDF generator is a small Rust crate, [`markdown2pdf-core`](https://github.com/woodyjon/markdown2pdf/tree/main/rust/crates/core). The CLI and the WASM module both wrap it. Use it directly in your own Rust project to skip the binary entirely. ## Add the dependency Until the crate is on crates.io, depend on it via git: ```toml [dependencies] markdown2pdf-core = { git = "https://github.com/woodyjon/markdown2pdf", package = "markdown2pdf-core" } ``` ## Convert markdown to PDF bytes ```rust use markdown2pdf_core::{markdown_to_pdf, Options}; fn main() -> anyhow::Result<()> { let markdown = std::fs::read_to_string("README.md")?; let opts = Options { title: Some("My Document".into()), }; let pdf_bytes: Vec<u8> = markdown_to_pdf(&markdown, &opts) .map_err(|e| anyhow::anyhow!("{e}"))?; std::fs::write("README.pdf", pdf_bytes)?; Ok(()) } ``` That's the entire API. The function returns a `Vec<u8>` containing the rendered PDF — write it to a file, stream it over HTTP, attach it to an email, anything. ## Inspect the intermediate Typst source Useful for debugging conversion issues: ```rust use markdown2pdf_core::markdown_to_typst; let typst_source: String = markdown_to_typst("# Hello\n\nWorld"); println!("{}", typst_source); ``` ## Use Typst directly If you want to skip the markdown step and feed Typst markup directly: ```rust use markdown2pdf_core::{typst_source_to_pdf, Options}; let typst = r#" = My Heading Some *bold* text in Typst. "#; let pdf = typst_source_to_pdf(typst, &Options::default()) .map_err(|e| anyhow::anyhow!("{e}"))?; ``` ## Embedded fonts [Inter](https://rsms.me/inter/) and [JetBrains Mono](https://www.jetbrains.com/lp/mono/) ship inside the crate via `include_bytes!`. The compiled binary is ~20 MB but has zero runtime dependencies — copy it anywhere and it works. ## Build for WebAssembly The same crate compiles to WASM via [`markdown2pdf-wasm`](https://github.com/woodyjon/markdown2pdf/tree/main/rust/crates/wasm). The build script `rust/build-wasm.sh` runs `cargo build --target wasm32-unknown-unknown`, then `wasm-bindgen --target web`, then `wasm-opt -Oz` if available. Output lands in `src/lib/wasm/`. The WASM exports a single function: `markdown_to_pdf(md: string) -> Uint8Array`. ---