Skip to content

Template Syntax

Docstron uses the powerful Jinja2 templating engine to process your HTML templates. This guide covers the most common syntax patterns you’ll need to create dynamic PDF documents.


Use double curly braces {{ }} to insert variables into your template:

<h1>Hello, {{ customer_name }}!</h1>
<p>Invoice #{{ invoice_number }}</p>
<p>Total Amount: {{ total_amount }}</p>

Data to pass:

{
"customer_name": "John Doe",
"invoice_number": "INV-12345",
"total_amount": "$299.00"
}

Output:

<h1>Hello, John Doe!</h1>
<p>Invoice #INV-12345</p>
<p>Total Amount: $299.00</p>

Access nested object properties using dot notation:

<h2>Customer Information</h2>
<p>Name: {{ customer.name }}</p>
<p>Email: {{ customer.email }}</p>
<p>Address: {{ customer.address.street }}, {{ customer.address.city }}</p>

Data to pass:

{
"customer": {
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York"
}
}
}


Show or hide content based on conditions:

{% if is_paid %}
<div class="status-paid">PAID</div>
{% else %}
<div class="status-unpaid">UNPAID</div>
{% endif %}

Data to pass:

{
"is_paid": true
}
{% if total_amount > 1000 %}
<p class="high-value">High Value Order</p>
{% elif total_amount > 500 %}
<p class="medium-value">Medium Value Order</p>
{% else %}
<p class="low-value">Standard Order</p>
{% endif %}
{% if discount %}
<p>Discount Applied: {{ discount }}%</p>
{% endif %} {% if not shipping_address %}
<p>No shipping address provided</p>
{% endif %}
{% if is_premium and total_amount > 500 %}
<p>Premium customer discount applied!</p>
{% endif %} {% if status == 'shipped' or status == 'delivered' %}
<p>Order is on the way!</p>
{% endif %}


Display items from an array:

<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.price }}</td>
</tr>
{% endfor %}
</tbody>
</table>

Data to pass:

{
"items": [
{ "name": "Product A", "quantity": 2, "price": "29.99" },
{ "name": "Product B", "quantity": 1, "price": "49.99" },
{ "name": "Product C", "quantity": 3, "price": "19.99" }
]
}

Access the loop index and count:

<ul>
{% for product in products %}
<li>{{ loop.index }}. {{ product.name }} - ${{ product.price }}</li>
{% endfor %}
</ul>

Available loop variables:

  • loop.index - Current iteration (starts at 1)
  • loop.index0 - Current iteration (starts at 0)
  • loop.first - True if first iteration
  • loop.last - True if last iteration
  • loop.length - Total number of items
<table>
{% for item in items %}
<tr
{%
if
loop.index
is
odd
%}class="odd-row"
{%
else
%}class="even-row"
{%
endif
%}
>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
</tr>
{% endfor %}
</table>
<ul>
{% for item in cart_items %}
<li>{{ item.name }} - ${{ item.price }}</li>
{% else %}
<li>Your cart is empty</li>
{% endfor %}
</ul>

Filters modify variables before display using the pipe | symbol.

<!-- Uppercase -->
<h1>{{ customer_name | upper }}</h1>
<!-- Output: JOHN DOE -->
<!-- Lowercase -->
<p>{{ email | lower }}</p>
<!-- Output: john@example.com -->
<!-- Capitalize first letter -->
<p>{{ status | capitalize }}</p>
<!-- Output: Pending -->
<!-- Title case -->
<h2>{{ product_name | title }}</h2>
<!-- Output: Premium Wireless Headphones -->
<!-- Round to 2 decimal places -->
<p>Price: ${{ price | round(2) }}</p>
<!-- Format as integer -->
<p>Quantity: {{ quantity | int }}</p>
<!-- Absolute value -->
<p>Amount: {{ balance | abs }}</p>

Provide fallback values for missing data:

<p>Phone: {{ phone_number | default('Not provided') }}</p>
<p>Notes: {{ customer_notes | default('No notes') }}</p>
<p>Description: {{ description | truncate(100) }}</p>
<!-- Truncates to 100 characters -->
<p>Characters: {{ message | length }}</p>
<!-- Shows length of string -->
<p>Date: {{ order_date | date }}</p>
<p>Created: {{ created_at | datetime }}</p>

Chain multiple filters together:

<h2>{{ product_name | title | truncate(50) }}</h2>
<p>{{ description | lower | default('No description available') }}</p>

Perform calculations within templates:

<!-- Basic arithmetic -->
<p>Subtotal: ${{ price * quantity }}</p>
<p>Tax (10%): ${{ (price * quantity) * 0.10 }}</p>
<p>Total: ${{ (price * quantity) * 1.10 }}</p>
<!-- With variables -->
<p>Total: ${{ subtotal + tax + shipping }}</p>
<p>Discount: -${{ total * (discount_percentage / 100) }}</p>
{% set total = 0 %}
<table>
{% for item in items %} {% set item_total = item.price * item.quantity %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.price }}</td>
<td>${{ item_total }}</td>
</tr>
{% set total = total + item_total %} {% endfor %}
<tr class="total-row">
<td colspan="3"><strong>Total:</strong></td>
<td><strong>${{ total }}</strong></td>
</tr>
</table>

Set variables within the template:

{% set company_name = "Acme Corporation" %} {% set current_year = 2025 %}
<footer>
<p>© {{ current_year }} {{ company_name }}</p>
</footer>
{% set subtotal = price * quantity %} {% set tax = subtotal * 0.08 %} {% set
total = subtotal + tax %}
<div class="summary">
<p>Subtotal: ${{ subtotal }}</p>
<p>Tax (8%): ${{ tax }}</p>
<p>Total: ${{ total }}</p>
</div>

Add comments that won’t appear in the output:

{# This is a comment - it won't be rendered #}
<h1>Invoice</h1>
{# TODO: Add customer logo here #} {# Multi-line comment for longer notes #}

Control whitespace and line breaks:

<!-- Remove whitespace before -->
<p>Hello {{- customer_name }}</p>
<!-- Remove whitespace after -->
<p>{{ greeting -}} World</p>
<!-- Remove whitespace on both sides -->
<p>{{- value -}}</p>

By default, Jinja2 escapes HTML. To render raw HTML:

<!-- Escaped (safe) -->
<div>{{ user_input }}</div>
<!-- Raw HTML (use with caution) -->
<div>{{ html_content | safe }}</div>

⚠️ Warning: Only use | safe with trusted content to prevent security issues.


<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.info-section {
margin: 20px 0;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #4caf50;
color: white;
}
.total {
font-size: 1.2em;
font-weight: bold;
}
.paid {
color: green;
}
.unpaid {
color: red;
}
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
<p>Invoice #{{ invoice_number }}</p>
<p>Date: {{ invoice_date }}</p>
</div>
<div class="info-section">
<h3>Bill To:</h3>
<p><strong>{{ customer.name }}</strong></p>
<p>{{ customer.email }}</p>
{% if customer.phone %}
<p>Phone: {{ customer.phone }}</p>
{% endif %}
<p>{{ customer.address.street }}</p>
<p>
{{ customer.address.city }}, {{ customer.address.state }} {{
customer.address.zip }}
</p>
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% set grand_total = 0 %} {% for item in items %} {% set item_total =
item.quantity * item.unit_price %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.unit_price }}</td>
<td>${{ item_total }}</td>
</tr>
{% set grand_total = grand_total + item_total %} {% endfor %}
</tbody>
</table>
<div class="info-section">
{% if discount > 0 %}
<p>Subtotal: ${{ grand_total }}</p>
<p>Discount ({{ discount }}%): -${{ grand_total * (discount / 100) }}</p>
{% set final_total = grand_total - (grand_total * (discount / 100)) %} {%
else %} {% set final_total = grand_total %} {% endif %}
<p class="total">Total: ${{ final_total }}</p>
<p class="{% if is_paid %}paid{% else %}unpaid{% endif %}">
Status: {% if is_paid %}PAID{% else %}UNPAID{% endif %}
</p>
</div>
{% if notes %}
<div class="info-section">
<h3>Notes:</h3>
<p>{{ notes }}</p>
</div>
{% endif %}
<div class="info-section">
<p>Thank you for your business!</p>
</div>
</body>
</html>

Data to pass:

{
"invoice_number": "INV-2025-001",
"invoice_date": "2025-10-18",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "+1-555-0123",
"address": {
"street": "123 Main Street",
"city": "New York",
"state": "NY",
"zip": "10001"
}
},
"items": [
{ "description": "Web Design Service", "quantity": 1, "unit_price": 1500 },
{ "description": "SEO Optimization", "quantity": 1, "unit_price": 800 },
{ "description": "Monthly Maintenance", "quantity": 3, "unit_price": 200 }
],
"discount": 10,
"is_paid": false,
"notes": "Payment due within 30 days"
}
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: "Georgia", serif;
text-align: center;
padding: 50px;
}
.certificate {
border: 10px solid #4caf50;
padding: 40px;
}
h1 {
font-size: 48px;
color: #4caf50;
}
.recipient {
font-size: 36px;
font-weight: bold;
margin: 30px 0;
}
.course {
font-size: 24px;
font-style: italic;
}
</style>
</head>
<body>
<div class="certificate">
<h1>Certificate of Completion</h1>
<p>This is to certify that</p>
<p class="recipient">{{ student_name | upper }}</p>
<p>has successfully completed</p>
<p class="course">{{ course_name }}</p>
{% if grade %}
<p>with a grade of <strong>{{ grade }}</strong></p>
{% endif %}
<p>on {{ completion_date }}</p>
{% if instructor %}
<p style="margin-top: 50px;">
_________________________<br />
{{ instructor.name }}<br />
{{ instructor.title }}
</p>
{% endif %}
</div>
</body>
</html>

  • Use descriptive variable names - customer_name instead of cn
  • Provide default values - Use {{ variable | default('N/A') }}
  • Handle empty lists - Use {% for %}...{% else %} pattern
  • Keep logic simple - Complex calculations should be done before passing data
  • Test with sample data - Always test templates with realistic data
  • Comment complex sections - Use {# comments #} for clarity
  • Don’t use complex business logic - Keep templates focused on presentation
  • Don’t forget to handle missing data - Always provide defaults or conditionals
  • Don’t use | safe on user input - Risk of XSS vulnerabilities
  • Don’t nest loops too deeply - Can impact PDF generation performance
  • Don’t hardcode values - Use variables for all dynamic content

Test your templates with sample data before generating PDFs:

  1. Start simple - Test with basic variable substitution first
  2. Add complexity gradually - Add loops, conditionals, and filters one at a time
  3. Test edge cases - Empty lists, missing data, special characters
  4. Verify calculations - Double-check mathematical operations
  5. Check formatting - Ensure filters produce expected output

<!-- Bad: Will error if customer_name doesn't exist -->
<h1>Hello {{ customer_name }}</h1>
<!-- Good: Provides a default -->
<h1>Hello {{ customer_name | default('Customer') }}</h1>
<!-- Bad: Using wrong variable name -->
{% for product in products %}
<p>{{ item.name }}</p>
{# Should be product.name #} {% endfor %}
<!-- Good: Consistent naming -->
{% for product in products %}
<p>{{ product.name }}</p>
{% endfor %}
<!-- Bad: Missing endif -->
{% if condition %}
<p>Content</p>
<!-- Good: Properly closed -->
{% if condition %}
<p>Content</p>
{% endif %}

If you need assistance with template syntax: