CLI tool
elliotblackburn/mdpdf avatar
elliotblackburn/mdpdf

mdpdf: Markdown to PDF with real page headers and footers

Markdown to PDF command line app with support for stylesheets

804 stars57 forksTypeScriptApache-2.0

At a glance

What is it?
A Node command line tool that renders Markdown through headless Chrome, letting you supply a stylesheet, a Handlebars header and footer, page borders and format, with the caveats that it inherits Puppeteer's limits and that its author labels it a best effort project.
Who is it for?
Adopt mdpdf if you need a Markdown to PDF pipeline with a repeated page header, a footer with page numbers and a custom stylesheet, and you already have Node in your build. Do not adopt it if you need a table of contents with page references, a table of figures, or a guarantee of identical output across machines, since the rendering depends on the Chromium version Puppeteer pulls in.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 84 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap is headers and footers, not Markdown rendering

mdpdf is described as a command line Markdown to PDF converter with support for page headers, footers and custom stylesheets, and the project notes that it is highly configurable and has a JavaScript API for more elaborate use.

Markdown to PDF conversion is not the hard part, and there are many ways to do it. What mdpdf goes after is the page furniture. A page number in a footer, a document title in a header, a company logo, a version stamp: those are the elements that turn a document into something you would send to a customer, and they are exactly the elements that a naive HTML-to-PDF pipeline renders badly.

The mechanism underneath is Puppeteer, the headless Chrome project from Google. That choice is the whole design. Headers and footers in a PDF are not part of the HTML document you render; they are properties of the page box, which is why the renderer has to support them and why styling them is awkward.

The project is explicit that Puppeteer provides a number of header and footer HTML classes which can be used to insert things such as page numbers. The header and footer are supplied as HTML files, and the header is described as a Handlebars file, which means you can template dynamic content into it rather than hardcoding it.

The examples directory holds separate folders for headers, footers and API usage, which is the fastest way to see what a working header looks like.

Installing for the command line or for the API

The installation section splits the two use cases, which is the right split because they pull the package in differently.

For command line use, install it globally:

bash
npm install mdpdf -g

For programmatic use, install it locally so the API is available to your project:

bash
npm install mdpdf --save

The package is published as `mdpdf` on npm, and the binary it installs is `mdpdf`, mapping to `dist/bin/index.js` in package.json. The project is written in TypeScript with the package marked as an ES module, the main entry point being `./dist/src/index.js`, and the build script compiles with tsc and then copies the assets and layouts directories into the distribution, which is why `src/assets` and `src/layouts` are listed in the published files array.

The two example invocations from the documentation show the range from nothing to everything:

bash
mdpdf README.md

which converts with the GitHub Markdown CSS and some additional basic styling, and:

bash
mdpdf README.md --style styles.css --header header.hbs --h-height 22

which applies a custom stylesheet and a header of height 22mm.

Note the header file extension there: `.hbs`. The header is a Handlebars template, so a page number can be rendered into it rather than pasted in.

Borders, format and the length parameters that need units

The option list is long enough that the defaults are the interesting part.

Page geometry defaults to A4 in portrait orientation. The border defaults to 20mm on all four sides, and each side can be overridden individually with `--border-top`, `--border-left`, `--border-bottom` and `--border-right`. The available formats are A3, A4, A5, Legal, Letter and Tabloid, and orientation defaults to portrait. The PDF title defaults to the source filename without its extension, and is described as being primarily used by Chrome PDF Viewer.

There is one rule that catches people out: length parameters, meaning height and size, require a unit. The valid units are `mm`, `cm`, `in` and `px`.

Two options are worth calling out for what they change. `--gh-style` enables the default gh-styles when `--style` is also used, so you get the GitHub Markdown CSS as a base with your own stylesheet applied on top. And `--no-emoji` disables emoji conversion entirely, which matters because the converter uses showdown-emoji and some emoji render as images.

`--debug` is the one you will want when something looks wrong. It saves the generated HTML so you can open it in a browser and see exactly what the renderer was given before Chrome turned it into a PDF.

The PhantomJS break, and what it still costs you

The most useful thing in the documentation is the paragraph about what happened in the 2.x rewrite, because it tells you about a real regression.

Before 2.x, mdpdf used PhantomJS, which had considerably better support for headers and footers and better styling support for them. PhantomJS is no longer supported and had a number of rendering bugs, so it could not remain a core component of mdpdf. The current position is that headers and footers do work in 2.x and later releases, but their styles must be handled independently of the main markdown file via the styles options, and a few CSS tags do not work correctly. Past that, there should not be any issues with 2.x headers and footers.

The practical consequence is that if you relied on good header and footer support, the documentation suggests sticking with 1.x releases until Puppeteer prioritises it. That is a real trade-off: an old major version with a deprecated browser engine, or the current one with slightly broken CSS in the page furniture.

The environment variable section adds one more piece. `MDPDF_STYLES` gives the full path to a stylesheet to use when `--style` is not specified on the command line, which is the way to set a house style once for a machine rather than on every invocation.

Emoji handling has a similar sharp edge. In text, emoji are supported, but a few shorthand forms do not work and need the longhand version, with `:+1:` failing where `:thumbsup:` succeeds.

The API is one function with an options object

The programmatic surface is deliberately small. There is a single function, `convert()`, which takes some options and returns a promise, and the documentation points at `bin/index.js` for a full example.

The JavaScript form looks like this:

js
const mdpdf = require('mdpdf');
const path = require('path');

let options = {
    source: path.join(__dirname, 'README.md'),
    destination: path.join(__dirname, 'output.pdf'),
    styles: path.join(__dirname, 'md-styles.css'),
    pdf: {
        format: 'A4',
        orientation: 'portrait'
    }
};

mdpdf.convert(options).then((pdfPath) => {
    console.log('PDF Path:', pdfPath);
}).catch((err) => {
    console.error(err);
});

The TypeScript version imports `convert` as a named export and uses the same option shape.

The options are worth reading carefully because several defaults differ from what you might expect. `source` and `destination` are required, as is the `pdf` object. Inside `pdf`, `format` is required, and `orientation` is documented with the values spelled as `potrait` and `orientation`, which reads like a typo in the source and is the kind of detail that will cost you an afternoon if you take the documentation literally.

The two boolean defaults are surprising. `ghStyle` defaults to false, so the GitHub Markdown CSS is not applied unless you ask for it. `defaultStyle` also defaults to false, and it is the one that provides the basic border and font size, which means that by default you get neither the GitHub look nor the basic layout, and the styling is entirely down to the stylesheet you pass.

`styles` is applied last, which is the behaviour you want. `header` takes a path to a Handlebars file and requires the header height to be set as well. And `debug` takes an optional path where the intermediate HTML is saved.

Where mdpdf is the wrong tool

Three limits are concrete.

The first is that everything depends on a browser. Puppeteer downloads a Chromium build and drives it, which means a large install, a real memory footprint and a rendering result that depends on the Chromium version. In CI that usually means a cached Puppeteer download or a Docker image with a system Chromium. The Dockerfile in the repository shows the second approach: it starts from node:22-alpine, installs chromium, nss, freetype, harfbuzz, ca-certificates, ttf-freefont and font-noto-cjk, sets `PUPPETEER_EXECUTABLE_PATH` to the system browser, runs `npm ci && npm run build`, and uses the compiled binary as the entrypoint. The CJK font is there because the container otherwise renders Chinese text as boxes.

The second is the scope of what a Markdown to PDF converter can express. There is no table of contents with page numbers, no table of figures, no index, no footnote handling beyond what the Markdown and the template engine provide. If your document needs cross references that resolve to page numbers, this is the wrong tool and no amount of CSS will fix it.

The third is the maintenance posture, which the author states plainly. The project started as a simple tool to solve one personal need, it is open to contributions, but it is maintained on a best effort basis when the author has the time and desire, and the note says explicitly that this is not a priority project and that responses and actions may be slow. That is honest, and it is a factor in whether you depend on it.

The alternative to name is a dedicated document toolchain such as a LaTeX based pipeline or a full publishing system with a template engine and a reference-able build. The difference in approach is that those give you page-numbered cross references, a table of contents and reproducible output as part of the format, while mdpdf gives you a small scriptable wrapper around headless Chrome that is far easier to slot into an existing Node build. If you only need headers, footers and a stylesheet, that is the cheaper answer; if you need a book, it is not.

Editorial conclusion

Adopt mdpdf if you need a Markdown to PDF pipeline with a repeated page header, a footer with page numbers and a custom stylesheet, and you already have Node in your build. Do not adopt it if you need a table of contents with page references, a table of figures, or a guarantee of identical output across machines, since the rendering depends on the Chromium version Puppeteer pulls in. Verify first that your header and footer styles work in the current major version, because the project documents that a few CSS tags did not render correctly after the move off PhantomJS, and check whether the npm version you install matches the tag you tested with.

Frequently asked questions

How do I install mdpdf for command line use?

Run `npm install mdpdf -g` for a global install so the mdpdf command is available, or `npm install mdpdf --save` to add it locally when you want the JavaScript API. The package is published on npm as mdpdf.

How do I add a header with a page number to a mdpdf PDF?

Pass a Handlebars file with `--header=header.hbs` and set the height with `--h-height`, for example `mdpdf README.md --style styles.css --header header.hbs --h-height 22`. The header is rendered by Puppeteer's page header HTML, which is where page number classes come from.

What are the mdpdf style and length options?

`--style` applies a single stylesheet that is applied last, `--gh-style` enables the default GitHub Markdown styles alongside it, and `--no-emoji` disables emoji conversion. Length parameters such as `--h-height` and the border sizes require a unit, with mm, cm, in and px being valid.

What does the mdpdf convert() API return?

A single convert() function that takes an options object and returns a promise. The options require source, destination and a pdf object containing at least a format, with optional styles, ghStyle, defaultStyle, header and debug path; ghStyle and defaultStyle both default to false.

Does mdpdf work well with headers and footers?

Headers and footers do work in the 2.x and later releases, but their styles must be handled independently of the main markdown file through the styles options, and a few CSS tags do not work correctly. The documentation notes that the earlier 1.x releases had better header and footer support because they were built on PhantomJS.

Official sources

  1. elliotblackburn/mdpdf on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elliotblackburn-mdpdf.svg)](https://hysenlabs.com/projects/elliotblackburn-mdpdf)

Was this useful?