Skip to content

Markdown to PDF

Markdown to PDF is a full-stack web application that turns Markdown into polished, print-ready PDF and PNG documents. Paste your text or drag-and-drop a .md file, pick your output format, and download — all from the browser. The server-side conversion pipeline uses Puppeteer to render styled HTML into pixel-perfect output, complete with syntax-highlighted code blocks and smart page-break handling so images and tables never get sliced across pages.

Try it directly in the browser — no install required: jadujoel.github.io/markdown-to-pdf

  • Dual Output Formats — Convert to PDF (A4, print-ready) or full-page PNG in a single click. The download button bundles both formats at once using parallel conversion.
  • Paste or Upload — Switch between a live Markdown editor with monospace font and syntax placeholder, or drag-and-drop / browse for .md files. File type validation keeps things clean.
  • Syntax-Highlighted Code Blocks — Code fences are rendered with highlight.js using a One Dark–inspired theme, supporting auto-detection across dozens of languages.
  • Smart Page Break Control — A configurable setting prevents images, tables, code blocks, and blockquotes from splitting across PDF pages using CSS break-inside: avoid and page-break-inside: avoid rules.
  • Persistent Settings — User preferences (like the image-split toggle) are saved in localStorage and restored on return visits.
  • Live Preview — Open a rendered preview in a new browser tab before committing to a download.
  • Beautiful Typography — The PDF output uses Inter and Fira Code fonts, styled blockquotes, rounded code blocks, and a carefully tuned color palette for a professional look.
  • Hot Module Reloading — Development uses Bun’s HMR with a custom import.meta.hot.data pattern in frontend.tsx to preserve React root across reloads.

The frontend React app in App.tsx sends Markdown to a /api/convert endpoint via FormData. The server in index.ts accepts both JSON and multipart payloads with a 5 MB size limit. The actual conversion happens in convert.ts:

// convert.ts — core pipeline
const htmlBody = await marked.parse(markdown);
const fullHtml = htmlTemplate(htmlBody, resolvedOptions);
const browser = await getBrowser();
const page = await browser.newPage();
await page.setContent(fullHtml, { waitUntil: "networkidle0" });
if (format === "pdf") {
const pdf = await page.pdf({
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
});
return Buffer.from(pdf);
}

A single Puppeteer browser instance is kept alive and reused across requests for fast throughput — each conversion only opens and closes a page, not an entire browser process.

For PNG output, the renderer dynamically measures the body height and sets the viewport to match, producing a seamless full-length screenshot:

const bodyHandle = await page.$("body");
const { height } = await bodyHandle!.boundingBox() as { height: number };
await page.setViewport({ width: 900, height: Math.ceil(height) + 40 });
const png = await page.screenshot({ type: "png", fullPage: true });
LanguageTypeScript
RuntimeBun
FrontendReact 19
Markdownmarked + marked-highlight
Syntax Highlightinghighlight.js
PDF / PNG RenderingPuppeteer (headless Chrome)
StylingCustom CSS with Inter & Fira Code fonts
DeploymentGitHub Pages via GitHub Actions

The source code is available on the project’s GitHub repository.