ScreenshotNeo

BlogHTML to image & PDF

Convert HTML to PDF With Links

Convert HTML to a PDF that keeps external, internal, and fragment links clickable with WeasyPrint, Puppeteer, or wkhtmltopdf.

By the ScreenshotNeo team30 September 20267 min read

Convert HTML to PDF With Links

Short answer: keep normal <a href="..."> elements in your HTML, give the renderer a correct base URL, wait for scripts that create links, and test the generated PDF in the viewer your users will use. Choose WeasyPrint for mostly static HTML/CSS in Python, Puppeteer when JavaScript and browser rendering determine the page, and wkhtmltopdf when its command-line controls fit your deployment.

  1. Use ordinary anchor elements. Do not replace links with click handlers only.
  2. Use absolute URLs, or configure a base URL so relative links can be resolved.
  3. Keep the target element’s id when using fragment links such as /guide#install.
  4. Wait until JavaScript has inserted or rewritten links before rendering.
  5. Open the final PDF and click representative external, internal, and fragment links.

A renderer can preserve link annotations, but the result still depends on the HTML, CSS, scripts, loaded resources, and the PDF viewer. There is no universal link-retention percentage to assume.

A PDF renderer turns HTML anchors into link annotations that readers can click.
A PDF renderer turns HTML anchors into link annotations that readers can click.

2. Choose the renderer

Renderer Use it when Link and rendering notes
WeasyPrint Python workflow; mostly static HTML and CSS Accepts a URL, filename, or HTML string and documents clickable internal and external hyperlinks.
Puppeteer The page needs JavaScript, browser APIs, or precise browser rendering Runs Chromium. PDF output uses print CSS by default; emulate screen media when required.
wkhtmltopdf A standalone CLI and explicit switches suit your deployment Provides controls for external and internal links, JavaScript, images, forms, and local files.

3. WeasyPrint in Python

Install WeasyPrint using the method for your operating system, then render a URL directly:

from weasyprint import HTML

HTML(url="https://example.com/article").write_pdf("article.pdf")

That is enough for a page whose links already use absolute URLs. For HTML strings or local files, set a base URL so relative links resolve:

from weasyprint import HTML

html = """


  
    

Article

External documentation

Jump to details

Details

""" HTML(string=html, base_url="https://example.com/").write_pdf("article.pdf")

WeasyPrint documents hyperlinks, bookmarks, attachments, and forms in its PDF output. It also documents PDF/A and PDF/UA variants, but a requested variant is not a guarantee that the result passes every conformance check; validate the finished file when archival or accessibility compliance matters.

WeasyPrint considerations

  • It is not a browser JavaScript runtime. Links created after page load will not appear unless they are present in the HTML supplied to WeasyPrint.
  • Resolve relative assets and links with base_url or absolute URLs.
  • For a site that requires client-side rendering, generate the final HTML with a browser first or use Puppeteer.

4. Puppeteer in Node.js

Puppeteer renders the page in Chromium, waits for the network to settle, and writes a PDF:

import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com/article", {waitUntil: "networkidle0"});
await page.pdf({path: "article.pdf", format: "A4", printBackground: true});
await browser.close();

page.pdf() uses the print CSS media type by default. If the screen stylesheet is the intended output, emulate it before creating the PDF:

import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com/article", {waitUntil: "networkidle0"});
await page.emulateMediaType("screen");
await page.pdf({
  path: "article-screen.pdf",
  format: "A4",
  printBackground: true,
  preferCSSPageSize: true
});
await browser.close();
await page.goto("https://example.com/article", {waitUntil: "domcontentloaded"});
await page.waitForSelector("article a[href]");
await page.waitForFunction(() => document.fonts.status === "loaded");
await page.pdf({path: "article.pdf", format: "A4", printBackground: true});

Use a page-specific readiness selector when possible. A generic network-idle condition can finish too early for delayed timers or long-lived connections.

Useful Puppeteer PDF settings

Setting Purpose
format Choose a paper preset such as A4.
printBackground Include background colors and images.
preferCSSPageSize Honor CSS @page dimensions when defined.
landscape Use landscape orientation.
margin Set top, right, bottom, and left margins.
displayHeaderFooter Add Chromium header and footer templates when needed.

5. wkhtmltopdf from the command line

Make link and JavaScript behavior explicit in scripts that must be reproducible:

wkhtmltopdf --enable-external-links --enable-internal-links \
  --enable-javascript --javascript-delay 500 \
  page.html page.pdf

The project documents controls for external and internal links, JavaScript, images, forms, and local-file access. External and internal links are enabled by default, but explicit switches make deployment intent clear.

When local files or delayed resources are involved

  • Confirm the input path is readable by the process.
  • Use the documented local-file option only when your deployment requires it.
  • Increase the JavaScript delay only enough for your page’s links and assets to settle; it increases each conversion’s wait time.

Prefer fully qualified URLs:

<a href="https://example.com/pricing">Pricing</a>

If you use /pricing or ../pricing, supply a correct base URL and verify the result in the PDF viewer.

<a href="#installation">Installation</a>

<h2 id="installation">Installation</h2>

The destination needs a stable id. Do not depend on an automatically generated heading ID that can change between builds.

Render with a browser, wait for the selector or application-ready signal, then create the PDF. WeasyPrint and other non-browser renderers will not execute the code that creates those anchors.

7. A repeatable validation checklist

  1. Generate the PDF from the same URL or HTML fixture used in production.
  2. Check an external HTTPS link, a relative link, an internal fragment, and a link inside a repeated component.
  3. Open the file in at least one desktop viewer and one browser PDF viewer used by your audience.
  4. Inspect the link annotation destination when a click appears to do nothing.
  5. Repeat after changing print or screen CSS, JavaScript timing, fonts, or asset hosting.

8. Troubleshooting

Symptom Likely cause Fix
Links are visible but not clickable The renderer received text or an element without an href. Keep real <a href> elements in the source and inspect the generated PDF.
Relative links open the wrong place No correct base URL was supplied. Use absolute URLs or set WeasyPrint base_url; verify the page URL passed to the browser or CLI.
Fragment links do nothing The target id was removed or changed. Keep a stable target ID and test the final PDF.
JavaScript links are missing Rendering finished before the application created them. Use Puppeteer, wait for a readiness selector, and then call page.pdf().
Screen layout changed in the PDF Puppeteer used print media by default. Call page.emulateMediaType("screen"), or adjust print CSS intentionally.
Images or fonts are absent Resources were not loaded, were blocked, or were local files without permission. Check URLs, credentials, network access, and local-file settings; wait for fonts and images before capture.
Output is blank or incomplete Navigation, scripts, or a protected page failed. Capture console and network errors, use a page-specific readiness condition, and verify the source URL independently.
PDF/A or PDF/UA validation fails Renderer support does not guarantee conformance. Use valid semantic HTML, run a conformance checker, and fix the reported violations.

9. Performance, reliability, and cost

  • Performance: browser rendering generally costs more startup and memory than a direct HTML/CSS renderer. Reuse a Puppeteer browser process for batches, but isolate pages and close them after each job.
  • Reliability: set navigation and conversion timeouts, log the source URL and renderer version, and retry only transient navigation failures. A retry cannot fix broken HTML or a permanently blocked resource.
  • Determinism: pin fonts, CSS, browser versions, and time-dependent data when PDFs are compared or archived.
  • Cost: measure total conversion time, CPU, memory, browser downloads, and storage in your own deployment. The cited renderer documentation does not publish universal speed or resource figures.
A clean capture pipeline removes obstructing overlays before producing the document.
A clean capture pipeline removes obstructing overlays before producing the document.

10. Or skip the browser setup

ScreenshotNeo provides a website capture API and MCP server. One GET request returns a clean screenshot or PDF, while its capture pipeline accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Each cleanup step can be turned off.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for PDF output and the available capture options. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

11. FAQ

Only when the renderer emits link annotations and receives valid anchors. Always test the final file in your target viewer.

Which option handles a React or Vue page?

Use Puppeteer and wait for the application-specific ready state before calling page.pdf().

Can I use relative URLs?

Yes, if the renderer has a correct base URL. Absolute URLs remove one common source of failures.

Should I use print or screen CSS?

Use print CSS for a document-oriented layout. Call Puppeteer’s emulateMediaType("screen") when the screen design is the intended PDF.

Keep a fixture containing external, relative, internal, and fragment links, generate the PDF in CI, and click or inspect each annotation in the output.