Templates Guide

Varel uses VeeMarker, a FreeMarker-compatible template engine built for V. This guide covers everything you need to render beautiful HTML views.

Table of Contents


Getting Started

Note: Template rendering imports:

import leafscale.varel.http       // For Context.render()
import leafscale.varel.templates  // For configuration

What is VeeMarker?

VeeMarker is a template engine that:

  • Compatible with FreeMarker - Familiar syntax
  • Built for V - Fast and type-safe
  • Compile-time - Templates checked at build time
  • Secure - Auto-escapes HTML by default

Project Structure

views/
├── layouts/
│   └── base.html.vtpl       # Default layout (.html.vtpl extension required)
├── products/
│   ├── index.html.vtpl      # List products
│   ├── show.html.vtpl       # Show product
│   ├── new.html.vtpl        # Create form
│   └── edit.html.vtpl       # Edit form
├── shared/
│   ├── header.html.vtpl     # Header partial (no underscore prefix)
│   ├── footer.html.vtpl     # Footer partial
│   └── flash.html.vtpl      # Flash messages
└── errors/
    ├── 404.html.vtpl        # Not found
    └── 500.html.vtpl        # Server error

Note: Template files MUST use the .html.vtpl extension. Varel uses VeeMarker templates, and partials do not require underscore prefixes.

Rendering Templates

// controllers/products.v
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
    products := models.Product.all(c.db) or { [] }

    return ctx.render('products/index', {
        'products': products
        'page_title': 'All Products'
    })
}

Template Syntax

Variables

<!-- Output variable -->
<h1>${page_title}</h1>

<!-- With default value -->
<h1>${page_title!'Untitled'}</h1>

<!-- HTML escape (automatic by default) -->
<p>${product.description?html}</p>

Conditionals

<!-- If statement -->
<#if user.is_admin>
    <a href="/admin">Admin Panel</a>
</#if>

<!-- If-else -->
<#if product.stock > 0>
    <button>Add to Cart</button>
<#else>
    <p class="out-of-stock">Out of Stock</p>
</#if>

<!-- If-elseif-else -->
<#if order.status == 'pending'>
    <span class="badge yellow">Pending</span>
<#elseif order.status == 'shipped'>
    <span class="badge blue">Shipped</span>
<#elseif order.status == 'delivered'>
    <span class="badge green">Delivered</span>
<#else>
    <span class="badge gray">Unknown</span>
</#if>

Loops

<!-- List products -->
<#list products as product>
    <div class="product">
        <h3>${product.name}</h3>
        <p>${product.price?string.currency}</p>
    </div>
</#list>

<!-- Empty list -->
<#list products as product>
    <div class="product">${product.name}</div>
<#else>
    <p>No products found</p>
</#list>

<!-- Index and counters -->
<#list products as product>
    <tr>
        <td>${product?index + 1}</td>  <!-- 1-based index -->
        <td>${product.name}</td>
    </tr>
</#list>

Formatting

<!-- Number formatting -->
<p>Price: ${product.price?string("0.00")}</p>
<p>Stock: ${product.stock?string.number}</p>

<!-- Date formatting -->
<p>Created: ${product.created_at?date}</p>
<p>Updated: ${product.updated_at?datetime}</p>

<!-- String operations -->
<p>${product.name?upper_case}</p>
<p>${product.name?lower_case}</p>
<p>${product.name?cap_first}</p>
<p>${product.description?truncate(100)}</p>

Important: Comparison Operators

VeeMarker only supports symbolic operators in conditionals, NOT text operators.

✅ Correct:

<#if product.stock > 0>
    <button>Add to Cart</button>
</#if>

<#if user.age >= 18>
    <p>Adult content visible</p>
</#if>

<#if order.status == "completed">
    <span class="badge green">Done</span>
</#if>

❌ Wrong (DO NOT USE):

<#if product.stock gt 0>      <!-- WILL NOT WORK -->
<#if user.age gte 18>          <!-- WILL NOT WORK -->
<#if order.status eq "done">   <!-- WILL NOT WORK -->

Supported Operators:

  • > (greater than) - NOT gt
  • < (less than) - NOT lt
  • >= (greater than or equal) - NOT gte
  • <= (less than or equal) - NOT lte
  • == (equal) - NOT eq
  • != (not equal) - NOT ne

Arithmetic in Conditionals

You cannot perform arithmetic directly in <#if> statements. You must use <#assign> first.

✅ Correct:

<#assign size_mb = file.size / 1024 / 1024>
<#if size_mb > 10>
    <span class="warning">Large file!</span>
</#if>

❌ Wrong:

<#if (file.size / 1024 / 1024) > 10>  <!-- WILL NOT WORK -->

✅ Arithmetic works in display expressions:

<!-- This works - direct display -->
<p>File size: ${(file.size / 1024 / 1024)?string("0.00")} MB</p>

<!-- But for conditionals, use assign first -->
<#assign size_mb = file.size / 1024 / 1024>
<#if size_mb > 100>
    <p class="large-file">Very large file!</p>
</#if>

Common Patterns

File Size with Conditional:

<#if file.size > 0>
    <span class="file-size">
        ${(file.size / 1024)?string("0.00")} KB
    </span>
<#else>
    <span class="file-size">Unknown size</span>
</#if>

Pluralization:

<p>Found ${count} item<#if count != 1>s</#if></p>

Selected Option:

<select name="category">
    <#list categories as category>
        <option value="${category.id}" <#if category.id == selected_id>selected</#if>>
            ${category.name}
        </option>
    </#list>
</select>

Troubleshooting VeeMarker Errors

Error: "Unexpected token in conditional"

  • Cause: You used a text operator (gt, lt, etc.)
  • Fix: Replace with symbolic operator (>, <, etc.)

Error: "Cannot evaluate arithmetic in conditional"

  • Cause: You tried arithmetic directly in <#if>
  • Fix: Use <#assign> to compute the value first

Error: "Unexpected parentheses in conditional"

  • Cause: Complex expressions in conditionals aren't supported
  • Fix: Use <#assign> to compute intermediate values

Layouts

Default Layout

Create views/layouts/base.html.vtpl:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${page_title!'My App'}</title>
    <link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
    <#include "shared/header.html.vtpl">

    <main class="container">
        <#include "shared/flash.html.vtpl">

        <!-- Page content goes here -->
        ${content}
    </main>

    <#include "shared/footer.html.vtpl">

    <script src="/static/js/app.js"></script>
</body>
</html>

Using Layouts

// controllers/products.v
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
    products := models.Product.all(c.db) or { [] }

    // Render with default layout
    return ctx.render('products/index', {
        'products': products
        'page_title': 'Products'
    })
}

Custom Layouts

// Use different layout
return ctx.render_with_layout('admin/dashboard', 'layouts/admin', {
    'stats': stats
    'page_title': 'Admin Dashboard'
})

Partials

Creating Partials

views/shared/_header.html:

<header>
    <nav>
        <a href="/">Home</a>
        <a href="/products">Products</a>

        <#if current_user??>
            <a href="/dashboard">Dashboard</a>
            <a href="/logout">Logout</a>
        <#else>
            <a href="/login">Login</a>
            <a href="/signup">Sign Up</a>
        </#if>
    </nav>
</header>

views/shared/_flash.html:

<#if flash_success??>
    <div class="alert alert-success">
        ${flash_success}
    </div>
</#if>

<#if flash_error??>
    <div class="alert alert-error">
        ${flash_error}
    </div>
</#if>

Including Partials

<!-- Include partial -->
<#include "shared/header.html.vtpl">

<!-- Include with parameters -->
<#include "shared/product_card.html.vtpl" product=product>

Note: Include paths must specify the full .html.vtpl extension. Partial files do not use underscore prefixes.


Template Helpers

URL Helpers

<!-- Link to route -->
<a href="/products/${product.id}">View Product</a>

<!-- Link with query params -->
<a href="/products?page=${page + 1}">Next Page</a>

<!-- Form action -->
<form action="/products/${product.id}" method="POST">
    <!-- form fields -->
</form>

Form Helpers

<!-- Text input -->
<input type="text" name="name" value="${product.name!''}">

<!-- Select dropdown -->
<select name="category_id">
    <#list categories as category>
        <option value="${category.id}"
                <#if category.id == product.category_id>selected</#if>>
            ${category.name}
        </option>
    </#list>
</select>

<!-- Checkbox -->
<input type="checkbox" name="published"
       <#if product.published>checked</#if>>

<!-- Radio buttons -->
<#list ['draft', 'published', 'archived'] as status>
    <label>
        <input type="radio" name="status" value="${status}"
               <#if product.status == status>checked</#if>>
        ${status?cap_first}
    </label>
</#list>

CSRF Token

<!-- CSRF protection for forms -->
<form action="/products" method="POST">
    <!-- Get CSRF token from context -->
    <input type="hidden" name="_csrf_token" value="${csrf_token}">

    <input type="text" name="name">
    <button type="submit">Create Product</button>
</form>

Best Practices

1. Auto-Escape HTML

VeeMarker auto-escapes HTML by default:

<!-- Safe - auto-escaped -->
<p>${product.description}</p>

<!-- Unsafe - raw HTML (only if you trust the source!) -->
<p>${product.description?no_esc}</p>

2. Provide Default Values

<!-- Good - has default -->
<h1>${page_title!'Untitled Page'}</h1>

<!-- Bad - might error if missing -->
<h1>${page_title}</h1>

3. Keep Logic in Controllers

<!-- ❌ BAD - Complex logic in template -->
<#if product.price > 100 && product.stock > 0 && !product.discontinued>
    <button>Buy Now</button>
</#if>

<!-- ✅ GOOD - Logic in controller, simple check in template -->
<#if product.can_purchase>
    <button>Buy Now</button>
</#if>

4. Use Partials for Reusability

<!-- ❌ BAD - Repeated code -->
<!-- products/index.html -->
<div class="product-card">...</div>

<!-- products/featured.html -->
<div class="product-card">...</div>  <!-- Duplicate! -->

<!-- ✅ GOOD - Reusable partial -->
<!-- shared/_product_card.html -->
<div class="product-card">
    <h3>${product.name}</h3>
    <p>${product.price}</p>
</div>

<!-- products/index.html.vtpl -->
<#list products as product>
    <#include "shared/product_card.html.vtpl">
</#list>

Scaffolding Views

Automatic View Generation

When you scaffold a resource with the Varel CLI, it automatically generates all views:

# Generate complete resource with views
varel generate scaffold Product name:string price:decimal description:text stock:int

# This creates:
# - views/products/index.html  (list view)
# - views/products/show.html   (detail view)
# - views/products/new.html    (create form)
# - views/products/edit.html   (edit form)

Generated Index View

views/products/index.html:

<h1>Products</h1>

<a href="/products/new">New Product</a>

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Description</th>
            <th>Stock</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        <#list products as product>
            <tr>
                <td>${product.name}</td>
                <td>${product.price?string("0.00")}</td>
                <td>${product.description}</td>
                <td>${product.stock}</td>
                <td>
                    <a href="/products/${product.id}">Show</a>
                    <a href="/products/${product.id}/edit">Edit</a>
                    <a href="/products/${product.id}" data-method="delete">Delete</a>
                </td>
            </tr>
        <#else>
            <tr>
                <td colspan="5">No products found</td>
            </tr>
        </#list>
    </tbody>
</table>

Generated Show View

views/products/show.html:

<h1>${product.name}</h1>

<dl>
    <dt>Name:</dt>
    <dd>${product.name}</dd>

    <dt>Price:</dt>
    <dd>$${product.price?string("0.00")}</dd>

    <dt>Description:</dt>
    <dd>${product.description}</dd>

    <dt>Stock:</dt>
    <dd>${product.stock}</dd>
</dl>

<a href="/products/${product.id}/edit">Edit</a>
<a href="/products">Back to Products</a>

Generated Form Views

views/products/new.html:

<h1>New Product</h1>

<form action="/products" method="POST">
    <input type="hidden" name="_csrf_token" value="${csrf_token}">

    <div>
        <label for="name">Name:</label>
        <input type="text" name="name" id="name" required>
    </div>

    <div>
        <label for="price">Price:</label>
        <input type="number" name="price" id="price" step="0.01" required>
    </div>

    <div>
        <label for="description">Description:</label>
        <textarea name="description" id="description"></textarea>
    </div>

    <div>
        <label for="stock">Stock:</label>
        <input type="number" name="stock" id="stock" required>
    </div>

    <button type="submit">Create Product</button>
    <a href="/products">Cancel</a>
</form>

views/products/edit.html:

<h1>Edit Product</h1>

<form action="/products/${product.id}" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_csrf_token" value="${csrf_token}">

    <div>
        <label for="name">Name:</label>
        <input type="text" name="name" id="name" value="${product.name}" required>
    </div>

    <div>
        <label for="price">Price:</label>
        <input type="number" name="price" id="price" value="${product.price}" step="0.01" required>
    </div>

    <div>
        <label for="description">Description:</label>
        <textarea name="description" id="description">${product.description}</textarea>
    </div>

    <div>
        <label for="stock">Stock:</label>
        <input type="number" name="stock" id="stock" value="${product.stock}" required>
    </div>

    <button type="submit">Update Product</button>
    <a href="/products/${product.id}">Cancel</a>
</form>

Customizing Generated Views

The generated views are starting points. Customize them:

<!-- Add styling -->
<div class="card">
    <h1 class="card-title">${product.name}</h1>
    <p class="card-text">${product.description}</p>
</div>

<!-- Add validation messages -->
<#if errors??>
    <div class="alert alert-error">
        <#list errors as error>
            <p>${error}</p>
        </#list>
    </div>
</#if>

<!-- Add image uploads -->
<div>
    <label for="image">Product Image:</label>
    <input type="file" name="image" id="image" accept="image/*">
</div>

<!-- Add rich text editor -->
<div>
    <label for="description">Description:</label>
    <textarea name="description" id="description" class="rich-editor">${product.description}</textarea>
</div>
<script src="/static/js/editor.js"></script>

Summary

You've learned:

✅ VeeMarker template syntax ✅ Variables, conditionals, and loops ✅ Layouts and partials ✅ Form helpers and CSRF tokens ✅ Scaffolding views with the CLI ✅ Customizing generated views ✅ Best practices for security and maintainability

Continue to the Sessions & Auth Guide for user authentication!