# dtm2 - Dynamic Template Manager 2
`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.
## Quick Start
Create a `templates/` folder in your application and put your templates inside
it. DTM2 supports `.html`, `.htm`, `.xml`, `.txt`, and `.text` by default.
```v
import x.templating.dtm2
fn main() {
mut manager := dtm2.initialize(
template_dir: 'templates'
)
placeholders := {
'title': 'DTM2'
'body': 'escaped by default'
}
rendered := manager.expand('page.html', placeholders: &placeholders)
println(rendered)
}
```
Example template:
```html
@title
@body
```
The rendered `@body` value is escaped by default.
## Veb Example
```v
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)
}
```
## Available Options
### Manager options
`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:
```v ignore
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:
```v ignore
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.
### Template extensions
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 mode: `.html`, `.htm`, `.xml`
- Text mode: `.txt`, `.text`
Project-specific extensions should be configured with a JSON file.
Example `templates/dtm2_extensions.json`:
```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:
```v ignore
mut manager := dtm2.initialize(
template_dir: 'templates'
)
```
You can also point to an explicit config file:
```v ignore
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:
```sh
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.
### Render options
`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:
```v ignore
placeholders := {
'title': 'Example'
}
html := manager.expand('page.html',
placeholders: &placeholders
missing_placeholder_prefix: '@'
)
```
## The Placeholder System
Template placeholders use the `@name` form.
```html
@title
@body
```
In DTM2, placeholder values are strings:
```v ignore
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.
## Explicit HTML Inclusion
The historical `_#includehtml` suffix is still supported for compatibility.
It allows a placeholder to include a restricted set of HTML tags in `.html`
templates.
```v ignore
placeholders := {
'body_#includehtml': 'allowed
'
}
html := manager.expand('page.html', placeholders: &placeholders)
```
The template still uses the normal placeholder name:
```html
@body
```
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:
```html
,
, ,
, ,
, ,
, ,
,
,
, ,
, ,
,
,
, , ,
, ,
, , , ,
, , ,
, , , , , ,
, | , ,
, , | , , ,
, , , , , , , ,
, , , ,
, , , , ,
, , , ,
, , ,
```
## Includes
Templates can include other templates with a simple line-level directive:
```html
@body
```
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.
## Backward Compatibility With DTM v1
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:
```v ignore
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:
```v ignore
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:
- Replace `import x.templating.dtm` with `import x.templating.dtm2`.
- Replace `DtmMultiTypeMap` placeholder maps with `map[string]string`.
- Convert numeric values to strings before rendering.
- Remove `stop_cache_handler()` calls; DTM2 does not start an async cache
server.
- Keep using `_#includehtml` only when HTML inclusion is intentional.
## Design Notes
DTM2 intentionally keeps rendering and rendered-output caching separate.
The manager caches:
- canonical template paths;
- parsed template trees;
- dependency metadata for root templates and includes.
The manager does not cache rendered HTML responses. If a future rendered-cache
layer is needed, it should remain a small optional layer above DTM2 rather than
part of the parser/renderer core.
## Benchmarks
The local benchmark harness lives in:
```sh
vlib/x/templating/dtm2/benchmarks/
```
Run it from the repository root:
```sh
vlib/x/templating/dtm2/benchmarks/run_dtm2_benchmark.sh
```
Useful options:
- `DTM2_BENCH_MODE=prod|prod_o2|dev`
- `DTM2_BENCH_CASE=all|small_hot|small_cold|many_hot|many_cold|include_hot`
- `DTM2_BENCH_CASE=include_cold|xml_hot|xml_cold`
- `DTM2_BENCH_ITERATIONS=50000`
- `DTM2_BENCH_COLD_ITERATIONS=500`
- `DTM2_BENCH_PLACEHOLDERS=50`
- `DTM2_BENCH_COMPRESS_HTML=true`
- `DTM2_BENCH_RELOAD_MODIFIED_TEMPLATES=false`
- `DTM2_BENCH_VALIDATE_EACH_ITERATION=false`
Benchmark result directories are generated locally under
`vlib/x/templating/dtm2/benchmarks/results/` and should not be committed.