Guide

Technical Documentation to PDF: READMEs, API Docs and User Manuals

Published July 30, 2026

Technical documentation is written for the screen and occasionally needs to exist on paper — for a compliance review, an offline audit, a customer who wants a manual, or an archive of what a system looked like at a point in time. That transition exposes a set of specific problems: relative links that resolve to nothing, badges that are live network requests, code blocks with no horizontal scroll to fall back on.

This guide covers four documentation types and the conversion problems each one has.

GitHub READMEs

A README is the commonest technical document and the one most likely to be converted. It is also written with a very specific renderer in mind, which is where the problems come from.

GitHub resolves relative links against the repository. [Contributing](CONTRIBUTING.md) works on github.com and produces a dead link in a PDF — there is no repository context, and no CONTRIBUTING.md next to the PDF.

Three options, in order of preference:

Rewrite to absolute URLs before converting. GNU sed has no lookahead, so use perl:

perl -pe 's{\]\((?!https?://|#)([^)]+)\)}{](https://github.com/acme/widget/blob/main/$1)}g' \
  README.md > README.absolute.md

Or inline the referenced content. If the PDF is meant to be self-contained — a handover document — concatenating is more useful than linking:

cat README.md CONTRIBUTING.md docs/ARCHITECTURE.md > handover.md

Insert <div class="page-break"></div> between them so each starts on a fresh page.

Or accept the dead links and expand them visibly, so the reader at least knows something was there:

@media print {
  a[href]:not([href^="http"])::after {
    content: " [" attr(href) "]";
    font-size: 8pt;
    color: #888;
  }
}

Anchor links (#installation) are a separate case and generally fine — they resolve within the document and work as internal PDF links.

Badges

Badges are live images from shields.io, codecov, CI providers. In a PDF they become one of three things: a correctly-rendered snapshot, a broken image icon, or nothing.

They are also stale by definition once embedded. A build-passing badge in a PDF from three months ago asserts something about today that it cannot know — arguably worse than absent.

For most documents, strip them:

@media print {
  p > a > img[src*="shields.io"],
  p > a > img[src*="badge"],
  p > img[src*="shields.io"] {
    display: none;
  }
}

If you want them, ensure they actually load — badges are remote requests, subject to the timing problem covered in the images guide:

await page.goto(url, { waitUntil: "networkidle0" });

Better still, replace the badge row with a static status table, which is honest about being a snapshot:

| | |
| --- | --- |
| Version | 4.2.0 |
| Build | Passing (as of 26 February 2026) |
| Coverage | 87% |
| Licence | Apache-2.0 |

Code blocks

A README is largely code blocks, and this is where screen-to-paper conversion hurts most. On screen a long line scrolls; on paper it is cut off at the margin and the information is gone.

@media print {
  pre {
    white-space: pre-wrap;
    word-wrap: break-word;
    overflow-wrap: break-word;
    font-size: 8.5pt;
    line-height: 1.45;
    break-inside: avoid;
    page-break-inside: avoid;
    background: #f7f7f9;
    border: 0.75pt solid #e4e4e7;
    padding: 7pt 9pt;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
}

pre-wrap is the important one. Wrapped code is uglier than scrolled code, and it is the only option that does not silently lose characters.

Wrapping creates its own ambiguity: the reader cannot tell a wrapped line from a real newline, which matters for shell commands that someone may retype. Two mitigations. Use explicit continuations so the wrap points are intentional:

docker run --rm \
  -v "$PWD:/work" \
  -w /work \
  ghcr.io/acme/widget:4.2.0 \
  build --target production

Or add a hanging indent so continuation lines are visibly distinct from line starts:

@media print {
  pre code {
    display: block;
    padding-left: 1.6em;
    text-indent: -1.6em;
  }
}

Syntax highlighting needs print-color-adjust: exact on the token spans, not just the pre:

@media print {
  pre, pre code, pre span {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
}

Without it on the spans, some engines strip token colours while keeping the block background — you get a grey box of black text.

For monochrome output, carry the distinctions with weight and style instead of hue:

@media print {
  .hljs-keyword { font-weight: 700; color: #111; }
  .hljs-string  { font-style: italic; color: #333; }
  .hljs-comment { color: #777; }
  .hljs-number,
  .hljs-literal { font-weight: 600; color: #111; }
}

There is a monochrome media feature in the spec, but implementation is thin enough that you should not rely on it. A dedicated print theme with greyscale-safe token colours is the dependable approach.

A README print stylesheet

@page {
  size: A4;
  margin: 20mm 18mm;
  @bottom-center {
    content: counter(page) " / " counter(pages);
    font-size: 8pt;
    color: #999;
  }
}

@media print {
  body {
    font-family: "Inter", system-ui, sans-serif;
    font-size: 10pt;
    line-height: 1.55;
  }

  h1 {
    font-size: 21pt;
    border-bottom: 1pt solid #d4d4d8;
    padding-bottom: 5pt;
  }

  h2 {
    font-size: 14.5pt;
    border-bottom: 0.5pt solid #e4e4e7;
    padding-bottom: 3pt;
    margin-top: 18pt;
    break-after: avoid;
  }

  h3 { font-size: 11.5pt; break-after: avoid; }

  :not(pre) > code {
    font-family: "JetBrains Mono", ui-monospace, monospace;
    font-size: 0.87em;
    background: #f1f1f4;
    padding: 0.1em 0.32em;
    border-radius: 2pt;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }

  table { width: 100%; border-collapse: collapse; font-size: 9pt; }
  thead { display: table-header-group; }
  th, td { border: 0.5pt solid #d4d4d8; padding: 4pt 6pt; text-align: left; }
  th {
    background: #f4f4f6;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }

  /* Task lists in a roadmap section. */
  input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    width: 9pt; height: 9pt;
    border: 0.75pt solid #555;
    margin-right: 4pt;
    vertical-align: -0.5pt;
  }
  input[type="checkbox"]:checked::after {
    content: "✓";
    display: block;
    font-size: 8pt;
    line-height: 8pt;
    text-align: center;
  }
}

There is a dedicated GitHub README to PDF page with the tool and a worked example.

API reference documentation

API docs are dense, repetitive, and scanned rather than read. Structural consistency matters more than anything else, because readers learn the shape once and navigate by it.

## POST /v1/documents

Creates a document and queues it for conversion.

**Authentication:** Bearer token, `documents:write` scope
**Rate limit:** 100 requests per minute per token
**Idempotent:** Yes, via `Idempotency-Key` header

### Request

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source` | string | Yes | Markdown source. Max 2 MiB. |
| `format` | enum | No | `pdf` (default) or `html`. |
| `page_size` | enum | No | `a4` (default), `letter`, `legal`. |
| `theme` | string | No | Theme identifier. Defaults to `clean`. |
| `metadata` | object | No | Arbitrary key–value pairs, echoed back. |

### Response — 201 Created

    {
      "id": "doc_8fH2kLp9",
      "status": "queued",
      "created_at": "2026-02-26T14:22:31Z",
      "download_url": null
    }

### Errors

| Status | Code | Cause |
| --- | --- | --- |
| 400 | `invalid_source` | `source` empty or not valid UTF-8 |
| 401 | `unauthorized` | Token missing, expired or malformed |
| 403 | `insufficient_scope` | Token lacks `documents:write` |
| 413 | `source_too_large` | `source` exceeds 2 MiB |
| 429 | `rate_limited` | Retry after the `Retry-After` header |

Layout for reference material

The constraint is that an endpoint should not straddle a page boundary if it can be avoided:

@media print {
  /* Each endpoint starts a fresh page in a long reference. */
  .endpoint {
    break-before: page;
    page-break-before: always;
  }

  .endpoint:first-of-type {
    break-before: auto;
    page-break-before: auto;
  }

  /* Keep a heading with its parameter table. */
  h3, h3 + p, h3 + table {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* Method + path as a distinct band. */
  h2 {
    font-family: "JetBrains Mono", monospace;
    font-size: 12pt;
    background: #f1f1f4;
    padding: 5pt 8pt;
    border-left: 3pt solid #1d4ed8;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }

  /* Parameter tables: fixed widths beat auto layout here. */
  .params { table-layout: fixed; }
  .params th:nth-child(1) { width: 22%; }
  .params th:nth-child(2) { width: 14%; }
  .params th:nth-child(3) { width: 12%; }
  .params th:nth-child(4) { width: 52%; }

  /* Required markers that survive greyscale. */
  .required { font-weight: 700; }
  .required::after { content: " *"; }
}

One endpoint per page is excellent for a printed reference someone flips through, and produces an unusable brick for a 200-endpoint API. There, group by resource and force breaks only between resources.

Generating from a spec

If you have an OpenAPI document, hand-writing reference docs is duplicated effort that will drift. Generate Markdown from the spec, then convert:

npx widdershins openapi.yaml -o api.md \
  --language_tabs 'shell:curl' 'javascript:Node'

The generated Markdown needs light editing — generators are verbose — but field tables and status codes stay in sync with the spec, which is the part that matters.

User manuals

A manual is written for someone who does not know the product, may be reading on paper, and is probably mildly frustrated. Numbered procedures and consistent terminology carry most of the weight.

# Chapter 3 — Configuring exports

## 3.1 Before you begin

You will need:

- A document open in the editor
- The page size your recipient expects (A4 outside North America, Letter within it)

## 3.2 Setting the page size

1. Open the **Export** panel from the toolbar.
2. Under **Page**, select a size from the dropdown.
3. The preview repaginates immediately. Confirm the page count at the
   bottom of the preview.

> **Note.** Changing page size after adjusting margins preserves your margin
> values. If content no longer fits, reduce the margins rather than the
> font size.

## 3.3 Adjusting margins

1. In the **Export** panel, locate **Margins**.
2. Enter values in millimetres, or choose a preset.
3. Values below 10 mm may be clipped by physical printers.

> **Caution.** Margins below 5 mm are rejected by some print drivers and the
> export may fail without a clear error.

## 3.4 Troubleshooting

| Symptom | Likely cause | Resolution |
| --- | --- | --- |
| Content cut off at the right edge | Table or code block wider than the measure | Reduce that element's font size, or switch to landscape (§3.6) |
| Unexpected blank page | Forced page break at a natural boundary | Remove the manual break |
| Preview differs from download | Fonts not finished loading | Wait for the preview to settle, then export |

Conventions worth adopting

Numbered sections that match the page furniture. 3.2 in the text and Chapter 3 in the running header lets a reader confirm where they are. Automate it:

body { counter-reset: chapter; }
h1 { counter-increment: chapter; counter-reset: section; }
h2 { counter-increment: section; }
h2::before { content: counter(chapter) "." counter(section) " "; }

Bold for interface elements. “Open the Export panel” — a consistent convention means the reader can scan for what to click.

Note, caution and warning levels differentiated by more than colour:

@media print {
  blockquote { break-inside: avoid; padding: 7pt 10pt; margin: 10pt 0; }

  .note    { border-left: 3pt solid #2563eb; background: #f5f8ff; }
  .caution {
    border-left: 3pt solid #d97706;
    border-top: 0.5pt solid #d97706;
    border-bottom: 0.5pt solid #d97706;
    background: #fffaf0;
  }
  .warning {
    border: 1.5pt solid #b91c1c;
    background: #fef4f4;
    font-weight: 500;
  }
  .note, .caution, .warning {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
}

Border weight and style differentiate them in greyscale, which colour alone would not.

A troubleshooting table per chapter, not one at the end — readers look for the fix near the task.

Screenshots at print resolution. Covered in the images guide: capture at 2×, and expect to need 1400–2000px for a full-measure figure.

Formal technical reports

Reports for clients, regulators or auditors have expectations about structure and completeness that internal documents do not.

<div class="cover">

# Load Testing Report

## Order Management Platform, Release 4.2

**Prepared for:** Northwind Trading GmbH
**Prepared by:** Rivera Consulting Ltd
**Report reference:** LT-2026-014
**Date:** 26 February 2026
**Classification:** Client Confidential

</div>

## 1. Executive summary

Two or three paragraphs a non-technical decision-maker can act on. State the
headline finding, the risk, and the recommendation. No methodology here.

## 2. Scope and objectives

What was tested, what was not, and the questions the test set out to answer.
Explicit exclusions belong here.

## 3. Methodology

Enough detail that another engineer could reproduce the test: tooling,
versions, environment specification, load profile, duration, data volumes.

## 4. Findings

### 4.1 Sustained throughput

Data first, interpretation second.

| Concurrent users | Requests/sec | p50 (ms) | p95 (ms) | p99 (ms) | Error rate |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 500 | 1,240 | 42 | 118 | 190 | 0.00% |
| 1,000 | 2,380 | 48 | 145 | 260 | 0.00% |
| 2,000 | 4,100 | 71 | 310 | 720 | 0.02% |
| 4,000 | 4,350 | 210 | 1,840 | 4,200 | 1.34% |

Throughput plateaus at approximately 4,350 requests per second. Beyond
2,000 concurrent users, p99 latency degrades non-linearly and the error rate
becomes material.

## 5. Recommendations

Numbered, prioritised, each with an owner and estimated effort.

1. **Raise the connection pool ceiling from 100 to 250.** Low effort,
   addresses the immediate constraint. *Est. 2 hours.*
2. **Add a read replica for order history queries.** Medium effort, required
   before the next peak. *Est. 3 days.*

## 6. Limitations

State them plainly. A report claiming no limitations is not credible.

## Appendix A — Environment specification

Report layout

@page {
  size: A4;
  margin: 26mm 22mm 24mm;
  @top-left {
    content: string(section, first);
    font-size: 8pt;
    color: #666;
  }
  @top-right {
    content: "LT-2026-014 · Client Confidential";
    font-size: 8pt;
    color: #999;
  }
  @bottom-center {
    content: counter(page) " of " counter(pages);
    font-size: 8.5pt;
    color: #666;
  }
}

@page :first {
  @top-left     { content: none; }
  @top-right    { content: none; }
  @bottom-center{ content: none; }
}

@media print {
  h2 { string-set: section content(); }

  body { counter-reset: sec; }
  h2 { counter-increment: sec; counter-reset: subsec; }
  h3 { counter-increment: subsec; }

  .toc a::after {
    content: leader(dotted) " " target-counter(attr(href), page);
  }

  table {
    font-variant-numeric: tabular-nums;
    font-size: 9pt;
  }
  thead { display: table-header-group; }

  /* Appendices restart with letters. */
  .appendix { counter-reset: sec; }
  .appendix h2::before {
    content: "Appendix " counter(sec, upper-alpha) " — ";
  }
}

The classification marking in @top-right is not decoration. For documents under a confidentiality obligation it is often contractually required on every page, and a margin box is the only way to guarantee that.

Keeping exports current

The recurring failure with documentation PDFs is that they outlive their accuracy. Someone finds a two-year-old exported manual, trusts it, and follows a procedure that no longer applies.

Stamp every export with its source and generation date.

@page {
  @bottom-left {
    content: "Generated 26 Feb 2026 from docs/manual/ch3.md";
    font-size: 7pt;
    color: #aaa;
  }
}

Generate in CI rather than by hand. A PDF built from main on every release is current by construction — see the automation guide.

Version the filename, not just the content. widget-manual-v4.2.0.pdf cannot be mistaken for the current version once 4.3 ships. manual.pdf can and will be.

Try it yourself

The editor here renders GFM — tables, task lists, fenced code with syntax highlighting — and exports paginated PDFs with the code-wrapping and header-repetition behaviour described above. Paste a README in to see how it handles your particular mix of badges, tables and code.

Related: tables, math, diagrams and images, team documentation, and print CSS.

About this guide

Written and maintained by the developer of MarkdownToFile — the browser-based converter this site runs on. Techniques described here are tested against the engines named in the text (Chromium's print pipeline, Paged.js, WeasyPrint), and engine limitations are stated rather than glossed over. Corrections are welcome via the contact page; more about the project on the about page.

Try it yourself — free, no signup

Convert your Markdown to a polished PDF right in your browser.

Open the editor