Export Markdown to PDF: 12kb Browser App Auto-Generates TOC
The Pandoc Trap: Why You're Shipping 3GB of Dependencies for a 200KB Document
Most engineering teams I've audited this year share the same blind spot. They need to export markdown to PDF with an automatic table of contents, so they reach for Pandoc plus a LaTeX distribution. On macOS, that's roughly 3.2GB of installed tooling. On a CI runner, you're pulling a Docker image that clocks in at 1.8GB just to render a 47-page API reference. I've watched build pipelines for documentation sites take 6 minutes to produce a single PDF — 4 of those minutes were spent installing texlive packages the document never actually uses.
The misconception is that automatic table-of-contents generation in PDF output requires a heavyweight typesetting engine. It doesn't. If you're building or using a lightweight markdown editor in the browser, you can generate a structured PDF with a clickable, page-numbered TOC using roughly 140KB of JavaScript — no server round-trip, no native dependencies, no Docker image larger than your actual documentation.
Let me walk through the exact approach I shipped for an internal tooling team last month. Their use case is specific but common: they maintain a markdown-based API reference inside a browser-based editor, and they need stakeholder-ready PDFs with a navigable table of contents generated on demand. Stakeholders click a TOC entry and jump to the right page. No manual numbering. No copy-paste into Word.
Why the Common Approaches Collapse Under Real Conditions
The Print-to-PDF Shortcut
The most frequent workaround I see in browser-based markdown editors is calling window.print() after rendering markdown to HTML. It works. Sort of. But the TOC — if it exists at all — is just a flat list of links with no page numbers. When a reviewer opens the PDF and sees "Authentication" in the table of contents, there's no "page 12" next to it. For a 47-page document, that's a non-starter. You've essentially shipped a web page disguised as a PDF.
The Server-Side Puppeteer Route
The second common pattern: send the markdown to a Node backend, spin up Puppeteer, render the HTML, and use Chromium's print engine to produce the PDF. This gets you closer — Puppeteer can inject header/footer templates and generate page numbers. But now you're maintaining a server, managing headless Chromium instances (roughly 350MB each), and adding latency. For a lightweight browser app, that architecture defeats the purpose. You've turned a client-side tool into a distributed system.
What's Actually Missing
The gap in both approaches is the same: nobody is computing the page position of each heading before the PDF is finalized. The browser's print engine paginates content internally, but it doesn't expose that information back to your JavaScript in a usable way. So your TOC can list headings, but it can't annotate them with page numbers because it doesn't know them yet.
The Lightweight Approach: Two-Pass Rendering in the Browser
Here's the method I implemented. It runs entirely client-side, works inside any browser-based markdown editor, and produces a PDF with a fully numbered, clickable table of contents. The trick is a two-pass rendering cycle.
Pass 1: Render and Measure
First, render your markdown to HTML and place it inside a hidden container that matches your target PDF page dimensions. For US Letter at 96 DPI (the browser's default), that's 816×1056 pixels with appropriate margins — I use 72px margins on all sides, giving a content area of 672×912 pixels.
At this stage, you're not generating the PDF. You're measuring where each heading lands. Walk the DOM, find every h1 through h3 (or whatever depth your TOC should cover), and record each heading's offsetTop relative to the container. Then calculate the page number:
pageNumber = Math.floor(headingOffset / pageContentHeight) + 1
For our 47-page API reference, this measurement pass took 340 milliseconds on a mid-range laptop. That's fast enough to feel instant to the user clicking "Export PDF."
Pass 2: Inject the TOC and Render to PDF
Now you have an array of headings paired with page numbers. Generate your TOC HTML — a structured list with the heading text, dot leaders (those dotted lines between title and page number), and the page number. Inject this TOC at the top of your document container, before the first heading.
Here's the critical detail most implementations miss: injecting the TOC shifts every heading's position downward. Your page numbers from Pass 1 are now wrong. So you run the measurement again — Pass 2's measurement. This time, you're measuring positions with the TOC already in place. Update the page numbers in the TOC, and if any heading moved to a different page, re-measure one more time.
In practice, the TOC for our 47-page document occupied 3 pages. After injecting it, 4 headings shifted to the next page. The re-measurement caught all of them. Total iterations: 3. Total time across all passes: 1.1 seconds.
The Actual Implementation Stack
Markdown Parsing
Use marked or markdown-it. Both are under 50KB minified. I prefer markdown-it for this use case because its plugin architecture lets you add heading anchors and custom heading IDs cleanly — which you need for clickable TOC entries.
PDF Generation
Use jsPDF with the html2canvas plugin, or — my preference for text-heavy documents — pdfmake. pdfmake is 147KB minified and gives you precise control over page breaks, internal links, and TOC formatting. It operates on a JSON document definition rather than rendering HTML directly, which sounds like more work but actually solves the pagination problem: you define your content as an array, insert page breaks explicitly, and the library tracks page numbers for you.
Connecting the Pieces
The workflow becomes: parse markdown to HTML with markdown-it, walk the resulting DOM to build a content array for pdfmake, insert a TOC entry at the top with placeholder page numbers, render once to measure actual page positions, update the TOC with real page numbers, and render the final PDF. The user clicks "Export," sees a brief spinner, and downloads a finished PDF — all within the browser.
Real Numbers From the Field
Let me ground this in the actual project. The document in question was an internal API reference for a payments platform. Here's what the team was working with:
- Source markdown: 47 pages when rendered, 2.3MB of content - Heading count: 89 headings across h1, h2, and h3 - Previous approach (Pandoc + LaTeX on CI): 6 minutes 12 seconds per build, 1.8GB Docker image - New approach (browser-based, two-pass): 1.1 seconds to generate, 147KB library footprint
The PDF output was 1.4MB — slightly smaller than the Pandoc output (1.6MB) because pdfmake embeds fonts more efficiently than the default LaTeX font configuration. The TOC spanned 3 pages, every entry was clickable, and page numbers were accurate to the final rendered position.
Handling Edge Cases That Will Bite You
Images and Page Breaks
Images are the biggest threat to your page-number accuracy. A large image near the bottom of a page can push a heading to the next page, invalidating your Pass 1 measurements. The two-pass approach handles this naturally — the re-measurement catches the shift. But if you have images that load asynchronously, you must wait for all images to finish loading before running Pass 1. Use Promise.all on image load events. Skip this and your TOC will be wrong on the first export after a cache clear.
Code Blocks and Syntax Highlighting
Long code blocks that span page boundaries create the same problem. Worse, syntax highlighters like Prism or highlight.js modify the DOM after initial render, changing element heights. Run your highlighter before Pass 1, not after. The measurement must reflect the final visual state.
Heading Hierarchies
Decide your TOC depth upfront. For API documentation, h1 through h3 is usually right. Going deeper than h3 produces a TOC that's longer than some sections of the document itself — I've seen TOCs that consumed 7 pages of a 30-page document. If you need deeper navigation, consider a collapsible sidebar in the editor view and limit the PDF TOC to h1–h2.
When This Approach Is Wrong
I'll be direct: if you need mathematical equation rendering, complex tables spanning multiple pages with repeating headers, or precise typographic control over kerning and ligatures, use LaTeX. The browser-based approach trades typographic perfection for simplicity, speed, and zero-install deployment. For the 90% of documentation that is prose, code blocks, lists, and standard tables, that trade is worth making.
The team I worked with hasn't touched their CI documentation pipeline in six weeks. The export button in their markdown editor does the job in 1.1 seconds, the TOC is always accurate, and the total added weight to their browser app is under 200KB. That's the ratio worth aiming for: milliseconds of render time, kilobytes of dependencies, and a PDF that looks like it came from a purpose-built tool.
Frequently Asked Questions
How do I export Markdown to PDF with a table of contents?
You can easily export your Markdown files to PDF with an automatic table of contents by using a browser-based Markdown editor that supports TOC generation. Simply write your document using standard heading tags, and the app will automatically compile them into a clickable TOC when you initiate the PDF export.
Can I convert Markdown to PDF directly in my browser without installing software?
Yes, lightweight browser apps allow you to convert Markdown to PDF entirely online without downloading heavy desktop software. These web-based editors process your files locally or via secure cloud conversion, ensuring quick and private PDF generation.
How do I automatically generate a table of contents in Markdown?
To automatically generate a table of contents, you need to structure your document using proper Markdown heading syntax like #, ##, and ###. A lightweight Markdown editor will scan these headings and automatically build a formatted, hyperlinked TOC for your final PDF document.
What is the best lightweight Markdown editor for PDF export?
The best lightweight Markdown editors for PDF export are browser-based applications that offer fast loading times and built-in conversion tools. Look for web apps that specifically advertise automatic TOC generation and CSS styling options to ensure your PDF looks professional.
How to add page numbers to a Markdown PDF export?
While standard Markdown does not natively support page numbers, many advanced browser-based Markdown editors include this feature in their PDF export settings. You can typically toggle page numbers, margins, and custom headers within the app's print or export dialog before generating the final document.
Is there a free online Markdown to PDF converter with automatic TOC?
Yes, several free browser apps offer Markdown to PDF conversion with automatic table of contents generation. These lightweight tools let you paste your Markdown text or upload a file, instantly providing a downloadable, formatted PDF complete with a linked TOC.
How do I make the table of contents clickable in a Markdown PDF?
A clickable table of contents is usually generated automatically when you use a browser Markdown editor that supports anchored headings. When the app compiles your Markdown into a PDF, it maps the TOC entries directly to the corresponding heading sections, creating fully navigable internal links.
Can I export Markdown to PDF offline using a browser app?
Many modern lightweight browser apps use client-side processing, allowing you to export Markdown to PDF completely offline. Once the web page is loaded, the JavaScript engine handles the Markdown parsing and PDF generation locally without needing an active internet connection.
How do I style a table of contents for a Markdown PDF?
You can style your automatic table of contents by using a browser Markdown editor that supports custom CSS or built-in export themes. These tools allow you to adjust font sizes, indentation, and numbering formats for your TOC before finalizing the PDF document.
Why is my Markdown table of contents missing in the exported PDF?
If your TOC is missing, it usually means the headings in your Markdown file lack the proper # syntax or the browser app's PDF export settings have TOC generation disabled. Double-check your heading formatting and ensure the automatic TOC option is toggled on in the export menu.