PDF Output
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against DocFX 2.81.0 (the latest release, published on 2026-09-25) and checked on 2026-10-26. The
behaviour described comes from the official PDF page, the 2.81.0 source (the PDF builder, the Playwright helper and the
modern template) and runs of docfx pdf in the mcr.microsoft.com/dotnet/sdk:10.0 Docker image against the Pocket Units
C# library, so the listings and page counts are real output. The pipeline that produces the site is described in
Basic Concepts and Pipeline and the command itself in
CLI.
What PDF output is
DocFX does not convert Markdown or YAML into PDF directly. The pdf stage opens the pages of an already built HTML site in a headless Chromium browser, prints each page to PDF, and merges the results into one file per table of contents (TOC). Because the pages are the same HTML the website shows, a PDF looks like the site, styled for print. The official page says the feature applies to DocFX 2.73.0 or later and to the modern template; the older default template is not covered. A PDF is useful for offline reading, for handing a manual to someone without access to the site, and as an archive of a release.
The stage has three moving parts:
-
Metadata keys (
pdf,pdfFileName,pdfTocPage,pdfCoverPageand others) that switch the feature on and shape the result. They are ordinary metadata, set globally, per file or inside atoc.yml. -
The pdf command.
docfx pdf(or the baredocfxcommand, which runs metadata, build and pdf in turn) readsmanifest.jsonfrom the output folder, so adocfx buildmust have run first. -
Node.js and a Playwright browser. DocFX drives Chromium through Playwright for .NET, whose driver is a Node.js program.
and toc.json files"] SITE --> PDF["docfx pdf"] NODE["Node.js 20 or newer
on PATH"] --> PDF PDF --> BR["headless Chromium
through Playwright"] BR --> PAGES["one PDF per page:
cover, TOC, articles"] PAGES --> MERGE["merge, add headers,
footers and bookmarks"] MERGE --> OUT["toc.pdf next to each
toc.html"]
The path from a built site to the merged PDF files. Each TOC with pdf: true yields one file.
Enabling PDF
PDF is switched on with the pdf metadata key, which the official page describes as both enabling the generation and
showing a "Download PDF" button on the site. There are two places to put it.
For the whole site, set it in globalMetadata of docfx.json:
{
"build": {
"globalMetadata": {
"pdf": true
}
}
}
Source: DocFX docs: Create PDF Files
For one TOC, put it at the top of its toc.yml:
pdf: true
items:
- name: Getting Started
href: getting-started.md
Source: DocFX docs: Create PDF Files
The sequence is then always the same: docfx build produces the site, and docfx pdf creates a toc.pdf file for
every TOC in the output folder that has pdf enabled. The docfx init scaffold already writes the pdf key, set to true, into
globalMetadata when the "Enable PDF?" question is answered yes (the default, and always with docfx init -y), so a
freshly initialised project is set up for PDF but cannot produce one until Node.js is installed.
Precedence
For TOC files the metadata is resolved as fileMetadata, then the values written in the toc.yml, then
globalMetadata, with the first match winning. Three consequences follow from the official description:
-
Values are chosen per key and never merged deeply, so a TOC that omits a key inherits the global value of that key (unless a
fileMetadatapattern matches), while an explicit value, includingfalseornull, counts as set. -
pdf: falsein atoc.ymlremoves that TOC from a site where PDF is enabled globally, unless a matchingfileMetadataentry turns it back on. AfileMetadataentry can likewise disable a TOC that sayspdf: true. -
Generated TOCs, such as the one produced for the API documentation, have no file to edit, so they are configured with
fileMetadata.
{
"build": {
"fileMetadata": {
"pdf": {
"api/**/toc.yml": true
}
}
}
}
Source: DocFX docs: Create PDF Files
The docfx.json reference sketches a top-level pdf section next to build and metadata, but the 2.81.0 source reads
only the build section for the pdf stage, and nothing described on the official PDF page lives under pdf. Use the
metadata keys described here.
One PDF per table of contents
A PDF contains the pages of one TOC: the TOC items and everything nested below them, in TOC order, with href pages
that are on the same host as the site. The result is written next to the TOC’s rendered file, so the TOC at the site root
produces _site/toc.pdf and the TOC of the API folder produces _site/api/toc.pdf. With pdf: true set globally on the
Pocket Units project, one run produced both files (the root one with 4 pages, the API one with 6 pages), which is often
more than a reader wants.
The usual ways to control which PDFs exist:
-
Use
fileMetadataor apdf: falseline to leave the unwanted TOCs out. -
Set
pdfFileNameto name the file; the default istoc.pdf. The name is only used for the file produced from that TOC. -
Build a single manual with a dedicated PDF TOC that includes the other TOCs as nested TOCs, and give it a large
orderso that it does not appear in the website navigation. The official page shows this form:
order: 200
items:
- name: Section 1
href: section-1/toc.yml
- name: Section 2
href: section-2/toc.yml
Source: DocFX docs: Create PDF Files
The dedicated PDF TOC pattern was not executed here; the DocFX repository’s own seed sample uses it (a TOC with order:
200 that references the articles, the API TOC and the REST API TOC), with pdf and pdfTocPage set globally.
The PDF carries bookmarks built from the TOC: in the run above the outline had the entries Home and API, and in the API PDF the namespace with its two types underneath. Links between pages that are in the same PDF are rewritten to jump to the right page inside the file; a link to a page of the same site that is not in the PDF is replaced by an empty link, and links to other sites are kept.
The metadata keys
All keys below are read from the TOC’s metadata and apply to that TOC’s PDF. They are the complete list in the 2.81.0 source.
| Key | Type and default | Meaning |
|---|---|---|
|
boolean, off |
Generates the PDF for the TOC and shows the "Download PDF" link on the site. The modern template renders the link in the
sidebar from the TOC’s |
|
string, |
The name of the output file, placed in the folder of the TOC. |
|
boolean, off |
Inserts a "Table of Contents" page at the beginning of the PDF, listing the TOC items with their page numbers. |
|
string, none |
Path of an HTML page, relative to the output folder, inserted at the beginning of the PDF as the cover page. The page must be part of the build. |
|
boolean, off |
Includes background colours and images when printing. |
|
string, none |
HTML for the print header, or the path of an HTML file relative to the output folder. |
|
string, built-in footer |
HTML for the print footer, or a path to an HTML file. If omitted, a footer with the page number and the total at the right is used. |
|
boolean, off |
Prints the header and footer on the cover page too. |
|
boolean, off |
Prints the header and footer on the generated table of contents pages. |
Source: DocFX docs: Create PDF Files
The last two keys are independent of the cover and TOC switches: they add nothing by themselves and only decide whether
headers and footers appear on pages that pdfCoverPage and pdfTocPage have already added. The cover and TOC pages are
counted in pageNumber and totalPages whether or not they show a header or footer.
Cover page
The cover is an ordinary page of the site, so the simplest way to make one is a Markdown file that is part of the build
and contains inline HTML. Its output path becomes the value of the key: the official page points out that a file
cover.md written to _site/cover.html is referenced as pdfCoverPage: cover.html. If the cover page does not exist in
the output (a 404 when the browser requests it), the PDF builder skips it without an error, so a wrong path gives a PDF
without a cover rather than a failure.
The modern template adds a print margin of 0.4 inch and does not print background graphics. For a cover that fills the
page, remove the margin with @page { margin: 0 } and keep the background with print-color-adjust: exact:
<style>
@media print {
@page { margin: 0 !important; }
body { print-color-adjust: exact; }
}
</style>
<div style="background-color: #1f4e79; width: 100%; height: 100%; display: flex; flex-direction: column">
<p style="flex: 1"></p>
<h1 style="align-self: end; margin: 1rem 2rem; color: white">Pocket Units</h1>
</div>
This is a variation of the DocFX seed sample with the Pocket Units title and a different colour. With
pdfCoverPage: cover.html it became the first page of both PDFs of the run below.
Table of contents page
With pdfTocPage: true DocFX generates a page with the heading "Table of Contents" and a nested list of the TOC items,
each with a dotted spacer and the number of the page where that item starts. The page is served by DocFX’s own local web
server, not read from the site. It is generated after the other pages are printed so that the numbers are correct, and
it is placed after the cover page when both exist. In the Pocket Units run the page read Home 3 and API 4, because the
cover is page 1 and the TOC page 2.
It is styled with the same site CSS, and four selectors are available to override the look from print styles:
| Selector | What it styles |
|---|---|
|
The "Table of Contents" heading. |
|
The list. |
|
A list item. |
|
The page number. |
|
The dotted line between the title and the number. |
Source: DocFX docs: Create PDF Files
Headers and footers
The header and footer templates are the Chromium print templates of Playwright: HTML strings in which two placeholder elements are replaced by the values of the page. DocFX substitutes them itself before printing, so that they are correct across the merged document and not only within one source page:
-
<span class='pageNumber'></span>is replaced by the current page number. -
<span class='totalPages'></span>is replaced by the total number of pages of the PDF.
Both quote styles for the class attribute are replaced. The official page warns that text in the header or footer only
appears if its font-size is set explicitly in the template’s CSS, which is why the built-in footer has
font-size: 12px. The default footer, used when pdfFooterTemplate is not set, is:
<div style="width: 100%; font-size: 12px;">
<div style="float: right; padding: 0 2em">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
</div>
Source: DocFX docs: Create PDF Files
A template can be given in two ways, and the difference matters. If the value contains characters that cannot be part of
a file path (such as the angle brackets and quotes of HTML), it is used as the template itself. Otherwise DocFX treats it as a path relative to the
output folder and reads that file; if no such file exists, the text of the value is printed as the template. So a header
value of header.html prints the words header.html on every page unless header.html really is in _site. In the test run, files named in
build.content with an .html extension were not copied to _site, so list them under build.resource. In the test run this pitfall showed up
exactly as described: with the files missing from _site, every page of the PDF began with the text "header.html" and
"footer.html".
Here is a complete configuration that combines every key of the table above. The two template files and the cover are
listed so that they reach _site; the API TOC is excluded with fileMetadata, so that only one PDF is produced:
{
"metadata": [{ "src": [{ "src": "src", "files": ["**/*.csproj"] }], "dest": "api" }],
"build": {
"content": [{ "files": ["api/**.yml", "api/index.md", "toc.yml", "*.md"] }],
"resource": [{ "files": ["header.html", "footer.html"] }],
"output": "_site",
"template": ["default", "modern"],
"globalMetadata": { "_appTitle": "Pocket Units" },
"fileMetadata": { "pdf": { "api/**/toc.yml": false } }
}
}
pdf: true
pdfFileName: pocket-units.pdf
pdfTocPage: true
pdfCoverPage: cover.html
pdfPrintBackground: true
pdfHeaderFooterOnCover: true
pdfHeaderFooterOnToc: true
pdfHeaderTemplate: header.html
pdfFooterTemplate: footer.html
items:
- name: Home
href: index.md
- name: API
href: api/
<div style="width: 100%; font-size: 10px; padding: 0 2em;">Pocket Units documentation</div>
<div style="width: 100%; font-size: 10px; text-align: center;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>
Source: DocFX docs: Create PDF Files
Running docfx build and then docfx pdf on this project produced _site/pocket-units.pdf with four pages. The text of
each page, read back with a PDF library, was:
page 1: Pocket Units documentation | Page 1 of 4 | Pocket Units (cover, header and footer on)
page 2: Pocket Units documentation | Page 2 of 4 | Table of Contents | Home 3 | API 4
page 3: Pocket Units documentation | Page 3 of 4 | Pocket Units | See API.
page 4: Pocket Units documentation | Page 4 of 4 | Classes | Converter | ...
bookmarks: Home, API
With the defaults of the first run (cover and TOC page on, no pdfHeaderFooterOnCover or pdfHeaderFooterOnToc), the
cover and TOC pages carried no header or footer, the content pages carried them, and the numbering still counted the
cover and TOC (page 3 of 4 for the home page).
Page size and print styles
The PDF is printed with the page size requested by the page’s own CSS (the browser option that prefers the CSS page size
is used), and backgrounds only when pdfPrintBackground is true. Page size and orientation can therefore differ per
page, and headers and footers follow the size and orientation of the page they sit on, so a landscape cover can sit in
front of portrait articles. In the test runs, with no @page size anywhere, the pages came out as 612 by 792 points,
which is US Letter.
While printing, DocFX opens the page with the user agent docfx/pdf, and the modern template detects it and skips the
navigation bar, theme switcher, search and "in this article" sidebar, running only the Markdown rendering and code
highlighting. To style the PDF without touching the site, put the rules in a print media query in the site’s custom CSS:
@media print {
/* All your print styles go here */
}
Source: DocFX docs: Create PDF Files
To preview the result without generating a PDF, print the page from a browser, or set the rendering emulation of the browser developer tools to the print media type. The custom CSS mechanism is explained in Templates and customization.
Requirements: Node.js and Playwright
The pdf stage needs, in addition to the .NET SDK or runtime that runs DocFX:
-
Node.js 20 or newer. The quick start of the official documentation lists Node.js v20 or higher as an optional prerequisite that is required for creating PDF files.
-
A Chromium build that Playwright can start. DocFX downloads it by itself: before rendering it runs the Playwright installer for
chromiumwith the--only-shelloption (the headless shell), so no separate installation command is needed, only network access to the Playwright download servers on the first run. The browser is cached in the user’s profile (in the test image, under the home directory in.cache/ms-playwright), so later runs do not download it again. -
The operating system libraries Chromium links against on Linux. DocFX does not install them.
Node.js is located like this: if the environment variable PLAYWRIGHT_DRIVER_SEARCH_PATH or PLAYWRIGHT_NODEJS_PATH is
set, DocFX uses it as is. Otherwise it searches the folders of PATH for node (node.exe on Windows), runs
node --version and logs the result, then sets PLAYWRIGHT_NODEJS_PATH for its own process. Without any Node.js it stops
with this message, seen in the test image:
error: FatalError: Docfx.Exceptions.DocfxException: Node.js executable is not found. Try to install Node.js or set the `PLAYWRIGHT_NODEJS_PATH` environment variable.
Two environment variables belong to the pdf stage:
| Variable | Effect |
|---|---|
|
The path of the Node.js executable that |
|
Maximum time in milliseconds that replaces the default Playwright timeout for PDF generation (it sets the default timeout of the browser context). The value is parsed as an integer, so a value that is not a number makes the command fail. |
Running it in a container
The mcr.microsoft.com/dotnet/sdk:10.0 image has neither Node.js nor the browser libraries. The runs for this page used
Node.js 24 (copied from the official Node.js image into a folder on PATH) and, before the first PDF run, the Playwright
driver bundled with DocFX to install the Chromium operating system dependencies in the throwaway container. The driver
command is Playwright’s own; the official DocFX pages do not prescribe it, so this is a note about the container, not a
DocFX step:
node <docfx tool folder>/.playwright/package/cli.js install-deps chromium
docfx pdf
Without the libraries, docfx pdf found Node.js and downloaded the browser but failed when launching it with a
TargetClosedException whose log ended in "error while loading shared libraries: libglib-2.0.so.0". On an ordinary
developer machine or on a GitHub Actions runner with a browser installed, this step is not needed.
The successful run printed a progress bar per output file and finished with the usual summary:
Searching for manifest in /w/_site
Using Node.js v24.22.1 executable.
api/toc.pdf: 99%
toc.pdf: 99%
Build succeeded.
0 warning(s)
0 error(s)
The output uses the producer docfx (2.81.0.0) in the PDF metadata.
Errors and pitfalls
| What happens | Cause and remedy |
|---|---|
|
|
|
No TOC in the manifest has |
|
Install Node.js 20 or newer, or set |
Failure launching the browser with missing shared libraries (Linux) |
Install Chromium’s system dependencies; in CI use an image that has them or Playwright’s own dependency installer. |
Warning "Timeout waiting for page to load, generated PDF page may be incomplete" |
A page did not become ready within the Playwright timeout; raise |
|
A page of the TOC could not be fetched from the local server (for example a link target that is missing in |
The header or footer shows a file name or is empty |
The path does not exist in the output folder, or the text has no |
The cover is missing |
|
Mermaid diagrams or math missing |
They need the modern template; the PDF is printed from the same pages as the site. |
The bare docfx command runs the pdf stage after the build, so a CI job that calls docfx on a configuration with
pdf: true needs Node.js, whereas docfx build does not. To publish the PDF files together with the site, deploy the
_site folder as usual; they are ordinary files next to the TOC pages, and the "Download PDF" link points to them.
Deployment is covered in Deployment.