ScreenshotNeo

BlogHTML to image & PDF

How to Generate a Single-Page PDF with PuppeteerSharp

Generate a reliable one-page PDF with PuppeteerSharp by controlling page size, CSS, margins, fonts, media, and deployment settings.

By the ScreenshotNeo team30 September 20268 min read

How to Generate a Single-Page PDF with PuppeteerSharp

To generate a single-page PDF with PuppeteerSharp, render the page in headless Chrome, choose a paper size that can contain the layout, set margins explicitly, wait for fonts and asynchronous content, and call PdfAsync. If the page defines its own size with CSS @page, set PreferCSSPageSize = true. A one-page result depends on the relationship between content dimensions, print CSS, margins, and the selected sheet size; there is no universal option that makes arbitrary content fit one page automatically.

1. Complete C# example

The following example creates an A4 portrait PDF. The CSS and PDF options agree on the paper size, margins are zero, backgrounds are printed, and font loading is awaited.

The capture pipeline: render the page, apply print rules, and fit the content to the selected sheet.
The capture pipeline: render the page, apply print rules, and fit the content to the selected sheet.
using PuppeteerSharp;

using var browser = await Puppeteer.LaunchAsync(
    new LaunchOptions { Headless = true });
using var page = await browser.NewPageAsync();

await page.SetContentAsync("""
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: 210mm 297mm; margin: 0; }
    html, body { margin: 0; padding: 0; }
    body { font-family: Arial, sans-serif; }
    .sheet {
      box-sizing: border-box;
      width: 210mm;
      min-height: 297mm;
      padding: 16mm;
      overflow: hidden;
    }
    h1 { margin-top: 0; }
    @media print {
      * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
    }
  </style>
</head>
<body>
  <main class="sheet">
    <h1>Single-page report</h1>
    <p>Keep the content inside the chosen printable area.</p>
  </main>
</body>
</html>
""");

await page.PdfAsync("output.pdf", new PdfOptions
{
    Width = "210mm",
    Height = "297mm",
    MarginOptions = new MarginOptions
    {
        Top = "0",
        Right = "0",
        Bottom = "0",
        Left = "0"
    },
    PreferCSSPageSize = true,
    PrintBackground = true,
    WaitForFonts = true
});

Install PuppeteerSharp through NuGet, then run this code in a .NET project. In a production service, install the browser during deployment and pass its executable path to LaunchAsync when your environment does not provide the expected Chromium installation.

2. What “single page” means in PuppeteerSharp

PuppeteerSharp prints the rendered document according to print layout rules. It does not first measure any arbitrary HTML document and calculate a perfect page height. If the content is taller than the printable area, Chrome creates additional pages. If you force a very small sheet, content can be clipped or scaled.

There are three practical ways to obtain one page:

  1. Constrain the document to a known sheet, such as A4 or Letter.
  2. Choose custom width and height that match the intended content.
  3. Reduce or reflow the content until it fits the selected printable area.

Always open the resulting PDF and verify its page count, clipping, font rendering, and colors. PageRanges = "1" does not solve fitting: it selects the first generated page and can silently discard later pages.

3. Choosing page size and ownership

Standard paper formats

Use Format when the output must be a standard paper size. This is convenient for invoices, letters, and reports intended for printing. When Format is set, it takes priority over Width and Height.

await page.PdfAsync("letter.pdf", new PdfOptions
{
    Format = PaperFormat.Letter,
    MarginOptions = new MarginOptions
    {
        Top = "12mm",
        Right = "12mm",
        Bottom = "12mm",
        Left = "12mm"
    },
    PrintBackground = true,
    WaitForFonts = true
});

Do not combine Format with custom dimensions when custom dimensions are the requirement. Remove Format and set both dimensions instead.

Custom width and height

Set dimensions with units such as mm, cm, in, or CSS-compatible lengths. A custom height is useful for a receipt, a certificate, or a report whose content has a known vertical extent.

await page.PdfAsync("custom.pdf", new PdfOptions
{
    Width = "180mm",
    Height = "260mm",
    MarginOptions = new MarginOptions
    {
        Top = "5mm",
        Right = "5mm",
        Bottom = "5mm",
        Left = "5mm"
    },
    PrintBackground = true,
    WaitForFonts = true
});

Remember that margins consume the sheet. A 210mm by 297mm sheet with 10mm margins has less usable space than its nominal dimensions suggest.

Let CSS own the page size

Put the authoritative size in @page and enable PreferCSSPageSize. This is usually the clearest choice when the page already has print styles.

@page {
  size: 148mm 210mm;
  margin: 8mm;
}
new PdfOptions
{
    PreferCSSPageSize = true,
    PrintBackground = true,
    WaitForFonts = true
}

With PreferCSSPageSize = false, the PDF options control the sheet and the documentation describes content as being scaled to fit that paper size. Choose one owner deliberately so CSS and C# do not fight each other.

4. Fit the HTML before calling PdfAsync

Reliable one-page output starts with predictable layout. Give the main wrapper an explicit width and height, use box-sizing: border-box, reset default body margins, and avoid unbounded elements.

html, body { margin: 0; padding: 0; }
*, *::before, *::after { box-sizing: border-box; }
.sheet {
  width: 210mm;
  height: 297mm;
  padding: 14mm;
  overflow: hidden;
}
img { max-width: 100%; height: auto; }
table { width: 100%; border-collapse: collapse; }

Use overflow: hidden only when clipping is an intentional design decision. For reports, it is safer to detect overflow and revise the content or sheet size. Long tables, large images, fixed-position elements, and unbroken code lines are common causes of a second page.

For dynamic pages, wait for the application state you need before PDF generation:

await page.SetContentAsync(html);
await page.WaitForSelectorAsync("#report-ready");
await page.EvaluateExpressionAsync("document.fonts.ready");
await page.WaitForTimeoutAsync(100);

WaitForFonts defaults to true and waits for document.fonts.ready. If that wait never resolves for a background page, activate it with BringToFrontAsync() before generating the PDF.

5. Print media, colors, and backgrounds

PDF generation uses print CSS media by default. If your page has screen-only rules, request screen media before calling PdfAsync:

await page.EmulateMediaTypeAsync(MediaType.Screen);
await page.PdfAsync("screen-style.pdf", new PdfOptions
{
    Width = "210mm",
    Height = "297mm",
    PrintBackground = true,
    PreferCSSPageSize = true
});

For normal documents, leave print media active and define print-specific rules in @media print. PrintBackground defaults to false, so enable it when colored panels, images, or background graphics are part of the design. Browsers can modify printed colors; -webkit-print-color-adjust: exact requests closer color preservation, but inspect the actual PDF.

6. Returning bytes or a stream

Use PdfAsync with a file path for a straightforward command-line or batch job. Use PdfDataAsync when an ASP.NET endpoint must return bytes, or PdfStreamAsync when the surrounding API expects a stream.

var pdfBytes = await page.PdfDataAsync(new PdfOptions
{
    Width = "210mm",
    Height = "297mm",
    PreferCSSPageSize = true,
    PrintBackground = true
});

return Results.File(pdfBytes, "application/pdf", "report.pdf");

Keep the browser alive for a batch of pages and create or reuse pages carefully. Launching a new Chromium process for every document adds startup work and increases resource pressure.

7. Options that affect pagination

Option Effect One-page guidance
Format Selects a standard paper size and overrides dimensions. Use alone for standard paper.
Width, Height Define a custom sheet. Set both with explicit units.
PreferCSSPageSize Gives CSS @page priority. Enable when CSS owns sizing.
MarginOptions Reserves printable margins. Set all four deliberately.
Scale Changes rendering scale; permitted range is 0.1 to 2, default 1. Use only after layout is correct.
PrintBackground Includes background graphics. Enable for designed sheets.
PageRanges Selects pages from the generated output. Do not use it as a fitting mechanism.
WaitForFonts Waits for web fonts by default. Keep enabled unless you manage readiness yourself.

8. Troubleshooting common failures

The PDF has two or more pages

Cause: content exceeds the printable height, margins are larger than expected, or a child element has fixed dimensions. Fix: inspect the rendered layout, reduce padding or font size, constrain images and tables, increase custom height, or choose a larger standard sheet. Do not hide the problem with PageRanges.

Content is clipped

Cause: a fixed-height wrapper or overflow: hidden cuts off content. Fix: remove the clipping rule while debugging, measure the content, then redesign the layout or increase the sheet height.

Backgrounds are missing

Cause: PrintBackground is false, which is its default. Fix: set it to true and confirm the styles are active under print media.

Fonts are wrong or the PDF hangs

Cause: the font has not loaded, the page is in a background state, or a remote font request is unavailable. Fix: wait for the application’s ready marker and document.fonts.ready; call BringToFrontAsync() when needed; package critical fonts locally for deployment.

PDF generation times out in deployment

Cause: Chromium is missing, the browser version differs from development, or sandbox permissions are incorrect. PuppeteerSharp’s troubleshooting guidance describes a Chromium 125+ case where PDF generation requires sandbox permissions. Check InstalledBrowser.PermissionsFixed, install the browser ahead of runtime, and pass its path to LaunchAsync.

Screen layout appears in the PDF

Cause: the page depends on screen media rules. Fix: either add print styles or call EmulateMediaTypeAsync(MediaType.Screen) intentionally before printing.

9. Or skip the browser setup

If your goal is a clean PDF or screenshot endpoint rather than managing Chromium, ScreenshotNeo provides a single API request. See the ScreenshotNeo documentation for the available parameters.

A clean capture removes common overlays before the final document is produced.
A clean capture removes common overlays before the final document is produced.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -d format=pdf -o report.pdf
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"},
    timeout=90,
)
r.raise_for_status()
open("report.pdf", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await Bun.write('report.pdf', data);

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

10. Performance, reliability, and cost checklist

  • Reuse a browser process for batches, while isolating pages and closing them after use.
  • Wait for a deterministic application-ready selector instead of adding an unnecessarily long fixed delay.
  • Use local assets or controlled network access when reproducible output matters.
  • Pin compatible Chromium and PuppeteerSharp versions across development and deployment.
  • Record the selected dimensions, margins, media type, and page count with each generated document.
  • Open representative PDFs in an automated review step to catch clipping and unexpected pagination.
  • For hosted capture, compare the predictable API cost with the engineering and infrastructure cost of operating browsers. ScreenshotNeo bills only clean shots and offers caching with a caller-selected TTL.

11. FAQ

Can PuppeteerSharp guarantee one page for any HTML?

No. You must make the content fit the selected page dimensions and printable area.

Should I use Format or Width and Height?

Use Format for standard paper. Use custom dimensions when the document has a defined sheet size or receipt-like layout.

Does Scale automatically solve overflow?

It changes rendering scale within the documented 0.1–2 range, but you still need to inspect pagination and readability.

Why does PageRanges = "1" lose content?

It selects the first generated page; it does not reflow the document into one page.

Can I generate PDFs without writing a temporary file?

Yes. Use PdfDataAsync for bytes or PdfStreamAsync for a stream.