WelisaWelisa DocGen
v1.1.0 welisa.com

HTML templates#

HTML templates let you author in any tool that produces HTML — Google Docs is the flagship workflow — and render to PDF. Every merge tag that works in Word templates works identically here: {Name}, loops, conditionals, aggregates, images, {Today}, charts, signatures.

Why use them? Word locks authors into Microsoft Word. HTML lets content teams design where they already work (Google Docs, Notion, an AI assistant, Apple Pages, any rich-text editor that emits HTML) and gives you full CSS control over the PDF — within the engine's rules below.

Tip

HTML or Canvas? An HTML template describes a flowing document and lets the engine lay it out — right when the content drives the shape, and the only choice if you author elsewhere. A Canvas template positions blocks to the inch. You are not locked in: Import HTML on the Canvas designer converts an HTML document onto an artboard.

Authoring in Google Docs#

  1. Design the document in Google Docs — headings, tables, images, colours, fonts.
  2. Add merge tags as plain text: {Name}, {Account.Name}, {Amount:currency}, loops like {#Contacts}…{/Contacts}. Full syntax: Merge tag reference.
  3. File → Download → Web Page (.html, zipped). Google Docs produces a .zip with your HTML plus an images/ folder.
  4. In the Command Hub, create a template with Type = HTML (output is forced to PDF) and upload the .zip.
  5. The zip is unpacked in your browser: each image is saved as a file linked to the template and the HTML's <img src="images/…"> references are rewritten to Salesforce file URLs the PDF engine resolves natively. The zip itself is never uploaded, so the org's file-type restrictions never see it.
  6. Save as New Version — the template is live.

Other authoring tools#

  • Notion / Confluence — export the page as HTML.
  • An AI assistant — ask for HTML (use the prompt below) and save it to a .html file, or paste it straight into the wizard.
  • Apple Pages — File → Export To → HTML.
  • Hand-written HTML — any text editor.

Inline <img src="data:image/…"> images (common in Notion, AI and rich-text paste output) are detected on save — upload, template-bundle import or AI generation alike — and extracted to files with the src rewritten, because the PDF engine cannot decode data URIs itself.

CSS rules — what works, what does not#

PDF rendering goes through Salesforce's Blob.toPdf(), which is essentially a CSS 2.1 renderer with a small CSS 3 subset. Modern layout properties are silently ignored: the page still renders, but the layout collapses to default block flow with no error message.

UseDon't useReplacement
<table> for side-by-side layoutdisplay: flex, display: gridOne <table> with one <tr>; columns become <td>s
Solid background-colorlinear-gradient(…), radial-gradient(…)Pick the dominant colour
padding, margingapPadding on cells, margin on blocks
Fixed width/height in pt/in/pxcalc(…), CSS variablesCompute the literal value
font-size in ptrem, em on a non-default rootPoints are most predictable for print
border — incl. dashed and dottedborder-radius, box-shadow, text-shadowCorners stay square; see below
Hex colours (#eaf2fb)rgba(…), hsla(…)Pre-compute the tint as flat hex
Full-strength colouropacityA lighter hex instead
:nth-child(even) zebra striping:has(…), :is(…), container queriesnth-child and nth-of-type are supported
Table-based multi-column layoutscolumn-count, columnsTables work everywhere
text-align, vertical-align on <td>place-items, align-selfCell alignment

Fonts. Only Helvetica (sans-serif), Times (serif), Courier (monospace) and Arial Unicode MS exist in the PDF engine. @font-face is not supported. Symbols such as ✓ ✔ ☑ ☐ ★ → and any CJK, Greek, Cyrillic or Hebrew text render as nothing at all under the default fonts — set font-family: 'Arial Unicode MS' on them explicitly. (Arial Unicode MS has no bold face.)

Rounded corners, shadows and tints — the honest answer#

There are no rounded corners. border-radius is ignored in every form: on a <div>, a <td>, a <table> with border-collapse: separate, the four-value shorthand, and the engine's own -fs-border-radius. Boxes render square. This has been measured, not inferred.

What you can use for visual interest, all confirmed working: border: 2pt dashed #2b6cb0 and border: 2pt dotted #b02b2b (the only border decorations available); solid fills, contrasting borders and zebra striping; a background image if you truly need a rounded shape.

Two failure modes that look alike and are not:

  • linear-gradient(…) is discarded when the stylesheet is parsed; the cascade falls back to any solid background set earlier. Harmless.
  • rgba(…) parses, resolves to nothing, and the background disappears entirely — a tinted panel renders invisible. Compute the tint and write flat hex.

opacity, box-shadow, transform, calc() and outline are all ignored too.

Long values overflow a table cell — and no CSS fixes it#

A value with no spaces (an external Id, a URL, a long product code) runs straight through the cell border. The engine cannot break an unbroken run of characters. Measured across every candidate:

TechniqueResult
word-wrap: break-wordignored (the engine already applies it to every cell)
overflow-wrap: break-word, word-break: break-allignored
table-layout: fixedthe column stops growing, so the text overflows the border instead
&#8203; zero-width spaceignored
&shy; soft hyphenbreaks, but prints visible hyphens into the value — corrupts data
<wbr/>works — breaks cleanly, value unchanged

So the only correct fix for static text is a <wbr/> at each allowed break point (ORD-2026<wbr/>-000148<wbr/>-REV3). For merged values you cannot pre-break, keep the column wide enough, put the value on its own full-width row beneath its label, or make it breakable at the source with a formula field that inserts a space every N characters. See Troubleshooting.

How to draw a circle (bullets, status dots)#

Since border-radius does nothing, a circle has to be a character: &#8226; • small filled dot, &#9679; ● filled circle, &#9675; ○ hollow circle, &#9711; ◯ large ring, &#176; ° small ring. Size with font-size, colour with color:

<span style="font-size: 14pt; color: #184d47;">&#9679;</span> Delivered

Never use ZapfDingbats or Symbol for this: those fonts are absent, so the letter falls back to a serif face and prints a literal "l" or "m".

Positioning#

position: absolute, relative, fixed and float all work and are exact against declared inches, with two limits: an absolutely positioned box with no explicit height grows to fit merged content (pin a height and the content overflows visibly instead), and nothing beyond page 1 renders for absolute layout — a box at top: 10.5in on Letter is dropped. For multi-page positioned documents use a Canvas template with one artboard per page.

Page setup — @page, don't double-declare#

Two ways to control page size, margins and orientation:

  1. Template fields — Page Size, Page Orientation, Page Margins, Custom Margins on the template record. The engine wraps your HTML with a <style> block declaring @page from them.
  2. Source CSS — your HTML's own <style> declares @page { size: … }.

Pick one. If both are set, two <style> blocks declare @page, the cascade is non-deterministic and dimensions come out wrong. Leave the template fields blank when your source already specifies @page. Google Docs sometimes injects an @page block on export.

@page { size: 8.27in 11.69in; margin: 1.5cm; }   /* A4 portrait */
@page { size: 8.5in 11in; margin: 0.6in; }       /* US Letter */

Do not include @media queries — the engine ignores them.

Two optional fields on the template's Header / Footer tab, applied to any template type's PDF output (a Word → PDF template with Header HTML set gets it too):

  • Header HTML — rendered in the top page margin of every page.
  • Footer HTML — rendered in the bottom page margin of every page.

Each has a rich-text editor with a Show HTML toggle for raw markup. Every merge tag that works in the body works here, plus {%asset:key} inside an <img src>.

Page numbers#

Put {PageNumber} and {TotalPages} in the Header or Footer HTML; they compile to CSS page counters inside the PDF's margin boxes, so "Page 3 of 17" is right on every page.

<div style="text-align:center; font-size:9pt; color:#888;">Page {PageNumber} of {TotalPages}</div>

Engine limitation: page counters only resolve inside @page rules. When a header or footer contains counter tokens, that margin is rendered as plain text (no images or rich formatting). A header without counters stays rich HTML. Practical pattern: logo in the header, page count in the footer.

Images#

Three ways to get images into an HTML template:

  1. Google Docs zip — images bundled in the .zip are extracted automatically.
  2. Inline data URIs — <img src="data:image/png;base64,…"> is scanned on save and each image becomes a file with the src rewritten.
  3. Merge tags — {%Image:N} (the Nth image attached to the record), {%FieldName} (a file Id stored in a field), {%asset:key} (a shared asset). Each tag is the image: it emits its own <img> — never wrap a tag inside <img src="…">. Sizing rules: Merge tag reference.

The PDF engine can only fetch images through relative Salesforce file URLs; it cannot reach arbitrary HTTPS URLs. Images that are not in the zip, a data URI or a merge tag render as broken squares.

Loops in tables#

Put the loop tags inside the first and last cell of the row you want repeated. The engine expands the whole <tr> once per record — the same pattern Word (<w:tr>) and Excel (<row>) use.

<table>
  <thead><tr><th>Product</th><th>Qty</th><th>Amount</th></tr></thead>
  <tbody>
    <tr>
      <td>{#OpportunityLineItems}{Product2.Name}</td>
      <td>{Quantity}</td>
      <td>{TotalPrice:currency:EUR:nl_NL}{/OpportunityLineItems}</td>
    </tr>
  </tbody>
</table>

Do not put a loop tag on its own line between rows or wrap the row from outside ({#Rel}<tr>…</tr>{/Rel}): text placed directly inside <table> is foster-parented out of the table by the HTML parser and the loop breaks (you see the tag text floating above the table). Keeping the tags in cells also makes them editable pills in the Visual Designer. <li> items auto-expand the same way. A real <thead> repeats at the top of each page section of a long table.

Signatures, barcodes and charts in HTML#

  • Signatures — fully supported, and HTML is the recommended format for signature templates: the stamp card has the most room to breathe. {@Signature_Role:Order:Type} on its own line or in its own cell. See E-signatures.
  • Barcodes / QR — {*Field:qr} and {*Field:code128} render as crisp CSS in the PDF.
  • Charts — {Chart:…} renders as an image. In HTML → PDF, use the CSS-bar styles (bar, pivot, clustered, stacked); column, pie, donut, line and area need inline SVG, which the PDF engine drops. See Charts.

Very large datasets#

HTML templates work with every generation path: single, bulk (individual or merged) and the automatic large-dataset path for child relationships of 2,000 rows and more. On that path, outside the large loop only these tag families resolve: plain field and parent tags with format suffixes, {Today}/{Now}, {RunningUser.X}, aggregates, {#ChartBucket}, {%Image:N} and {%asset:…}. Conditionals, secondary child loops, barcodes, {%FieldName} image tags and signature or form-field tags print as raw text at the parent level. Keep the parent shell of a 2,000+-row HTML template to the supported tags; Word templates have no such restriction.

Skeleton template#

A minimal, engine-clean starting point: side-by-side header, two-column "for/from" block, line-item loop, totals, signature row, footer. Drop in your fields and adjust colours.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
  @page { size: 8.27in 11.69in; margin: 1.5cm; }
  body { font-family: Helvetica, Arial, sans-serif; color: #151a37; font-size: 11pt; }
  table { width: 100%; border-collapse: collapse; }
  td { vertical-align: top; }
  .hdr { border-bottom: 3px solid #384494; padding-bottom: 8px; }
  .hdr-logo { font-size: 22pt; font-weight: bold; color: #384494; }
  .hdr-meta { font-size: 9pt; text-align: right; }
  .section-title { font-size: 10pt; font-weight: bold; color: #384494; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid #e3e6f3; padding-bottom: 3px; margin-bottom: 6px; }
  .grid-table td { width: 50%; padding-right: 18px; }
  .grid-table td.r { padding-right: 0; padding-left: 18px; }
  .items th { background-color: #384494; color: #ffffff; font-size: 10pt; padding: 6px 8px; text-align: left; }
  .items th.r, .items td.r { text-align: right; }
  .items td { font-size: 10pt; padding: 6px 8px; border-bottom: 1px solid #edf1f7; }
  .totals tr.final td { font-size: 13pt; font-weight: bold; padding-top: 8px; }
  .sig-line { border-bottom: 1px solid #8899b5; height: 30px; margin-top: 4px; }
  .footer { margin-top: 36px; border-top: 1px solid #d9e2ef; padding-top: 8px; font-size: 8pt; color: #8899b5; }
</style>
</head>
<body>
<table class="hdr"><tr>
  <td><div class="hdr-logo">{Account.Name}</div></td>
  <td class="hdr-meta">Quote #: {Name}<br />Date: {Today:dd-MM-yyyy}</td>
</tr></table>

<table class="grid-table"><tr>
  <td><div class="section-title">Prepared for</div><strong>{Account.Name}</strong><br />{Account.BillingStreet}<br />{Account.BillingPostalCode} {Account.BillingCity}</td>
  <td class="r"><div class="section-title">Prepared by</div>{RunningUser.Name}<br />{RunningUser.Email}</td>
</tr></table>

<div class="section-title" style="margin-top: 18px;">Line items</div>
<table class="items">
  <thead><tr><th>Product</th><th class="r">Qty</th><th class="r">Price</th><th class="r">Total</th></tr></thead>
  <tbody>
    <tr>
      <td>{#OpportunityLineItems}{Product2.Name}</td>
      <td class="r">{Quantity}</td>
      <td class="r">{UnitPrice:currency:EUR:nl_NL}</td>
      <td class="r">{TotalPrice:currency:EUR:nl_NL}{/OpportunityLineItems}</td>
    </tr>
  </tbody>
</table>

<table class="totals" style="margin-top: 8px;"><tr class="final">
  <td></td><td class="r">Total</td><td class="r">{Amount:currency:EUR:nl_NL}</td>
</tr></table>

<table style="margin-top: 36px;"><tr>
  <td style="width: 50%; padding-right: 24px;">Customer signature<div class="sig-line">{@Signature_Buyer:1:Full}</div></td>
  <td style="width: 50%; padding-left: 24px;">Date<div class="sig-line">{@Signature_Buyer:1:Date}</div></td>
</tr></table>

<table class="footer"><tr>
  <td>{Account.Name} &bull; {Account.BillingCity}</td>
  <td style="text-align: right;">{Account.Website}</td>
</tr></table>
</body>
</html>

Common conversion patterns#

When an AI assistant or a designer hands you modern CSS, these are the mechanical rewrites:

<!-- Flex header → table header -->
<div style="display: flex; justify-content: space-between;"><div>ACME</div><div>Date: {Today}</div></div>
<table style="width: 100%;"><tr><td>ACME</td><td style="text-align: right;">Date: {Today}</td></tr></table>

<!-- Grid columns → table columns -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;"><div>Left</div><div>Right</div></div>
<table style="width: 100%;"><tr><td style="width: 50%; padding-right: 12px;">Left</td><td style="width: 50%; padding-left: 12px;">Right</td></tr></table>
/* Gradient → solid colour */
.accent { background: linear-gradient(to right, #384494, #6ec1e4); }   /* before */
.accent { background-color: #384494; }                                 /* after  */

/* gap between stacked blocks → margin */
.stack { display: flex; flex-direction: column; gap: 12px; }           /* before */
.stack > * { margin-bottom: 12px; }                                    /* after  */

Paste-ready AI prompt#

Copy this verbatim into your AI assistant and replace the bracketed sections. It is the same brief the designer's Copy AI Prompt button produces.

Generate a single self-contained HTML file for Welisa DocGen.

Audience: rendered to PDF by a CSS 2.1 engine (plus a small CSS 3 subset). Modern CSS layout features are silently ignored.

HARD RULES — never use these:
- display: flex, display: grid, gap
- linear-gradient(...), radial-gradient(...), conic-gradient(...)
- calc(...), CSS variables (--name, var(--name))
- transform, transition, animation, @keyframes
- box-shadow, text-shadow, outline, opacity
- border-radius — in EVERY form. There are no rounded corners. Every box is square.
- rgba(...) and hsla(...) — the background disappears completely. Write a flat hex instead.
- :has(), :is(), :where(), container queries, @media

POSITIONING: position: absolute/relative/fixed and float work and are exact in inches, but an absolutely
positioned box with no explicit height grows to fit merged content, and nothing beyond page 1 renders.

USE INSTEAD:
- <table> for any side-by-side layout. One <tr>, columns are <td>s with explicit widths.
- Solid background-color as a HEX value.
- padding/margin in pt or in. font-size in pt. Fonts: Helvetica, Arial, "Times New Roman", Courier.
- Symbols (checkmarks, arrows) and non-Latin text need font-family: 'Arial Unicode MS'.
- Circles must be characters: &#8226; &#9679; &#9675; &#9711; &#176; sized with font-size. Never ZapfDingbats or Symbol.
- For visual interest: solid fills, contrasting borders, :nth-child(even) zebra striping, dashed/dotted borders.

PAGE SETUP — one <style> in <head> with:
  @page { size: 8.27in 11.69in; margin: 1.5cm; }   /* A4 — or 8.5in 11in for US Letter */
  body { font-family: Helvetica, Arial, sans-serif; font-size: 11pt; color: #333; }

MERGE TAGS — plain text:
- Field merge:        {FieldApiName}              e.g. {Name}, {Account.Name}, {Amount}
- Built-ins:          {Today}, {Now}, {RunningUser.Name}, {RunningUser.Email}
- Format suffixes:    {Amount:currency:EUR:nl_NL}, {CloseDate:dd-MM-yyyy}, {Quantity:#,##0}
- Loop:               {#RelationshipName} ... {/RelationshipName}
- Loop IN A TABLE:    put the tags INSIDE the cells — open in the first cell of the repeating row,
                      close in the last. The engine expands the whole <tr>. Never wrap the row from
                      outside and never put a loop tag on its own line between rows.
- Conditional:        {#IF Field = "Value"} ... {:else} ... {/IF}
- Aggregates:         {SUM:Rel.Field:currency}, {COUNT:Rel}, {AVG:...}, {MIN:...}, {MAX:...}
- Images:             {%asset:logo:200x}, {%Image:1:300x}
- Page counters:      {PageNumber}, {TotalPages}   (only inside header/footer fields, not the body)
- Signature:          {@Signature_Role:1:Full} on its own line or cell

OUTPUT: a single .html file. No external CSS, no <script>, no web fonts, no <link rel="stylesheet">. Inline everything.

Now generate a [QUOTE / INVOICE / ORDER CONFIRMATION / …] template for the [Opportunity / Account / Order]
record, with these sections: [header with logo + date, customer block, line-item table, totals, notes,
signature]. Use the merge tag syntax above.

Troubleshooting HTML templates#

  • "Your company doesn't support the following file types: .zip" — the zip is unpacked client-side and never uploaded, so this should not appear. If it does, hard-refresh the page (Cmd/Ctrl+Shift+R) to clear a cached component bundle.
  • Images show as broken squares — the engine can only fetch relative Salesforce file URLs. Use the zip, a data URI, {%Image:N}, {%asset:key} or {%Field} pointing at a real file.
  • Page numbers appear without a configured footer — the source HTML already has an @page { @bottom-center { content: counter(page) } } block (Google Docs sometimes adds one). Remove it or keep it.
  • A merge tag prints literally — the field is not in the Query Configuration, the name is wrong, or the editor HTML-encoded the braces (&#123; and &#125; are decoded automatically; other encodings are not).
  • Raw CSS at the top of the PDF — see Troubleshooting.

Verify in the end by generating on a real record (Download Sample or the runner), not in a browser: a browser approximates the layout, shows tags as text, and cannot resolve file images.