How I Built a Free Markdown-to-PDF Converter in the Browser
Last month I shipped MD2PDF Online — a free tool that converts Markdown to PDF, Word, HTML and Mind Map, with zero server-side processing. Everything runs client-side. Here's what I learned building it and why the approach matters. Most online converters upload your file to a server, process it, then send back the result. This works, but: Your document content leaves your device There are file size limits Server costs scale with usage Privacy concerns for sensitive documents I wanted a converter where the browser does all the work. Your Markdown never leaves your computer. ┌──────────────────────────────────────────────┐ I used the unified ecosystem with remark-parse, remark-gfm, and rehype-stringify. This gives me GitHub-Flavored Markdown support including tables, task lists, and strikethrough. import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; async function markdownToHtml(md: string) { return String( await unified() .use(remarkParse) .use(remarkGfm) .use(remarkRehype) .use(rehypeStringify) .process(md) ); } The parser runs synchronously in the browser. No API calls. Instead of sending the HTML to a server and waiting for a PDF back, I use html2pdf.js — a client-side wrapper around html2canvas and jsPDF: import html2pdf from "html2pdf.js"; function exportToPdf(htmlContent: string) { const element = document.createElement("div"); element.innerHTML = htmlContent; document.body.appendChild(element); html2pdf().from(element).save(); } This renders the HTML as a canvas, then converts it to a PDF. The trade-off is that it's a rasterized PDF (not vector), but for most use cases it's perfectly fine and the zero-server approach is worth it. For DOCX, I used the docx library: import { Document, Packer, Paragraph, TextRun } from "docx"; const doc = new Document({ sections: [{ children: paragraphs }], }); const blob = await Packer.toBlob(doc); saveAs(blob, "document.docx"); This was the most interesting part. I used markmap-lib to parse Markdown headings and lists into a hierarchical data structure, then rendered it with markmap-view as an interactive SVG mind map: import { Transformer } from "markmap-lib"; const transformer = new Transformer(); const { root, features } = transformer.transform(markdown); Client-Side Performance Running everything in the browser means the main thread can get busy. I solved this by: Lazy loading heavy libraries (html2pdf, docx, markmap) only when the user clicks export Using useMemo and React.lazy in Next.js to avoid re-parsing Debouncing the editor input at 100ms Internationalization The tool supports 6 languages (English, Chinese, Japanese, French, German, Spanish). I used next-intl with the new App Router. Each language gets its own URL (/en, /zh, /ja etc.) with proper hreflang tags for SEO. The PDF-to-Markdown Pipeline Converting PDF back to Markdown is harder because the browser can't run OCR. I used @opendocsg/pdf2md which works well for PDFs with selectable text. For scanned PDFs, I recommend local tools like Tesseract. Why This Matters The "process everything in the browser" approach has real benefits: Server-side Client-side File uploads to remote servers File stays on your device Server costs scale with traffic Free to run (static hosting) Privacy concerns Zero data collection Rate limits needed No limits For a simple converter, there's no reason to send files to a server. Modern browsers are powerful enough to handle the entire pipeline locally. Want to Try It? You can use it for free at https://md2dfonline.com. No signup, no tracking, no file uploads. The code is a standard Next.js app — I'm happy to answer any questions about the architecture or tradeoffs in the comments. What's your favorite browser-based productivity tool?
