ScreenshotNeo

BlogHTML to image & PDF

How to Create Internal Links in PDFs with wkhtmltopdf

Create clickable same-document PDF links with HTML fragments, wkhtmltopdf options, TOCs, outlines, and fixes for footer and build issues.

By the ScreenshotNeo team30 September 20269 min read

How to Create Internal Links in PDFs with wkhtmltopdf

Direct answer: create a normal HTML fragment link such as href="#details", give the destination element the matching id="details", and convert the document with wkhtmltopdf’s local links enabled. Internal links are enabled by default, but --enable-internal-links makes the intent explicit. Do not use --disable-internal-links when the PDF needs same-document navigation.

This pattern works because the HTML link and target form a same-document fragment relationship. The HTML specification defines an element’s id as a destination that a fragment identifier can address. The wkhtmltopdf manual calls these “local links” and documents the conversion option that turns them into PDF references.

1. Minimal working example

Save this as input.html:

A fragment link and matching ID become a clickable destination during PDF conversion.
A fragment link and matching ID become a clickable destination during PDF conversion.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Internal link example</title>
  <style>
    body { font: 16px/1.5 sans-serif; margin: 48px; }
    section { page-break-before: always; }
  </style>
</head>
<body>
  <h1>Project guide</h1>

  <p><a href="#details">Jump to details</a></p>

  <p>Introductory content appears here.</p>

  <section id="details">
    <h2>Details</h2>
    <p>This is the destination of the internal link.</p>
  </section>
</body>
</html>

Convert it with the default behavior:

wkhtmltopdf input.html output.pdf

Or state the local-link setting explicitly:

wkhtmltopdf --enable-internal-links input.html output.pdf

Open output.pdf in your target viewer and click “Jump to details.” It should move to the element whose ID is details.

The upstream wkhtmltopdf usage manual documents --enable-internal-links as the default and --disable-internal-links as the switch that turns local links off. The W3C link specification describes the fragment-and-target relationship.

A fragment link has two parts:

Part Example Rule
Source link <a href="#details"> The value after # is the destination ID.
Destination <h2 id="details"> The id must match the fragment exactly.
PDF result Clickable annotation wkhtmltopdf maps the local HTML relationship into PDF navigation.

Use unique IDs. Matching is case-sensitive in practice, so treat #Details and id="details" as different values. Avoid spaces and punctuation that need URL encoding; short lowercase IDs with hyphens are easiest to maintain.

<nav aria-label="Contents">
  <a href="#installation">Installation</a>
  <a href="#configuration">Configuration</a>
</nav>

<h2 id="installation">Installation</h2>
<p>Install the required package.</p>

<h2 id="configuration">Configuration</h2>
<p>Set the required options.</p>

Putting the ID on the heading is usually clearest. You can put it on a wrapper such as a section when the link should land before the heading and its related content.

Footer links need extra care because wkhtmltopdf renders header and footer content separately from the main page object. A body-to-body link is the simplest case. A footer link to a main-document anchor has been reported as an edge case in upstream issue #2522; the report does not establish that every current build behaves the same way.

Start with a body link and target that you know works:

<body>
  <main>
    <h1 id="top">Report</h1>
    <p>Report content...</p>
    <h2 id="appendix">Appendix</h2>
    <p>Supporting information...</p>
  </main>
</body>

Then add the footer link through a footer HTML file:

<!doctype html>
<html>
<body style="font: 10px sans-serif; margin: 0 24px;">
  <a href="#top">Back to report</a>
</body>
</html>

Convert with:

wkhtmltopdf \
  --enable-internal-links \
  --footer-html footer.html \
  input.html output.pdf

If the footer link opens as an external link, does nothing, or lands incorrectly, test the same link in the body. If the body version works but the footer version fails, you have isolated the separate header/footer case. Check the exact wkhtmltopdf version, the viewer, and whether the footer is being rendered as a separate object. A practical workaround is to place navigation links in the document body when that layout is acceptable.

These three features are often combined, but they are not interchangeable:

Feature How it is created Typical use
Authored local link href="#id" plus a matching id “Jump to details” or “Back to top” links.
Generated TOC A toc object in wkhtmltopdf A contents page generated from heading tags.
PDF outline/bookmarks Heading-derived outline support Navigation pane entries in a PDF viewer.

The manual says generated TOCs and outlines are based on heading tags. TOC links can be disabled with --disable-toc-links; that option affects generated TOC navigation, not a hand-authored href="#id".

A multi-object command can add a TOC before the main document:

wkhtmltopdf \
  --enable-internal-links \
  toc \
  input.html \
  output.pdf

Inspect the outline XML when you need to diagnose bookmarks:

wkhtmltopdf --dump-outline outline.xml input.html output.pdf

The manual also documents --dump-default-toc-xsl as a starting point for customizing generated TOC output and --xsl-style-sheet for supplying a stylesheet. These settings do not replace explicit fragment links.

5. Options that control local navigation

Option or setting Effect When to use it
--enable-internal-links Enables local HTML links in the PDF. Use it explicitly in build scripts for readability.
--disable-internal-links Disables local links. Only when navigation annotations are unwanted.
--disable-toc-links Removes links generated by the TOC. When you need a visual contents page without clickable entries.
--dump-outline Writes the generated outline XML. Debugging heading-derived bookmarks.
toc Adds a generated table of contents object. Documents with heading-based navigation.
useLocalLinks libwkhtmltox page setting corresponding to local links. Applications embedding the library instead of invoking the CLI.
toc.forwardLinks Controls TOC-to-content links. Library-level TOC configuration.
toc.backLinks Controls links back to the TOC. Library-level return navigation.

The older libwkhtmltox page-settings documentation exposes the library equivalents. Keep command-line options and library settings aligned; enabling useLocalLinks in code while passing a conflicting command-line configuration can make debugging harder.

6. Multi-page documents and object boundaries

A long HTML document can span many PDF pages without changing the fragment-link syntax. The destination ID remains attached to the source element, even when that element moves to a later page.

Problems become more likely when you combine multiple wkhtmltopdf objects, such as a cover, a TOC, and one or more page objects. Keep IDs unique across the complete input set. Give each destination a descriptive prefix if separate HTML files are assembled:

<h2 id="guide-installation">Installation</h2>
<h2 id="appendix-installation">Appendix installation notes</h2>

Verify links after the final PDF is assembled. A link that works when converting one HTML file can behave differently when a cover, TOC, or separate object changes page ordering.

7. Troubleshooting checklist

  • Confirm the source uses a fragment such as href="#details", not a misspelled or full URL.
  • Confirm a destination has exactly id="details".
  • Ensure --disable-internal-links is not present.
  • Rebuild the PDF; changing HTML does not update an existing file.
  • Try another PDF viewer to distinguish a viewer display issue from a missing annotation.

“It clicks, but lands on the wrong section”

  • Search for duplicate IDs and remove them.
  • Check case, hyphens, and spelling in both values.
  • Inspect whether the target is inside content that is hidden, repositioned, or conditionally rendered by CSS.
  • Test a short document with one link and one target to isolate layout effects.

Treat this as the documented historical footer edge case. Test the exact installed version and viewer, then move the link into the body or provide a body navigation link if the footer arrangement remains unreliable.

“The TOC has entries but they are not linked”

Check that --disable-toc-links was not supplied. Also distinguish generated TOC links from authored fragment links: a working href="#id" does not prove that the TOC object is configured correctly.

“Bookmarks are missing”

Outline support depends on the build. The upstream manual describes outline support with patched Qt, while the Debian bookworm manual documents a build without patched Qt. Check the installed binary and package documentation instead of assuming every distribution provides identical features.

“The command succeeds, but navigation differs between viewers”

Open the final PDF in the viewer used by your readers and in a second viewer during diagnosis. Verify both the clickable annotation and the landing location. The documented options describe intended behavior, but builds and viewers can differ at the edges.

8. Verification and build reliability

Make link verification part of the PDF build review:

  1. Generate the PDF from the exact binary used in production.
  2. Open it in the target viewer.
  3. Click every navigation class: body-to-section, back-to-top, TOC-to-section, and footer links.
  4. Confirm the destination is visible and at the expected heading.
  5. Repeat after changes to headings, pagination, CSS, object order, or wkhtmltopdf packages.

For deterministic builds, keep the HTML, CSS, wkhtmltopdf version, command-line flags, and input object order under version control. Avoid relying on implicit defaults in scripts: include --enable-internal-links when local navigation is required, and record whether TOC and outline features are supported by the installed package.

9. Performance, reliability, and cost considerations

Internal links add little authoring complexity because they are ordinary HTML relationships. Conversion time is usually dominated by page rendering, external assets, JavaScript, fonts, and the number of pages rather than by the presence of fragment links. Keep navigation IDs stable so content edits do not require rewriting every link.

For reliability, avoid duplicate IDs, verify generated output, and test header/footer links separately. A successful process exit only says that wkhtmltopdf produced a file; it does not prove every annotation works in the reader’s PDF viewer.

wkhtmltopdf is a local command-line workflow, so its cost depends on the machines and infrastructure running it. If you need a hosted screenshot or PDF endpoint instead of maintaining browser setup, ScreenshotNeo provides PDF capture through its API. It also supports paper size, margins, landscape mode, and page ranges.

10. Or skip the browser setup

For a hosted capture workflow, ScreenshotNeo accepts one GET request and returns a screenshot or PDF. See the ScreenshotNeo API documentation for the full option set.

Hosted capture can clean page overlays before producing a PDF or image.
Hosted capture can clean page overlays before producing a PDF or image.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed as clean shots, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

11. FAQ

No. A standard HTML anchor and matching element ID are sufficient.

Should the ID go on the heading or its parent section?

Put it on the heading for a precise landing point, or on a section wrapper when the link should land before a group of content.

No. Local links are authored fragment links. Bookmarks are outline entries commonly derived from heading tags.

It may work, but it has a documented historical edge-case report. Test the exact build and viewer, and keep a body-link fallback when necessary.

How do I inspect generated outline data?

Use --dump-outline and inspect the resulting XML separately from ordinary fragment links.