Generate PDF files using an html template and Playwright

  • Gérald Barré

There are many ways to generate PDF files. You can use a PDF library, but generating a PDF manually is not trivial. You can use a report library, but they are often paid solution and not trivial to use for simple needs.

In this post, we'll use an html template and Playwright to generate a PDF file. Using a browser to convert an html file to a PDF file is very convenient as it allows to use all html/CSS features. So, you can use CSS frameworks, custom fonts, etc. It also support multiple-page documents.

#Creating the html file

For the template, I use Scriban as it is very simple scripting language to use and powerful enough for my use case. First you need to add a reference to the NuGet package:

csproj (MSBuild project file)
    <PackageReference Include="Scriban" Version="4.0.1" />

Then, you need to create an html file to store your template:

<!DOCTYPE html>
<head><link rel="stylesheet" href="invoice.css"></head>
<body style="padding: 3rem">
    Awesome company<br />
    7026 Hunters Creek Dr<br />

    <h2 style="margin-top: 3rem">Bill to</h2>
    {{ | html.escape }}<br />
    {{ invoice.customer.address | html.escape }}<br />

    <div style="margin-top: 3rem">
        Invoice No: #{{ }}<br />
        Date: #{{ invoice.created_at }}

    <table class="table">
                <th>Item Code</th>
                <th>Unit Price</th>
                <th>Total Price</th>

        {{ for order_line in invoice.order_lines }}
            <td>{{ order_line.item_code | html.escape }}</td>
            <td>{{ order_line.description | html.escape }}</td>
            <td class="text-end">${{ order_line.quantity }}</td>
            <td class="text-end">${{ order_line.unit_price | math.format "F2" }}</td>
            <td class="text-end">${{ order_line.total_price | math.format "F2" }}</td>
        {{ end }}

                <td class="text-end" colspan="4"><strong>Total:</strong></td>
                <td class="text-end">${{ invoice.total_price | math.format "F2" }}</td>

Finally, you can load your template and render it:

// using Scriban;

// Load the template
// perf: you should parse the template once and reuse it for all rendering
var templateContent = File.ReadAllText("template.html");
var template = Template.Parse(templateContent);

// note: scriban convert names to snake case (OrderId => order_id)
// In your template, you must use {{ invoice.order_id }} instead of {{ Invoice.OrderId }}
var templateData = new { Invoice = LoadOrder(orderId) };
var pageContent = template.Render(templateData);

You now have an html file that you need to convert to PDF.

#Converting HTML to PDF

Playwright is a tool to automate a browser. You can navigate to pages, get element properties, execute javascript, take screenshots, record videos, etc. You can also use it to export a page to PDF or an image file.

First, you need to install the Playwright NuGet package and install download necessary browsers:

dotnet add package Microsoft.Playwright
dotnet build
playwright install

Then, you can open the page and export the page to PDF:

// using Microsoft.Playwright;

// In my case, I don't want to create a file, so I use a data url to open the html file
var dataUrl = "data:text/html;base64," + Convert.ToBase64String(Encoding.UTF8.GetBytes(pageContent));

// Open the page and wait for resources to be loaded (useful if you embed images or external stylesheets)
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
    Headless = true,

await using var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.GotoAsync(dataUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });

// Generate the PDF
var output = await page.PdfAsync(new PagePdfOptions
    Format = "A4", // or "letter"
    Landscape = false,

// Save the pdf to the disk
await File.WriteAllBytesAsync("output.pdf", output);

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub