Add a PDF download to an existing Blazor page, or generate a PDF from a template. This guide shows how to reuse Razor components to generate PDFs on the server with PDFCrowd's HTML to PDF API, then return them as downloads through an HTTP endpoint.
Set up the client
Install the .NET client in your Blazor Web App's server project:
dotnet add package Pdfcrowd.Official
You can run the examples below with the included demo credentials, or replace
them with your PDFCrowd username and API key.
Keep the client and credentials on the server.
Add a PDF download to an existing page
Keep the page's usual rendering, data preparation, and access checks. A download form posts to a server endpoint, which prepares the PDF and returns it to the browser. The page can use static rendering or Interactive Server rendering.
Choose what to send to PDFCrowd:
- Rendered HTML reuses the page's document component and application data.
- The page URL loads the page through its normal address.
Convert rendered HTML
Suppose your existing page at /documents/{id} displays a Document model. Move its
document markup into a reusable component, DocumentContent.razor. For example:
<h1>@Document.Title</h1>
<p>@Document.Summary</p>
@code {
[Parameter, EditorRequired]
public Document Document { get; set; } = default!;
}
Here, Document is your application's model; adapt the fields to your document. Import
your model and component namespaces where needed, including in _Imports.razor.
On the existing page, use that component and add this form outside any other form.
document is the model the page has already loaded and authorized:
<DocumentContent Document="document" />
<form method="post" action="@($"documents/{document.Id}/pdf")" data-enhance="false">
<AntiforgeryToken />
<button type="submit">Download PDF</button>
</form>
The action is relative to the application's existing <base> URL. AntiforgeryToken
adds the hidden request token; keep the standard forms namespace in _Imports.razor.
This is a normal browser POST to an HTTP endpoint. data-enhance="false" lets the browser
handle the download response; no @onsubmit handler is needed.
Create DocumentPdf.razor to wrap the shared content in a complete HTML document:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@Document.Title</title>
@if (!string.IsNullOrWhiteSpace(BaseUrl))
{
<base href="@BaseUrl" />
}
<link rel="stylesheet" href="css/document.css" />
</head>
<body>
<DocumentContent Document="Document" />
</body>
</html>
@code {
[Parameter, EditorRequired]
public Document Document { get; set; } = default!;
[Parameter]
public string? BaseUrl { get; set; }
}
Use your document's stylesheet in place of css/document.css; the
asset configuration below makes relative paths usable.
The PDF contains the shared document content, without the page's navigation or download form.
Keep these document components renderable as static HTML: pass their data through parameters and leave out interactive render modes and JavaScript-dependent content. The surrounding page can remain interactive. The PDF is rendered from freshly loaded data, so unsaved page edits are not included.
Add RazorComponentRenderer.cs to the server project. This helper uses Blazor's
HtmlRenderer to produce an HTML string:
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; // Standard C# and ASP.NET Core dependency injection/logging imports are omitted. public sealed class RazorComponentRenderer( IServiceProvider services, ILoggerFactory loggerFactory) { public async Task<string> RenderAsync<TComponent>(ParameterView parameters) where TComponent : IComponent { await using var renderer = new HtmlRenderer(services, loggerFactory); return await renderer.Dispatcher.InvokeAsync(async () => { var output = await renderer.RenderComponentAsync<TComponent>(parameters); return output.ToHtmlString(); }); } }
Register the helper in Program.cs before builder.Build():
builder.Services.AddScoped<RazorComponentRenderer>();
Keep the application's existing authentication, authorization, and UseAntiforgery()
middleware. Add the following endpoint before app.Run(), with the imports at the top
of Program.cs:
using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Components; // Standard C# and ASP.NET Core HTTP imports are omitted. app.MapPost("/documents/{id:int}/pdf", async ( int id, HttpContext context, IAntiforgery antiforgery, DocumentService documents, RazorComponentRenderer renderer, IConfiguration configuration) => { if (!await antiforgery.IsRequestValidAsync(context)) return Results.BadRequest(); var document = await documents.GetForUserAsync(id, context.User); if (document is null) return Results.NotFound(); var parameters = ParameterView.FromDictionary(new Dictionary<string, object?> { [nameof(DocumentPdf.Document)] = document, [nameof(DocumentPdf.BaseUrl)] = configuration["Pdf:BaseUrl"] }); var html = await renderer.RenderAsync<DocumentPdf>(parameters); var client = new pdfcrowd.HtmlToPdfClient("demo", "demo"); client.setContentViewportWidth("balanced"); byte[] pdf = client.convertString(html); return Results.File(pdf, "application/pdf", $"document-{id}.pdf"); }).RequireAuthorization();
DocumentService represents your application's data service, registered with dependency
injection. Replace GetForUserAsync(id, context.User) with your lookup and permission checks;
here, it returns null if the document is missing or the user cannot access it.
RequireAuthorization() requires a signed-in user; apply the page's named policy here too
if it uses one. The endpoint checks access again because it receives a separate HTTP request.
Normal page requests still display HTML. Only a valid POST to the PDF endpoint starts
conversion. The helper waits for component rendering, including asynchronous initialization,
then the SDK converts the HTML. The complete PDF is held in memory as byte[] before the
download response is created. The SDK call is synchronous and runs in the HTTP endpoint.
Rendering and conversion exceptions propagate to your application's normal error handling.
Return a generic error to the browser; see
SDK error handling for details.
To display the PDF in the browser, omit the filename from Results.File.
Set page size,
margins, or other
PDF options before convertString().
Make CSS and images available
PDFCrowd needs reachable URLs for stylesheets, images, and fonts referenced by the HTML.
Absolute asset URLs, such as https://your-site.example/css/document.css, need no base URL.
If your components use CSS isolation, include the application's compiled .styles.css
bundle in DocumentPdf.razor as well.
For relative paths, set the public site or application-directory URL in your configuration,
for example in appsettings.json:
{ "Pdf": { "BaseUrl": "https://your-site.example/" } }
Replace https://your-site.example/ with your website's public URL. The endpoint passes
this value to DocumentPdf, which adds one escaped <base> before its stylesheet links.
It does not change the live application's base URL.
For example, css/document.css resolves to https://your-site.example/css/document.css.
If the base is https://your-site.example/app/, it resolves to
https://your-site.example/app/css/document.css. Keep the trailing slash for a directory;
a leading slash in the asset path instead resolves from the site's root. The base also
affects hyperlinks, so avoid adding another <base> to the PDF template.
PDFCrowd cannot reach assets on your development machine through localhost or 127.0.0.1.
For local assets, you can instead package the HTML and assets in a ZIP archive.
Keep paths relative to the archive's folders and omit the website <base> for that approach.
Convert the page URL instead
Instead of rendering the document component, you can ask PDFCrowd to open the existing
HTML page. Keep the endpoint's access checks, client setup, and response. Replace the
parameter preparation, renderer.RenderAsync(...), and convertString() calls with:
var baseUrl = new Uri(configuration["Pdf:BaseUrl"]!); var pageUrl = new Uri(baseUrl, $"documents/{id}"); byte[] pdf = client.convertUrl(pageUrl.AbsoluteUri);
Set Pdf:BaseUrl to your public application URL, including its directory and trailing slash.
Adapt documents/{id} to the page's HTML route; do not use the /pdf endpoint.
PDFCrowd makes a separate GET, so the server must be able to serve that page while the
download request waits. The URL must be reachable from PDFCrowd.
The separate request does not inherit the user's authentication cookie or current Blazor session. It loads a fresh page, including whatever content that page prepares on load; unsaved edits and interactions from the user's open page are not included. For protected pages, configure cookies or HTTP authentication as appropriate, or use rendered HTML with explicitly supplied data.
Generate a PDF from a template
You can also use a Razor component as a document template. This helper reuses
RazorComponentRenderer and returns PDF bytes to its caller:
using Microsoft.AspNetCore.Components; // Standard C# imports are omitted. public sealed class PdfRenderer(RazorComponentRenderer renderer) { public async Task<byte[]> RenderAsync<TComponent>(ParameterView parameters) where TComponent : IComponent { var html = await renderer.RenderAsync<TComponent>(parameters); var client = new pdfcrowd.HtmlToPdfClient("demo", "demo"); client.setContentViewportWidth("balanced"); return client.convertString(html); } }
Register it alongside RazorComponentRenderer:
builder.Services.AddScoped<PdfRenderer>();
For example, inject PdfRenderer and IConfiguration into your calling code, then pass
an already-authorized document and the same asset configuration:
var parameters = ParameterView.FromDictionary(new Dictionary<string, object?> { [nameof(DocumentPdf.Document)] = document, [nameof(DocumentPdf.BaseUrl)] = configuration["Pdf:BaseUrl"] }); byte[] pdf = await pdfRenderer.RenderAsync<DocumentPdf>(parameters);
Use the resulting bytes for a download, storage, or email; errors propagate to the caller.
The renderer supplies application services, but does not recreate the page's interactive
state or cascading parameters. Pass the document's data explicitly and use components
that render without a logged-in browser, JavaScript interop, or request-dependent helpers.
Outside an HTTP request, resolve PdfRenderer within an application service scope and
keep that scope alive until the awaited call finishes.
For more conversion inputs and PDF settings, see the .NET examples and API reference.