# How Listmonk Handles Template Rendering with Go Templates: Complete Technical Guide

> Discover how Listmonk handles template rendering with Go templates. Learn about its pipeline for auto-escaping HTML, Markdown conversion, and custom helpers.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: deep-dive
- Published: 2026-05-19

---

**Listmonk compiles stored email template strings into Go's `html/template` and `text/template` engines at send time, executing them against subscriber data through a pipeline that auto-escapes HTML, converts Markdown, and injects custom helper functions.**

The open-source newsletter manager [knadh/listmonk](https://github.com/knadh/listmonk) treats every stored email template as a Go template. When campaigns or transactional messages are triggered, listmonk retrieves template strings from PostgreSQL and passes them through a sophisticated compilation pipeline before execution.

## The Three-Step Template Rendering Pipeline

Listmonk's rendering process follows a strict three-phase architecture defined across [`models/templates.go`](https://github.com/knadh/listmonk/blob/main/models/templates.go) and [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go).

### Step 1: Detecting Template Expressions

Before compilation, listmonk checks if a string requires template processing. The helper function `hasTplExpr` in [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go) (lines 44-48) scans for the presence of both `{{` and `}}` delimiters, returning `true` only if the content contains actual Go template syntax.

### Step 2: Compiling Template Objects

Listmonk distinguishes between **transactional templates** (reusable stored templates) and **campaign templates** (message-specific compositions).

**Transactional Templates** ([`models/templates.go`](https://github.com/knadh/listmonk/blob/main/models/templates.go)):
The `(*Template) Compile` method processes the body using `html/template` for automatic HTML escaping, while subjects use `text/template` since they contain plain text.

**Campaign Templates** ([`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go)):
The `(*Campaign) CompileTemplate` method constructs a **base template** named `"base"` containing the HTML layout, and a **content template** named `"content"` containing the message body. These are merged using `AddParseTree` to embed the content into the base layout's `{{ template "content" . }}` placeholder.

### Step 3: Executing Against Subscriber Data

During message generation, listmonk calls `Execute()` on the compiled template, passing a `map[string]any` containing subscriber fields (`{{ .Email }}`, `{{ .Name }}`), campaign metadata, and custom variables.

## How Transactional Templates Are Compiled

In [`models/templates.go`](https://github.com/knadh/listmonk/blob/main/models/templates.go), the `Compile` method handles stored templates from the database:

```go
func (t *Template) Compile(f template.FuncMap) error {
    // Compile HTML body with html/template (auto-escaping)
    tpl, err := template.New(BaseTpl).Funcs(f).Parse(t.Body)
    if err != nil {
        return err
    }
    t.Tpl = tpl
    
    // Compile subject line with text/template (plain text)
    if hasTplExpr(t.Subject) {
        subjTpl, err := txttpl.New(BaseTpl).Funcs(txttpl.FuncMap(f)).Parse(t.Subject)
        if err != nil {
            return err
        }
        t.SubjectTpl = subjTpl
    }
    return nil
}

```

This design ensures that **HTML bodies are auto-escaped** to prevent XSS attacks, while **subject lines remain unescaped plain text**.

## How Campaign Templates Are Compiled

Campaign rendering in [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go) is more complex, handling Markdown conversion, alternate bodies, and header templating.

### Base and Content Template Architecture

The `CompileTemplate` method creates two distinct template objects and merges them:

```go
baseTPL, err := template.New(BaseTpl).Funcs(f).Parse(body)
if err != nil {
    return err
}
msgTpl, err := template.New(ContentTpl).Funcs(f).Parse(body)
if err != nil {
    return err
}
out, err := baseTPL.AddParseTree(ContentTpl, msgTpl.Tree)
if err != nil {
    return err
}
c.Tpl = out

```

The base template contains the overall layout with headers, footers, and CSS, while the content template holds the message body.

### Markdown Processing

If `ContentType` is `"markdown"`, listmonk converts Markdown to HTML using `markdown.Convert` during the `CompileTemplate` phase (lines 73-80) before template compilation occurs.

### Header and Alt-Body Templating

Listmonk compiles additional template objects for email headers and plain-text alternatives:

- **Header values**: Compiled into `text/template` instances stored in `c.HeaderTpls` (lines 112-138)
- **Alternate plain-text body**: Compiled into `c.AltBodyTpl` if the `AltBody` field contains template expressions

## Custom Template Functions and Helpers

Listmonk provides a rich `template.FuncMap` built in [`internal/core/core.go`](https://github.com/knadh/listmonk/blob/main/internal/core/core.go) that exposes functions like `fmtDate`, `lower`, `upper`, and internationalization helpers (`c.i18n.Ts`) to template authors. This function map is passed to every `Compile` and `CompileTemplate` call.

Example usage in templates:

```go
{{ .Name | upper }}
{{ fmtDate .CreatedAt "2006-01-02" }}

```

## Security: HTML Escaping vs Plain Text

Listmonk carefully selects template engines based on content type to balance security and functionality:

- **`html/template`**: Used for message bodies to automatically escape dangerous HTML entities
- **`text/template`**: Used for subject lines, headers, and plaintext alternatives where HTML escaping would corrupt content

## Summary

- Listmonk stores raw template strings in PostgreSQL and compiles them at send time using Go's standard `html/template` and `text/template` packages.
- The `hasTplExpr` helper in [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go) detects whether compilation is necessary by checking for `{{` and `}}` delimiters.
- Transactional templates compile via `(*Template) Compile` in [`models/templates.go`](https://github.com/knadh/listmonk/blob/main/models/templates.go), using `html/template` for bodies and `text/template` for subjects.
- Campaign templates use `(*Campaign) CompileTemplate` in [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go) to merge base layouts with content templates via `AddParseTree`.
- Markdown content is converted to HTML before template compilation when `ContentType` equals `"markdown"`.
- Custom functions from [`internal/core/core.go`](https://github.com/knadh/listmonk/blob/main/internal/core/core.go) provide date formatting, string manipulation, and internationalization capabilities.

## Frequently Asked Questions

### How does listmonk prevent XSS attacks in email templates?

Listmonk uses Go's `html/template` package for all HTML email bodies, which automatically escapes variables inserted via `{{ .Variable }}` syntax. This prevents malicious script injection from subscriber data while preserving intentional HTML from the template itself.

### Can I use Markdown in listmonk campaigns?

Yes. When a campaign's `ContentType` is set to `"markdown"`, listmonk converts the Markdown to HTML using `markdown.Convert` during the `CompileTemplate` phase (lines 73-80 of [`models/campaigns.go`](https://github.com/knadh/listmonk/blob/main/models/campaigns.go)) before passing the result to the template compiler.

### What custom functions are available in listmonk templates?

Listmonk injects a custom `FuncMap` built in [`internal/core/core.go`](https://github.com/knadh/listmonk/blob/main/internal/core/core.go) that includes formatting functions like `fmtDate`, `lower`, and `upper`, plus internationalization helpers. These functions are available in both campaign and transactional templates.

### Why does listmonk use different template engines for subjects and bodies?

Subjects, headers, and alternate plain-text bodies use `text/template` because they contain plain text where HTML entities would be corrupted (e.g., `&quot;` appearing literally in a subject line). Message bodies use `html/template` to ensure proper HTML escaping and prevent XSS vulnerabilities.