Skip to main content

What is a template?

A template is a reusable document design made of four parts:
PartRequiredPurpose
htmlTemplateYesThe document structure and layout (HTML + Liquid tags)
mockDataYes (in Studio)Sample data used for live preview
customCssNoExtra CSS appended to the document
settingsYes (has defaults)Paper size, orientation, margins, header, footer
When you call the generate API, the data you send is merged into htmlTemplate to produce the final HTML, which is then converted to a PDF.

The Studio

Open pdfgorilla.io/playground to experiment quickly, or the Studio (/templates/:id) to work on a saved template. Both have:
  • HTML, JSON (mock data), CSS, and Settings tabs with live syntax validation
  • A Generate button (or Cmd/Ctrl + S) that renders a preview PDF in the right panel
  • A Copilot panel for AI-assisted editing
  • A Beautify button to auto-format code
  • A Fullscreen editor mode (Esc to exit)
  • Download PDF to save the preview
The Studio additionally has:
  • Version history: browse, preview, and restore the last 50 saved versions
  • Import / Export: backup and restore template JSON

Liquid templating

PDF Gorilla uses LiquidJS in non-strict mode, which means:
  • Missing variables render as empty strings (they don’t crash)
  • Unknown filters are silently ignored

Variables

<h1>Invoice {{ invoice.number }}</h1>
<p>Customer: {{ customer.name }}</p>
<p>Amount due: {{ totals.grandTotal }}</p>
The variable name must match the key in your data object exactly, including case.

Loops

<table>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{ forloop.index }}</td>
      <td>{{ item.description }}</td>
      <td>{{ item.amount }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>
forloop.index counts from 1. Use forloop.first and forloop.last for conditional styling.

Conditionals

{% if customer.vatId %}
  <p>VAT ID: {{ customer.vatId }}</p>
{% else %}
  <p>No VAT ID on file.</p>
{% endif %}

Useful filters

{{ customer.name | upcase }}                  <!-- ACME CORP -->
{{ customer.nickname | default: "Customer" }} <!-- fallback value -->
{{ invoice.issuedAt | date: "%B %d, %Y" }}   <!-- April 06, 2026 -->
{{ price | prepend: "$" }}                     <!-- $99.00 -->

CSS and print styling

Use a <style> block inside htmlTemplate, or write CSS in the CSS tab (sent as customCss).
/* Set page size (also controllable via settings) */
@page {
  size: A4;
  margin: 14mm 12mm;
}

/* Body defaults */
body {
  font-family: Arial, Helvetica, sans-serif;
  font-size: 11px;
  color: #1a1a1a;
}

/* Force page breaks between sections */
.page-break {
  page-break-after: always;
}

/* Avoid breaking a row across pages */
tr {
  page-break-inside: avoid;
}

/* Repeat table headers on each page */
thead {
  display: table-header-group;
}
PDF generation always uses printBackground: true, so background colors and images will appear in the PDF.

External assets (images, fonts)

External URLs (e.g. <img src="https://...">) are fetched by the browser during render. They must be publicly reachable. Slow or unavailable resources can cause render timeouts. Use the Assets library (see below) to host images and reference them in templates. Files are stored privately; the app serves them only to signed-in users.

Assets library

Upload reusable images (logos, signatures, icons) at /assets.
  • Supported formats: PNG, JPG, WEBP
  • Max file size: 2 MB per image
  • Max images per account: plan-dependent (default Free: 10, Pro: 100)
Each uploaded asset gets:
  • A stable asset:TAG URI you can use in <img src="..."> and CSS url() values — e.g. asset:logo
  • An @tag (e.g. @logo) you can mention in Copilot prompts
  • Use the Copy tag button on the Assets page to copy the asset:TAG value
When generating a PDF, the server rewrites asset:TAG references to short-lived signed links so the render service can fetch the image. The older /api/assets/{id} format still works but asset:TAG is preferred for readability. Other external URLs in your template must still be publicly reachable by the render service.
<!-- Recommended: use asset:TAG syntax (copied from the Assets page) -->
<img src="asset:logo" alt="Logo" style="height: 40px" />

<!-- Also works: direct path with asset ID -->
<img src="/api/assets/cmjxxxxxxxxxxxx" alt="Logo" style="height: 40px" />
CSS example:
.header-logo {
  background-image: url('asset:logo');
  background-size: contain;
}

Page settings

Configure via the Settings tab in the Studio, or in settingsJson on the template record.
SettingDefaultOptions
formata4a0 to a6, letter, legal, tabloid, ledger
orientationportraitportrait, landscape
marginTop14mmAny CSS length (mm, in, px, cm)
marginRight12mm
marginBottom14mm
marginLeft12mm
headerLeft/Center/Right""Plain text + tokens (see below)
footerLeft/Center/Right"" / [page]/[topage]Plain text + tokens
Headers and footers are plain text, not Liquid: they are not rendered through the template engine. Use these special tokens:
TokenReplaced with
[page]Current page number
[topage]Total page count
[date]Current date
[title]Page <title>
[url]Page URL
Example footer: Page [page] of [topage]Page 3 of 12 Headers and footers are only rendered when at least one field is non-empty.

Copilot (AI assistant)

Click the Copilot icon in the top editor toolbar to open the AI chat panel. Copilot can generate or edit:
  • HTML template
  • Mock data
  • Custom CSS
  • Settings (format, margins, header/footer)
Tips:
  • Describe what you want in plain language: “Add a two-column layout with the customer address on the left and invoice details on the right”
  • Reference uploaded assets with @tag: “Use @logo in the top-left corner” — Copilot emits asset:TAG references that the server resolves automatically
  • Ask for iterations: “Make the table striped with alternating row colors”
  • Send with Enter; use Shift+Enter for a newline
Copilot uses your current template as context, so follow-up prompts refine the existing design.

Data format rules

The data (and mockData) must be a JSON object at the root, not an array, string, or number.
// ✅ Valid
{
  "invoice": { "number": "INV-001" },
  "items": [{ "name": "Service", "qty": 1 }]
}

// ❌ Invalid: root is an array
[{ "name": "Service" }]

// ❌ Invalid: root is a string
"hello"

Validation behavior

The Studio validates your code on every edit (debounced) and shows a colored dot on each tab:
  • 🟢 Green: valid
  • 🔴 Red: errors found (with line/column info when available)
Validation checks:
  • HTML: Liquid syntax + HTML tag balance
  • JSON: Must be a valid JSON object at root
  • CSS: Stylesheet parse
The Generate button is disabled when any tab has errors.

Common pitfalls

SymptomLikely causeFix
Variable shows as blankWrong key name in dataCheck spelling and nesting: invoice.number not invoiceNumber
Mock data must be a valid JSON objectRoot is an array []Wrap in {}
Header/footer tokens not replacedTypo in tokenUse exactly [page], [topage], etc.
Preview times outSlow external resource (font, image)Remove or host assets locally / in Assets library
Page breaks in wrong placeMissing page-break-after: alwaysAdd .page-break { page-break-after: always } between sections

Full invoice example

HTML template

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Invoice {{ invoice.number }}</title>
  <style>
    body  { font-family: Arial, sans-serif; padding: 40px; font-size: 11px; color: #1a1a1a; }
    h1    { font-size: 22px; margin-bottom: 4px; }
    table { width: 100%; border-collapse: collapse; margin-top: 16px; }
    th    { background: #f5f5f5; padding: 8px; text-align: left; border-bottom: 2px solid #ddd; }
    td    { padding: 8px; border-bottom: 1px solid #eee; }
    .total { text-align: right; font-size: 14px; font-weight: bold; margin-top: 16px; }
  </style>
</head>
<body>
  <h1>Invoice {{ invoice.number }}</h1>
  <p>Date: {{ invoice.date }} &nbsp;|&nbsp; Due: {{ invoice.dueDate }}</p>

  <h3>{{ customer.name }}</h3>
  <p>{{ customer.address }}</p>

  <table>
    <thead>
      <tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Total</th></tr>
    </thead>
    <tbody>
      {% for item in items %}
      <tr>
        <td>{{ item.description }}</td>
        <td>{{ item.qty }}</td>
        <td>{{ item.unitPrice }}</td>
        <td>{{ item.lineTotal }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>

  <p class="total">Grand total: {{ totals.grand }}</p>

  {% if notes %}
  <p style="margin-top: 32px; color: #555">Notes: {{ notes }}</p>
  {% endif %}
</body>
</html>

Mock data

{
  "invoice": {
    "number": "INV-2026-001",
    "date": "2026-04-06",
    "dueDate": "2026-04-20"
  },
  "customer": {
    "name": "Acme Corp",
    "address": "123 Main St, Springfield, USA"
  },
  "items": [
    { "description": "Consulting (4 h)", "qty": 4, "unitPrice": "$100.00", "lineTotal": "$400.00" },
    { "description": "Setup fee",        "qty": 1, "unitPrice": "$250.00", "lineTotal": "$250.00" }
  ],
  "totals": { "grand": "$650.00" },
  "notes": "Payment via bank transfer. Thank you!"
}

API call

curl -X POST https://pdfgorilla.io/api/v1/templates/TEMPLATE_ID/generate \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "data": {
    "invoice": { "number": "INV-2026-001", "date": "2026-04-06", "dueDate": "2026-04-20" },
    "customer": { "name": "Acme Corp", "address": "123 Main St" },
    "items": [
      { "description": "Consulting", "qty": 4, "unitPrice": "$100.00", "lineTotal": "$400.00" }
    ],
    "totals": { "grand": "$400.00" }
  }
}
EOF