Save Any Webpage as a PDF Programmatically: How URL-to-PDF Rendering Works (and Why Pages Get Cut Off)

URL-to-PDF is not a screenshot. When Chromium prints a page, it re-lays the entire layout at paper width, switches to @media print rules, and paginates by…

URL-to-PDF is not a screenshot. When Chromium prints a page, it re-lays the entire layout at paper width, switches to @media print rules, and paginates by filling each page and cutting wherever it runs out of room. That is why your PDFs get sliced in half, lose content, or look nothing like the live site.

Why PDF output looks different from the live page

Most developers expect URL-to-PDF to capture what they see in Chrome. The browser does something else entirely. It runs a separate layout pass optimized for paged media, not screens.

Three things change during that pass:

  • Paper width replaces screen width. A responsive site that shows a three-column grid at 1440px may collapse to a single column at A4 width [1].
  • @media print CSS rules replace screen rules. Colors invert, navigation hides, fonts swap, and margins appear [1][2].
  • Pagination fills pages top-to-bottom and cuts at boundaries. Content that flowed continuously in a viewport now breaks across discrete pages, and Chromium decides where to cut unless you tell it otherwise.

Geometry: when the layout pass clips your content

Elements built for screen width break when squeezed into paper dimensions. A 100vh hero section becomes one page tall and pushes everything else down. A sticky sidebar meant for scrolling becomes a fixed block that overlaps subsequent pages. overflow: hidden on a container truncates content that would have scrolled in the browser.

The content is still in the DOM. It simply disappears from the printed layout because the geometry no longer fits.

MarkupGo's emulatedMediaType: 'screen' preserves screen styling when you need the visual layout to match the live page rather than the print version [1][2]. This is useful for capturing dashboards or marketing pages where the screen design is what you want archived. For documents that should paginate properly, emulatedMediaType: 'print' applies the print stylesheet and lets you control the result with @page rules.

Timing: why lazy content and fonts cause flakiness

Modern pages rarely render completely on DOMContentLoaded. Images lazy-load as you scroll. SPAs mount components after hydration. Infinite scroll feeds append content when the user nears the bottom. Fonts swap from fallback to final glyphs, shifting line heights and moving pagination boundaries.

Puppeteer's page.pdf() only works in headless Chromium, and printBackground defaults to false, so backgrounds must be explicitly enabled [2]. A page that renders as 12 pages on one run may render as 13 on the next because a webfont arrived slightly later and pushed a heading onto the next page. A page that appears on a long document may simply disappear on the next render because the font race went differently.

MarkupGo provides waitDelay for fixed delays and waitForExpression for deterministic conditions like document.fonts.ready [1]. Setting waitForExpression: 'document.fonts.ready' blocks rendering until all font promises resolve, eliminating the line-height drift that causes unstable pagination.

Fragmentation: how Chromium slices tables and code blocks

Chromium's default fragmentation behavior is brutally simple: fill the current page, and cut wherever the next content does not fit [3]. Code blocks split mid-line. Tables sever across rows. Headings sit alone at the bottom of a page while their content starts on the next.

CSS protects against this, but most sites do not ship with print fragmentation styles. Four properties cover the common cases:

  • break-inside: avoid on blocks you want to keep together
  • break-after: avoid on headings to keep them with the following content
  • orphans: 3 and widows: 3 to prevent short lines isolated at page boundaries
  • display: table-header-group on thead so table headers repeat after each break

A test document with these fixes grew from 17 pages to 19 pages, about 12% longer, because content was pushed to start on fresh pages rather than splitting [3]. break-inside: avoid cannot save a block taller than one page, so a 600-pixel code block on A4 will still need to start near the top or it will overflow.

Rendering an external URL to PDF with one request

MarkupGo's /pdf endpoint accepts source.type: 'url' alongside html, template, and markdown sources [1].

For geometry, emulatedMediaType chooses screen or print styling. properties.landscape, properties.size, and properties.margins set paper dimensions. preferCssPageSize lets a CSS @page rule win over explicit dimensions [1].

For timing, waitDelay and waitForExpression control when rendering begins [1].

For fragmentation, properties.margins creates breathing room at page boundaries, and properties.nativePageRanges extracts specific pages if the full document is not needed [1].

Additional controls matter for untrusted URLs. cookies and extraHttpHeaders authenticate into protected pages. failOnHttpStatusCodes aborts on 404 or 500 responses rather than rendering an error page [1]. userAgent spoofs a desktop browser when sites gate content by client.

Custom headers and footers inject page numbers, dates, titles, and URLs into every page [1]. They must be complete HTML documents with inline styles, base64-encoded images, and -webkit-print-color-adjust: exact for background colors. JavaScript does not execute in headers or footers, and assets do not load — everything must be self-contained.

fetch('/api/v1/pdf', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    source: {
      type: 'url',
      data: 'https://example.com/report'
    },
    options: {
      emulatedMediaType: 'screen',
      waitForExpression: 'document.fonts.ready',
      properties: {
        printBackground: true,
        margins: {
          top: 10,
          bottom: 10,
          left: 10,
          right: 10
        }
      },
      header: '<html><head><style>body{font-size:10px;margin:auto 20px;}</style></head><body><span class="title"></span></body></html>',
      footer: '<html><head><style>body{font-size:10px;margin:auto 20px;}</style></head><body><span class="pageNumber"></span> of <span class="totalPages"></span></body></html>'
    }
  })
})

No-code and automation alternatives

MarkupGo's Magic Template URL feature generates PDFs from a template via a plain URL with query parameters [4]. Enable Magic URL on a template, then request https://render.markupgo.com/template/{id}.pdf?customer=Acme&amount=500 to receive a rendered PDF.

The same 15-day caching rule applies: reuse the same URL within 15 days and the cached file serves without consuming credits [4]. After 15 days, the cache clears and the next request regenerates.

This works from any platform that makes HTTP requests. The founder specifically notes Make, Zapier, and n8n as compatible platforms, and recommends feeding the documentation link to an LLM for tailored integration code [5].

When to use URL-to-PDF versus URL-to-image

PDF preserves text selection, searchability, and precise pagination. It carries metadata, headers, footers, and accessibility tags [1]. ScreenshotEngine positions PDF export as a way to store a complete, text-searchable visual record of a webpage, perfect for legal and compliance auditing. MarkupGo adds PDF/UA accessibility support and automatic deletion via expiration settings ranging from 60 seconds to 90 days [1].

Image is for sharing, OG cards, and social media previews where a single visual asset loads faster than a document. Image generation offers no margin, header, footer, or orientation controls because none are needed for a flat raster.

html2img's URL-to-PDF renders in real Chrome and auto-paginates to A4 portrait, but offers no page size, orientation, margin, header, or footer controls. It applies screen CSS and cannot render pages behind logins because it lacks cookie or header injection. Pricing starts at $9 for 1,000 credits with 50 free renders. MarkupGo's free tier provides 100 credits monthly with no credit card required, and paid plans run $29 for 2,000 credits, $49 for 10,000, or $99 for 20,000 with unlimited templates [6].

Pricing and getting started

MarkupGo's free trial offers 100 credits monthly with no credit card required [6]. Paid plans scale through Lite at $29 per month for 2,000 credits and 5 templates, Plus at $49 for 10,000 credits and 30 templates, and Pro at $99 for 20,000 credits with unlimited templates [6].

Start with the free tier to test URL-to-PDF rendering against your target pages. Use emulatedMediaType: 'screen' when the visual fidelity matters, waitForExpression: 'document.fonts.ready' when fonts destabilize output, and break-inside: avoid CSS when content splits where it should not.

Sources

  1. markupgo.com
  2. testmuai.com
  3. dev.to
  4. Magic Template URL (markupgo.com)
  5. How do I convert a Web Page or URL to PDF without being a (appsumo.com)
  6. markupgo.com