SfSkills Domains Docs GitHub

Home / apex / pdf-generation-patterns

pdf-generation-patterns

Generating PDF documents from Salesforce using Visualforce renderAs='pdf', PageReference.getContentAsPDF(), and ContentVersion storage. Covers the Flying Saucer rendering engine constraints, inline CSS requirements, server-side data loading, LWC PDF limitations, and programmatic PDF attachment to records. NOT for Quote PDF template customization (use apex/quote-pdf-customization). NOT for OmniStudio DocGen document generation. NOT for Salesforce Reports exported as PDF.

Open the skill folder on GitHub


PDF Generation Patterns

This skill activates when a Salesforce implementation needs to generate PDF documents — either rendered on demand from a Visualforce page or programmatically stored as Files (ContentVersion) on Salesforce records. It covers the complete lifecycle: Flying Saucer engine constraints, CSS and JavaScript restrictions, Apex controller design, PageReference.getContentAsPDF() usage, ContentVersion storage, LWC limitations, and async generation patterns.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Concept 1: Visualforce renderAs='pdf' and the Flying Saucer Engine

Setting renderAs="pdf" on <apex:page> directs Salesforce to pipe the rendered HTML through the Flying Saucer library (an iText-based HTML-to-PDF converter) before sending the response. Consequences that cause the majority of production bugs:

Concept 2: Server-Side Data Loading in the Apex Controller

Because the PDF renderer has no JavaScript engine, every piece of data shown in the PDF must be resolved in Apex before the page HTML is produced. Specific requirements:

Concept 3: PageReference.getContentAsPDF() for Programmatic PDF Generation

PageReference.getContentAsPDF() renders a Visualforce page to a raw PDF Blob from within Apex server-side code, enabling automated PDF creation without user interaction. This method:

Concept 4: LWC PDF Limitation and the VF Wrapper Pattern

LWC does not support renderAs="pdf" and cannot produce a PDF natively. The documented Salesforce pattern for LWC-initiated PDF generation is:

  1. Author a Visualforce page as the PDF template with a server-side Apex controller.
  2. Either navigate to the VF page URL from the LWC (using NavigationMixin.Navigate with type: 'standard__webPage') for a browser-download flow, or invoke a headless Apex method from the LWC that enqueues a Queueable to generate and store the PDF as a ContentVersion.

Common Patterns

Pattern 1: On-Demand PDF Rendered Directly in Browser

When to use: A user opens a record page, clicks "Download PDF," and the PDF downloads directly — no file storage needed.

How it works:

  1. Create a Visualforce page: <apex:page controller="InvoicePdfController" renderAs="pdf" showHeader="false" sidebar="false">.
  2. In InvoicePdfController, query all required data in the constructor using WITH USER_MODE.
  3. Build the logo absolute URL in the constructor: logoUrl = URL.getSalesforceBaseUrl().toExternalForm() + '/resource/' + RESOURCE_ID + '/logo.png'; or embed as base64.
  4. Use inline <style> with CSS 2.1 table layout for columns. No <script> tags.
  5. Expose the page via a button on the record page, passing ?id={!recordId} as a parameter.

Why not the alternative: Using {!$Resource.Logo} directly in an <img src> produces a relative URL the renderer cannot follow, resulting in a broken image. Loading data via AJAX/JS fails silently — the table renders empty.

Pattern 2: Programmatic PDF Attachment via Queueable

When to use: A PDF must be automatically generated and stored as a File on a record when triggered by a process (Flow, trigger, or platform event) — no user interaction.

How it works:

  1. An Apex trigger or Flow invokes a Queueable class (PdfAttachmentJob) that implements Database.AllowsCallouts, passing the source record Id.
  2. Inside execute(): build a PageReference to the VF page with ?id=<recordId>, call .getContentAsPDF(), check for null, create a ContentVersion with VersionData = blob, then link it via ContentDocumentLink with LinkedEntityId = recordId.
  3. Null-check pattern: Blob pdf = pageRef.getContentAsPDF(); if (pdf == null) { /* log and return */ }.

Why not the alternative: Calling getContentAsPDF() synchronously in a trigger violates the callout-after-DML restriction and throws a System.CalloutException: Callout from triggers are currently not supported. The async Queueable pattern is the only safe path.

Pattern 3: LWC-Initiated PDF Generation

When to use: An LWC component on the record page has a "Generate Report" button and the PDF must be saved to the record's Files and surfaced back in the UI.

How it works:

  1. LWC calls an @AuraEnabled Apex method that enqueues PdfAttachmentJob (Pattern 2).
  2. The LWC displays a spinner while the job completes. On completion, it refreshes the Files related list using refreshApex or LMS.
  3. Alternatively, for a synchronous browser-download flow, the LWC uses NavigationMixin.Navigate to open the VF page URL in a new tab.

Why not the alternative: Attempting to generate the PDF entirely in the LWC JavaScript layer is not possible — there is no Salesforce-native PDF library available in client-side JS. Third-party client-side libraries (jsPDF, etc.) are unofficial, unsupported, and unreliable for complex layouts.


Decision Guidance

SituationRecommended ApproachReason
User-triggered browser downloadVF page with renderAs='pdf', opened via button URLSimplest path; no async complexity
Auto-attach PDF on record changeQueueable implementing Database.AllowsCalloutsCallouts prohibited in trigger context
LWC button → save PDF to FilesLWC calls @AuraEnabled method → enqueues QueueableLWC cannot render PDF natively
Logo not appearing in PDF outputAbsolute URL in Apex or base64 data URI in CSSRenderer cannot follow relative $Resource paths
CSS layout broken in PDFReplace Flexbox/Grid with CSS 2.1 table layoutFlying Saucer supports CSS 2.1 only
Bulk generation (100+ records)Batch Apex with scope=1, each scope generates one PDFgetContentAsPDF() counts against 100-callout limit
Quote-specific PDFUse apex/quote-pdf-customization skill insteadQuote line item and template concerns handled separately

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Clarify the trigger and delivery mechanism — Determine whether the PDF is user-initiated (browser download) or system-initiated (attached to a record). Identify the source record type and the data required. Confirm whether an LWC is involved.
  2. Design the Apex controller — Write a custom controller (not a standard controller extension unless the object is natively supported). Load all required data in the constructor with WITH USER_MODE SOQL. Build all computed values (logo URL, conditional flags, formatted dates) as server-side properties.
  3. Author the Visualforce page — Set renderAs="pdf" showHeader="false" sidebar="false" on <apex:page>. Include no <script> tags. Use <style> blocks with CSS 2.1 table-based layout only. Reference logos via absolute URL or base64. Use rendered="{!property}" for conditional sections.
  4. Test the VF page iteratively — Open the VF page URL in a browser with ?id=<recordId>. Toggle renderAs="pdf" on and off to compare HTML vs. PDF output. Confirm logo visibility, column alignment, and page breaks.
  5. Implement programmatic generation if needed — Build a Queueable implementing Database.AllowsCallouts. In execute(): call getContentAsPDF(), null-check the blob, insert a ContentVersion (with PathOnClient, Title, VersionData), query the resulting ContentDocumentId, then insert a ContentDocumentLink to the source record.
  6. Wire the delivery path — For trigger/Flow invocation: enqueue the Queueable from a trigger handler or an Apex action in Flow. For LWC: expose the enqueue call via an @AuraEnabled method and handle spinner/refresh in the component.
  7. Validate security — Run the full flow as a non-admin user with minimum read access. Confirm no FLS violations in debug logs. Confirm ContentDocumentLink ShareType is set appropriately ('V' for Viewer, 'I' for Inferred).

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. JavaScript is silently ignored by the PDF renderer — The Flying Saucer engine has no JS execution context. Scripts and any layout they produce are absent in the PDF with no error. All layout and data must be resolved server-side.
  2. External CDN stylesheets are not fetched — A <link> to an external stylesheet (Bootstrap, Tailwind, any CDN URL) is silently skipped by the renderer. CSS must be in an inline <style> block or a Salesforce-hosted static resource.
  3. {!$Resource.LogoName} in <img src> produces a broken image — The expression resolves to a relative URL. The renderer makes its own HTTP request and cannot resolve it. Build the absolute URL in Apex using URL.getSalesforceBaseUrl() or embed as base64.
  4. getContentAsPDF() returns null on VF page error — not an exception — If the VF page throws, the method silently returns null. Omitting a null check leads to a NullPointerException when reading the blob or inserts a 0-byte ContentVersion.
  5. Callout-after-DML restriction prevents use in triggers — If any DML occurred before getContentAsPDF() in the same transaction, a CalloutException is thrown. Always call it from a Queueable or @future(callout=true) method.

Output Artifacts

ArtifactDescription
Visualforce page.page file with renderAs="pdf", CSS 2.1 table layout, no script tags, conditional rendered attributes
Apex controller classLoads all data server-side, builds absolute logo URL, exposes Boolean flags for conditional sections
Queueable PDF attachment classCalls getContentAsPDF(), null-checks blob, inserts ContentVersion + ContentDocumentLink
LWC wiring (if applicable)@AuraEnabled Apex method + LWC spinner/refresh pattern
Checker script outputIssues list from check_pdf_generation_patterns.py

Package files