dtm2 is the modern runtime renderer for Dynamic Template Manager.
It keeps the original DTM idea: templates are normal files on disk, so they can
be edited without recompiling the application.
The main change from x.templating.dtm is architectural. dtm2 caches parsed
template trees, not rendered HTML responses. This keeps rendering fast while
removing the old async rendered-cache server from the hot path.
Create a templates/ folder in your application and put your templates inside
it. DTM2 supports .html, .htm, .xml, .txt, and .text by default.
import x.templating.dtm2
fn main() {
mut manager := dtm2.initialize(
template_dir: 'templates'
)
placeholders := {
'title': 'DTM2'
'body': '<strong>escaped by default</strong>'
}
rendered := manager.expand('page.html', placeholders: &placeholders)
println(rendered)
}
Example template:
<!doctype html>
<html>
<head>
</head>
<body>
</body>
</html>
The rendered @body value is escaped by default.
import veb
import x.templating.dtm2
pub struct App {
pub mut:
templates &dtm2.Manager = unsafe { nil }
}
pub struct Context {
veb.Context
}
fn main() {
mut app := &App{
templates: dtm2.initialize(
template_dir: 'templates'
)
}
veb.run[App, Context](mut app, 18081)
}
@['/']
pub fn (mut app App) index(mut ctx Context) veb.Result {
placeholders := {
'title': 'Home'
'body': 'Hello from DTM2'
}
html := app.templates.expand('index.html', placeholders: &placeholders)
return ctx.html(html)
}
dtm2.initialize() accepts:
template_dir (string): root directory used for relative template paths.
If empty, /templates is used.compress_html (bool): enables a lightweight deterministic HTML
whitespace compressor. It is enabled by default.reload_modified_templates (bool): when enabled, DTM2 checks the source
template and included files before reusing a parsed template tree. It is
enabled by default.extension_config_file (string): optional JSON file containing extension
mappings. If empty, DTM2 automatically loads dtm2_extensions.json from the
configured template_dir when that file exists.Example:mut manager := dtm2.initialize(
template_dir: 'templates'
compress_html: true
reload_modified_templates: true
)
For maximum hot-path throughput in applications where templates are immutable
after startup, you can disable reload checks:mut manager := dtm2.initialize(
template_dir: 'templates'
reload_modified_templates: false
)
The recommended runtime model is one long-lived manager per application or
rendering context. Reusing the manager is what keeps parsed templates and path
resolution cached.DTM2 has two rendering modes:
TemplateType.html: HTML/XML-like output, with default escaping and optional
HTML compression.TemplateType.text: raw text output, also escaped by default.Default mappings:.html, .htm, .xml.txt, .textProject-specific extensions should be configured with a JSON file.Example templates/dtm2_extensions.json:{
"html": [".view", ".tmpl"],
"text": [".mail", ".md"]
}
If the file is named dtm2_extensions.json and is placed directly in the
configured template_dir, DTM2 loads it automatically:mut manager := dtm2.initialize(
template_dir: 'templates'
)
You can also point to an explicit config file:mut manager := dtm2.initialize(
template_dir: 'templates'
extension_config_file: 'config/my_dtm2_extensions.json'
)
DTM2 ships a default config example that can be copied into your own
templates/ directory:vlib/x/templating/dtm2/dtm2_extensions.json
If the JSON file is absent, invalid, or contains invalid entries, DTM2 keeps the
built-in defaults and prints a warning for entries that cannot be registered.
JSON extension config files are limited to 64 KB and every extension is
validated before being registered.manager.expand() accepts:
placeholders (&map[string]string): values inserted in the template.missing_placeholder_prefix (string): prefix written when a placeholder
is missing. The default is @, preserving the original placeholder text.Example:placeholders := {
'title': 'Example'
}
html := manager.expand('page.html',
placeholders: &placeholders
missing_placeholder_prefix: '@'
)
Template placeholders use the @name form.
<h1>@title</h1>
<p>@body</p>
In DTM2, placeholder values are strings:
placeholders := {
'title': 'Hello'
'count': '42'
}
Values are escaped by default in both HTML and text templates.
Security note for custom non-HTML formats such as SQL: DTM2 is a template
renderer, not a domain-specific sanitizer. By default, it escapes HTML-special
characters in placeholder values. It does not make SQL queries safe, does not
replace prepared statements, and does not validate business-specific formats.
If you add .sql or another sensitive extension through configuration, the
security of that generated content remains the responsibility of the
application.
The historical _#includehtml suffix is still supported for compatibility.
It allows a placeholder to include a restricted set of HTML tags in .html
templates.
placeholders := {
'body_#includehtml': '<p>allowed</p>'
}
html := manager.expand('page.html', placeholders: &placeholders)
The template still uses the normal placeholder name:
DTM2 escapes the complete value first, then restores only allowed tags. This
preserves the old opt-in behavior without allowing arbitrary raw HTML through.
In .txt templates, HTML is always escaped.
Allowed tags:
<div>, </div>, <h1>, </h1>, <h2>, </h2>, <h3>, </h3>, <h4>, </h4>,
<h5>, </h5>, <h6>, </h6>, <p>, </p>, <br>, <hr>, <span>, </span>,
<ul>, </ul>, <ol>, </ol>, <li>, </li>, <dl>, </dl>, <dt>, </dt>,
<dd>, </dd>, , <table>, </table>, ,
<th>, </th>, <tr>, </tr>, <td>, </td>, <thead>, </thead>,
, <tbody>, </tbody>, , ,
, , ,
, , ,
, <details>, </details>, ,
, <summary>, </summary>
Templates can include other templates with a simple line-level directive:
Include paths are resolved relative to the current template. If no file
extension is provided, .html is added. The final resolved include path must
stay inside the manager template_dir; attempts to include files through ../
or absolute paths outside that root fail.
The same boundary applies to templates passed to expand(). Absolute template
paths are accepted only when they resolve inside template_dir.
Included files are tracked as dependencies of the parsed template. When
reload_modified_templates is enabled, changing an included file invalidates
the cached parsed tree.
Existing code that imports x.templating.dtm is kept source-compatible for the
migration period. The v1 facade is deprecated, but it now delegates rendering to
DTM2 internally.
That means old code can continue to compile:
import x.templating.dtm
mut manager := dtm.initialize()
mut placeholders := map[string]dtm.DtmMultiTypeMap{}
placeholders['title'] = 'Legacy DTM'
placeholders['count'] = 7
html := manager.expand('page.html', placeholders: &placeholders)
For new code, prefer importing x.templating.dtm2 directly:
import x.templating.dtm2
mut manager := dtm2.initialize(template_dir: 'templates')
placeholders := {
'title': 'Modern DTM'
'count': '7'
}
html := manager.expand('page.html', placeholders: &placeholders)
Migration notes:
import x.templating.dtm with import x.templating.dtm2.DtmMultiTypeMap placeholder maps with map[string]string.stop_cache_handler() calls; DTM2 does not start an async cache
server._#includehtml only when HTML inclusion is intentional.DTM2 intentionally keeps rendering and rendered-output caching separate.
The manager caches:
The local benchmark harness lives in:
vlib/x/templating/dtm2/benchmarks/
Run it from the repository root:
vlib/x/templating/dtm2/benchmarks/run_dtm2_benchmark.sh
Useful options:
DTM2_BENCH_MODE=prod|prod_o2|devDTM2_BENCH_CASE=all|small_hot|small_cold|many_hot|many_cold|include_hotDTM2_BENCH_CASE=include_cold|xml_hot|xml_coldDTM2_BENCH_ITERATIONS=50000DTM2_BENCH_COLD_ITERATIONS=500DTM2_BENCH_PLACEHOLDERS=50DTM2_BENCH_COMPRESS_HTML=trueDTM2_BENCH_RELOAD_MODIFIED_TEMPLATES=falseDTM2_BENCH_VALIDATE_EACH_ITERATION=falseBenchmark result directories are generated locally under
vlib/x/templating/dtm2/benchmarks/results/ and should not be committed.