Use print CSS to control the document, then render that HTML with a browser engine or an HTML-to-PDF library. Define paper size and margins with @page, hide screen-only elements in @media print, and use break-before, break-after, and break-inside to manage pagination. For JavaScript-heavy pages, Puppeteer or Playwright provide browser-quality output; for a Python server workflow, WeasyPrint can convert HTML directly.
1. Build HTML that can paginate
Start with semantic sections rather than a single oversized container. Headings, paragraphs, lists, tables, and figures give the renderer meaningful boxes at which it can place page breaks. Keep content inside the document’s normal flow; absolute positioning is useful for a cover or watermark, but it can overlap or disappear when content grows.
A minimal multipage document
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quarterly report</title>
<link rel="stylesheet" href="print.css">
</head>
<body>
<header class="screen-only">Dashboard navigation</header>
<main>
<section class="chapter">
<h1>Quarterly report</h1>
<p>Summary content…</p>
</section>
<section class="chapter">
<h2>Results</h2>
<p>Detailed results…</p>
<table>…</table>
</section>
</main>
</body>
</html>
2. Add print CSS for paper, margins, and breaks
Print rules participate in normal CSS specificity and precedence. If an existing screen rule wins, increase the selector’s specificity or place the print stylesheet later in the cascade. The @page rule sets the printable page box; @media print changes the document’s appearance when it is printed or converted to PDF.
@page {
size: A4 portrait;
margin: 18mm 16mm 20mm;
}
@media print {
.screen-only,
nav,
.cookie-banner,
.chat-widget,
.print-button {
display: none !important;
}
body {
margin: 0;
color: #111;
background: #fff;
font: 10.5pt/1.45 Arial, sans-serif;
}
h1, h2, h3 {
break-after: avoid;
}
.chapter {
break-before: page;
}
.chapter:first-child {
break-before: auto;
}
figure, table, pre, blockquote {
break-inside: avoid;
}
a {
color: inherit;
text-decoration: none;
}
}
/* Legacy name remains useful for older engines. */
.chapter {
page-break-before: auto;
}
.chapter + .chapter {
page-break-before: always;
}
break-before: page forces a new page before a box. break-inside: avoid is a preference, not an absolute guarantee: if a table or paragraph is taller than the available page, the renderer must split it. The older page-break-before property is aliased for compatibility.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Page size and orientation
Use named sizes such as A4 or Letter, optionally followed by portrait or landscape. You can also specify dimensions, for example size: 210mm 297mm. Leave enough margin for the target printer or binding. CSS page margins and renderer options can both exist; decide which is authoritative to avoid surprises.
Backgrounds, images, and fonts
Background colors and images are commonly disabled by print defaults. Enable background printing in the renderer when the design depends on them. Use absolute or data URLs for assets when a renderer cannot resolve relative paths, and wait for web fonts before saving. A missing font can change line wrapping and therefore every later page break.
3. Render with Puppeteer (Node.js)
Puppeteer’s page.pdf() generates a PDF using the print CSS media type. The following script loads a local file, waits for fonts, and writes a multipage PDF.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: 'new'});
try {
const page = await browser.newPage();
await page.goto('file:///absolute/path/report.html', {
waitUntil: 'networkidle0'
});
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: {top: '18mm', right: '16mm', bottom: '20mm', left: '16mm'}
});
} finally {
await browser.close();
}
Install Puppeteer with npm install puppeteer. For a web URL, replace the file:// address and wait for the page’s data to finish loading. If the page needs authentication, set cookies or headers before navigation. The PDF method waits for fonts by default, but explicitly awaiting document.fonts.ready makes the dependency visible in your code.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsWhen to choose Puppeteer
- The document uses client-side JavaScript, complex browser layout, or components already tested in Chromium.
- You need browser APIs such as cookies, custom headers, or interaction before capture.
- You can run a browser process in your server, container, or build job.
4. Render with Playwright (Node.js)
Playwright exposes the same fundamental PDF workflow and documents options for paper format, dimensions, margins, page ranges, scale, background printing, and whether CSS page size should take precedence.
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage({viewport: {width: 1280, height: 900}});
await page.goto('https://example.com/report', {waitUntil: 'networkidle'});
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'report.pdf',
format: 'Letter',
landscape: false,
printBackground: true,
preferCSSPageSize: true,
margin: {top: '0.7in', right: '0.65in', bottom: '0.8in', left: '0.65in'},
scale: 1
});
} finally {
await browser.close();
}
Use page.pdf({pageRanges: '1-3'}) when you need only selected pages. If the PDF ignores your @page dimensions, check preferCSSPageSize; when true, CSS wins over a format or width/height supplied to the API.
5. Convert HTML with WeasyPrint (Python)
WeasyPrint provides a server-side library path. Its HTML object accepts a filename, URL, file object, or string, and write_pdf() writes the result. Its render() method returns a document whose page objects can be inspected.
from weasyprint import HTML
HTML(filename='report.html', base_url='.').write_pdf('report.pdf')
# From an HTML string, keep a base URL so relative assets resolve.
html = HTML(string='<h1>Report</h1><p>Body</p>', base_url='.')
document = html.render()
print(f'pages: {len(document.pages)}')
document.write_pdf('string-report.pdf')
Use this route when your application is Python-based and the content does not require a full browser’s JavaScript execution. Test the CSS your document relies on; browser and library pagination are not identical, especially for advanced layout, scripts, and interactive components.
Rank #3
6. Choose a rendering path
| Path | Best fit | Important controls | Operational trade-off |
|---|---|---|---|
| Puppeteer | Chromium-rendered pages and JavaScript applications | page.pdf(), print CSS, backgrounds, margins, CSS page size |
Operate a browser process |
| Playwright | Browser automation with explicit PDF and page-range options | Format or dimensions, margins, scale, preferCSSPageSize, printBackground |
Operate browser binaries and manage jobs |
| WeasyPrint | Python services with mostly server-rendered HTML/CSS | HTML(...).write_pdf(), render() and page objects |
Validate CSS and asset support for your design |
| Hosted API | Teams that do not want to run a renderer | Submit HTML or a document URL; provider-specific settings | External service, network and data-handling considerations |
There is no neutral, universal performance winner established by the documentation. Compare JavaScript requirements, language integration, page and break controls, background and font handling, and whether your team wants to patch and scale a renderer.
7. A repeatable production workflow
- Prepare the source. Make URLs, fonts, images, and data available to the rendering environment. Add a base URL for relative assets.
- Write print rules. Hide navigation and controls, set
@page, and add intentional chapter breaks. - Configure the renderer. Choose paper format, margins, background printing, scale, and CSS-page-size precedence.
- Wait for readiness. Wait for network activity, application-specific selectors, images, and
document.fonts.ready. - Inspect every page. Look for clipped edges, overflow, blank pages, broken tables, missing backgrounds, and headings stranded at the bottom.
- Regress representative documents. Keep a short, long, image-heavy, table-heavy, and multilingual fixture. Pagination can change when a font, browser, or CSS rule changes.
8. Troubleshooting common failures
Content is cut off or extends beyond the page
Check for fixed heights, absolute positioning, large unbreakable elements, and a renderer margin that conflicts with @page. Remove hard-coded screen heights and allow long words or code blocks to wrap. A block taller than one page cannot honor break-inside: avoid.
Every section starts on a new page, including the cover
A broad break-before: page selector is probably matching the first section. Override the first item with break-before: auto, as in the example, and inspect legacy page-break-before rules.
Background colors or images are missing
Enable printBackground in Puppeteer or Playwright. Also confirm the asset URL is reachable from the renderer and that the CSS rule is inside (or applies to) print media.
Rank #4
Fonts are wrong and pagination shifts
Make the font files accessible, wait for document.fonts.ready, and avoid saving immediately after navigation. If a remote font is unreliable, package it with the job or use a dependable fallback and test the resulting line lengths.
JavaScript content is blank
Do not save immediately after the initial response. Wait for a meaningful selector, application-ready signal, or network idle. If the page requires a click, perform it before calling the PDF method.
Local images or styles do not load in Python
Pass a correct base_url to WeasyPrint, use valid file paths, and check permissions. A document created from an HTML string has no useful relative URL base unless you provide one.
Tables split in unusable places
Apply break-inside: avoid to small tables or row groups where supported, but do not apply it to a table that can exceed a page. For very long tables, repeat headers with table-specific print CSS and accept row-level splits where necessary.
Best Value
Or skip the browser setup
ScreenshotNeo can capture a page or create a PDF through one request. Its PDF options include paper size, margins, landscape mode, and page ranges; it also supports custom CSS, JavaScript, waiting for a selector or network idle, cookies, headers, user agents, and authentication.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
For PDF output, add the service’s PDF parameters described in the ScreenshotNeo documentation and choose a PDF response format.
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
9. Reliability, performance, and cost considerations
- Browser startup: Reuse a browser process for batches instead of launching one per document, while isolating pages and closing them after each job.
- Network determinism: Remote ads, trackers, fonts, and third-party widgets can delay or alter pagination. Block unnecessary requests or self-host critical assets.
- Memory: Large images and very long pages increase browser memory. Resize source images and process large batches with bounded concurrency.
- Caching: Cache immutable HTML and assets, but invalidate when data, CSS, fonts, or browser versions change.
- Security: Treat user-supplied HTML and URLs as untrusted. Sandbox browser jobs, restrict network access where possible, and prevent access to internal metadata endpoints.
- Validation: Record renderer version, CSS revision, input URL or document hash, and output page count so a changed PDF can be diagnosed.
Frequently Asked Questions
Can CSS guarantee that a heading and its paragraph stay together?
No. break-after: avoid and break-inside: avoid are pagination preferences. The renderer may still split content when it cannot fit the requested block on a page.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Should I use a browser or WeasyPrint for JavaScript-rendered HTML?
Use a browser renderer when the final content depends on client-side JavaScript or browser APIs. WeasyPrint is a better fit for server-rendered HTML when you do not need JavaScript execution.
How can I generate only selected PDF pages?
Use a renderer option that supports page ranges, such as Playwright’s pageRanges. The exact syntax and availability depend on the renderer you choose.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

