Fix blank pages and missing content in HTML to PDF (C#)
A PDF that shows "Loading...", a broken image or a section you can see in the browser but not in the document usually has one of a few causes: the page was captured before its scripts finished, relative links could not be resolved, the page used different CSS for print, or wide content was cut at the right edge. Each has a converter option that fixes it.
Compiled and run: Linux ARM64 · Linux x64 · macOS ARM64 · Windows x64
Build it step by step.
-
1Install the NuGet packages
SelectPdf.Universalis the library. The native package carries the Chromium engine for one platform: add the one for the platform you run on.dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.win-x64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.linux-x64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.linux-arm64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.osx-arm64
-
2Create a console app
Any .NET 8 or .NET 10 project works. To follow along, create a console app, run the install commands above inside its folder, then replace
Program.cswith the code below.dotnet new console -n PdfSample cd PdfSample
-
3Import the namespace
using SelectPdf.Universal;
Everything used here is in
SelectPdf.Universal. -
4A page that fills itself in
// A report whose totals are written by a script 1.5 seconds after the page // has loaded, the way a chart or a fetch() call fills in a page. string html = """ <h1>Quarterly report</h1> <p>Sales for July to September 2026.</p> <div id="totals">Loading totals...</div> <script> setTimeout(() => { document.getElementById("totals").innerHTML = "<h2>Totals</h2><p>Revenue: 1,284,500 EUR</p><p>Orders: 9,312</p>"; }, 1500); </script> """;The totals are written by a script 1.5 seconds after the page has loaded, like a chart, a
fetch()call or a framework that renders on the client. Converted with the default options, the PDF shows "Loading totals...". -
5Wait for the script
HtmlToPdf converter = new HtmlToPdf(); // By default the page is captured as soon as it has loaded, so the PDF // shows "Loading totals...". Wait 3 more seconds for the script to run. converter.Options.ConversionDelay = 3;
By default the conversion starts as soon as the page has loaded.
ConversionDelayis the extra time, in seconds, the converter waits after that before it renders, so 3 seconds covers the 1.5-second script with room to spare. -
6Convert and save
PdfDocument doc = converter.ConvertHtmlString(html); doc.Save("report.pdf"); doc.Close();ConvertHtmlStringrenders the page with the delay and the PDF has the totals.Savewritesreport.pdfandClosereleases the document. -
7Run it
dotnet run
The program writes
report.pdfto the working folder. Without a license key, every page carries a trial watermark; setGlobalProperties.LicenseKeybefore the first conversion to remove it.
The complete Program.cs.
This is the exact file our samples pipeline compiled and ran on Linux ARM64 and Linux x64 and macOS ARM64 and Windows x64 to produce the output on this page.
using SelectPdf.Universal;
// A report whose totals are written by a script 1.5 seconds after the page
// has loaded, the way a chart or a fetch() call fills in a page.
string html = """
<h1>Quarterly report</h1>
<p>Sales for July to September 2026.</p>
<div id="totals">Loading totals...</div>
<script>
setTimeout(() => {
document.getElementById("totals").innerHTML =
"<h2>Totals</h2><p>Revenue: 1,284,500 EUR</p><p>Orders: 9,312</p>";
}, 1500);
</script>
""";
HtmlToPdf converter = new HtmlToPdf();
// By default the page is captured as soon as it has loaded, so the PDF
// shows "Loading totals...". Wait 3 more seconds for the script to run.
converter.Options.ConversionDelay = 3;
PdfDocument doc = converter.ConvertHtmlString(html);
doc.Save("report.pdf");
doc.Close();
Open the project on GitHub → this program and the variations below, ready to dotnet run
The PDF it produces.
Download report.pdf ↓ the file this sample produced, unedited
Variations you will need next.
Each variation is a separate file in the same project, compiled and run with the program above.
Let the page start the conversion
When you cannot know how long the scripts take, set StartupMode to HtmlToPdfStartupMode.Manual and call selectpdf.start() from the page once it is ready. The converter exposes the selectpdf object only inside the conversion, so the typeof check keeps the page working in a browser. If the call never comes, the conversion fails after NavigationTimeout seconds.
using SelectPdf.Universal;
static class StartFromJavaScript
{
public static void Run()
{
// the page tells the converter when it is ready: no fixed delay to guess
HtmlToPdf converter = new HtmlToPdf();
converter.Options.StartupMode = HtmlToPdfStartupMode.Manual;
converter.Options.NavigationTimeout = 30; // fails if selectpdf.start() is never called
PdfDocument doc = converter.ConvertHtmlString("""
<h1>Order summary</h1>
<div id="items">Loading items...</div>
<script>
setTimeout(() => {
document.getElementById("items").innerHTML =
"<p>3 x Laptop stand</p><p>1 x USB-C dock</p>";
if (typeof selectpdf == "object") selectpdf.start();
}, 2000);
</script>
""");
doc.Save("order-summary.pdf");
doc.Close();
}
}
Render with the print styles
The payment terms are hidden on screen and shown by the @media print rules. The converter renders with the screen styles by default, so they are missing from the PDF; CssMediaType = HtmlToPdfCssMediaType.Print applies the print styles instead, and the online-payment line disappears.
using SelectPdf.Universal;
static class PrintStyles
{
public static void Run()
{
// the terms are hidden on screen and shown only by the print styles
HtmlToPdf converter = new HtmlToPdf();
converter.Options.CssMediaType = HtmlToPdfCssMediaType.Print;
PdfDocument doc = converter.ConvertHtmlString("""
<style>
.print-only { display: none; }
@media print {
.print-only { display: block; }
.screen-only { display: none; }
}
</style>
<h1>Invoice #1042</h1>
<p class="screen-only">Pay online with the button below.</p>
<p class="print-only">Payment terms: 30 days from the invoice date.</p>
""");
doc.Save("invoice-print.pdf");
doc.Close();
}
}
Keep wide content instead of cutting it
Six 300-pixel columns make the table wider than the page, which is laid out 1,024 pixels wide (WebPageWidth). By default content past the right edge is cut, as a browser cuts it when it prints, and the Total column is lost. FitWideContent = true lays the page out at the width it needs, up to four times WebPageWidth, and scales the whole page down to fit, so every column is in the PDF, smaller.
using SelectPdf.Universal;
static class WideContent
{
public static void Run()
{
// six 300-pixel columns: wider than the 1,024-pixel web page, so by
// default the Total column is cut, as a browser cuts it when it prints
HtmlToPdf converter = new HtmlToPdf();
converter.Options.FitWideContent = true; // lay out at full width, scale to fit
PdfDocument doc = converter.ConvertHtmlString("""
<style>th { min-width: 300px; }</style>
<h1>Stock by warehouse</h1>
<table border="1" style="border-collapse: collapse">
<tr><th>Item</th><th>Berlin</th><th>Lyon</th>
<th>Milan</th><th>Vienna</th><th>Total</th></tr>
<tr><td>Laptop stand</td><td>120</td><td>85</td>
<td>64</td><td>40</td><td>309</td></tr>
</table>
""");
doc.Save("wide-table.pdf");
doc.Close();
}
}
Common problems and fixes.
Web fonts are missing or the text renders incorrectly
Usually the fonts have not finished loading when the page is captured. Set ConversionDelay (for example 2) and make sure the font files are reachable from the page; WOFF2, WOFF, TTF and OTF fonts are embedded.
Lazy-loaded images or reveal-on-scroll content are missing
LazyImagesLoadingEnabled is on by default and waits LazyImagesLoadingDelay milliseconds per PDF page (40 by default). A short delay can leave out content that loads when it is scrolled into view; raise it. With ManagedContentTrimming set to false, also set WebPageHeight, for example 768.
The PDF shows the login page instead of the content
The converter makes its own request, so it is anonymous. For HTTP or Windows authentication set Options.Authentication.Username and Password; for cookie authentication send the signed-in user's cookie with Options.HttpCookies.Add(name, value).
Conversion error: Navigation timeout
The page took longer than NavigationTimeout (60 seconds by default): raise it, or set RenderPageOnTimeout = true to convert what has rendered. In manual startup mode the same error means selectpdf.start() was never called.
Wide content is cut off at the right edge
Content wider than WebPageWidth (1024 pixels by default) is cut at the right edge. Set FitWideContent = true to lay the page out at the width it needs and scale it down to fit, raise WebPageWidth, or let the page lay the content out narrower, for example with white-space: pre-wrap.
An image is split between two pages
Put break-inside: avoid on the image or on the element that contains it, and the image moves whole to the next PDF page.
Good to know.
- ConversionDelay is additional time, in seconds, that the converter waits after the web page has loaded, for content that appears after the load event. The default is 0. Source
- With StartupMode set to Manual, the conversion starts only when the page calls selectpdf.start(); if it is never called, the conversion times out. Source
- ConvertHtmlString(String, String) takes a base URL: base URL + relative image or CSS URL = full absolute URL. Source
- CssMediaType is Screen by default; set it to Print to render the page with its printer-specific styles. Source
- The converter waits 60 seconds for a page by default; NavigationTimeout changes it, and RenderPageOnTimeout converts what has rendered instead of failing. Source
Questions, answered.
Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.