Why This Is Useful
This one’s more obvious than most of my videos, so I’ll be quick.
Businesses constantly need to generate a one-off PDF. A list of related records, a summary sheet, a simple quote. And the usual answer is to buy a document generation managed package like Conga Composer, which is a genuinely good product with a lot of work behind it, and also a lot of money for something you needed once.
If all you need is “turn this record into a PDF,” you can do it yourself with a Lightning Web Component and a bit of Visualforce.
Code’s on GitHub.
The Awkward Truth Up Front
Lightning Web Components cannot generate PDFs on their own. There’s no API for it, and the JavaScript libraries that do it are painful to get working under Lightning Web Security.
What Salesforce does have is renderAs="pdf" on Visualforce, which has worked reliably for over a decade.
So the approach is: LWC for the interface, Visualforce for the rendering. Slightly inelegant, completely reliable, and you don’t have to fight the platform.
The Visualforce Page
<apex:page controller="PdfGeneratorController"
renderAs="pdf"
applyHtmlTag="false"
showHeader="false">
<head>
<style type="text/css">
@page { size: A4; margin: 20mm; }
body { font-family: Arial, sans-serif; font-size: 12pt; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 6px; text-align: left; }
</style>
</head>
<body>
<h1>{!account.Name}</h1>
<table>
<tr><th>Name</th><th>Email</th></tr>
<apex:repeat value="{!contacts}" var="cont">
<tr>
<td>{!cont.Name}</td>
<td>{!cont.Email}</td>
</tr>
</apex:repeat>
</table>
</body>
</apex:page>renderAs="pdf" is doing all the work. Salesforce runs the rendered HTML through a PDF engine and serves it back.
That @page CSS rule controls paper size and margins, and it’s the thing people don’t know about. Without it you get whatever the default is and spend an hour wondering why your layout is wrong.
The Controller
public with sharing class PdfGeneratorController
{
public Account account { get; private set; }
public List<Contact> contacts { get; private set; }
public PdfGeneratorController()
{
Id recordId = ApexPages.currentPage().getParameters().get('id');
account = [SELECT Id, Name FROM Account WHERE Id = :recordId];
contacts = [SELECT Id, Name, Email FROM Contact WHERE AccountId = :recordId];
}
}Standard constructor work: read the Id from the URL, query what you need.
The Lightning Web Component
import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class PdfGenerator extends NavigationMixin(LightningElement) {
@api recordId;
handleGeneratePdf() {
this[NavigationMixin.Navigate]({
type: 'standard__webPage',
attributes: {
url: '/apex/PdfGenerator?id=' + this.recordId
}
});
}
}<template>
<lightning-card title="Generate PDF">
<div class="slds-p-around_medium">
<lightning-button
label="Download PDF"
variant="brand"
onclick={handleGeneratePdf}>
</lightning-button>
</div>
</lightning-card>
</template>NavigationMixin is the supported way to send users somewhere from an LWC. Don’t use window.open(), it behaves badly in the Salesforce mobile app and in Experience Cloud.recordId arrives automatically because the component is on a record page, as covered in this quick tip.
Saving It To The Record Instead
Opening it in a tab is fine. Attaching it to the record is often what people actually want:
@AuraEnabled
public static Id savePdfToRecord(Id recordId)
{
PageReference pdfPage = Page.PdfGenerator;
pdfPage.getParameters().put('id', recordId);
Blob pdfBlob = Test.isRunningTest()
? Blob.valueOf('test')
: pdfPage.getContentAsPDF();
ContentVersion cv = new ContentVersion();
cv.Title = 'Account Summary';
cv.PathOnClient = 'AccountSummary.pdf';
cv.VersionData = pdfBlob;
cv.FirstPublishLocationId = recordId;
insert cv;
return cv.Id;
}getContentAsPDF() gives you the PDF as a Blob without the user going anywhere.
Note the Test.isRunningTest() guard. getContentAsPDF() throws in test context, so without that your tests fail and there’s no obvious reason why. That one has cost people entire afternoons.FirstPublishLocationId is what attaches the file to the record. Set that and it shows up in the Files related list automatically.
Things That Will Annoy You
The PDF renderer is old. It does not support flexbox, grid, or most modern CSS. Use tables for layout. Yes, tables. I know. It’s 2026 and we’re laying out with tables again, but that’s what works.
Images need to be publicly accessible or uploaded as static resources. External images generally won’t render.
There’s a 15MB heap limit on generated PDFs. If you’re rendering hundreds of records, paginate or move it to a batch job.
Test your fonts. Anything unusual may not embed properly. Stick to the common ones unless you’ve verified otherwise.
Full details in the Visualforce PDF rendering documentation.
Want a Word document instead? I’ve covered that too, in how to generate a Word document from an LWC.
Get Coding With The Force Merch!!
We now have a redbubble store setup so you can buy cool Coding With The Force merchandise! Please check it out! Every purchase goes to supporting the blog and YouTube channel.
Get Shirts Here!
Get Cups, Artwork, Coffee Cups, Bags, Masks and more here!
Check Out More Coding With The Force Stuff!
If you liked this post make sure to follow us on all our social media outlets to stay as up to date as possible with everything!
Youtube
Patreon
Github
Facebook
Twitter
Instagram
Salesforce Development Books I Recommend
Advanced Apex Programming
Salesforce Lightning Platform Enterprise Architecture
Mastering Salesforce DevOps
Good Non-SF Specific Development Books:
Clean Code
Clean Architecture