Templates & controlled pages: two weapons of dynamic content
The last piece of server-side rendering: xrtTemplate handles rendering, the page/ directory handles "who may see the output". The demo's /template and /page routes are this chapter's living samples.
Overview
19.1 Intro
Not all dynamic content deserves a frontend framework: admin panels, report pages and mail rendering are natural fits for server-side templates. XServer's answer keeps a clean split — xrtTemplate (a compiled template engine, 32 APIs) handles "how to render", the page/ directory (outside wwwroot) handles "whether it is directly reachable": templates and controlled pages never surface in the static layer; they only go out through your routes via LoadPage/rendering.
19.2 Template syntax & rendering
/* 1. prepare data (an xvalue tree, ch18's ownership rules) */
xvalue* pData = xrtValueObject();
xvalue* pList = xrtValueArray();
xrtValueObjectSetNew(pData, XRT_STR_LITERAL("title"),
xrtValueString(XRT_STR_LITERAL("Template page")));
for ( i = 0; i < N; i++ )
xrtValueArrayAppendNew(pList,
xrtValueString(xrtStrView(arrItem[i])));
xrtValueObjectSetNew(pData, XRT_STR_LITERAL("list"), pList);
/* 2. render (LoadTemplate internally: read file → Compile → Render) */
(void)LoadTemplate(pReq, "page.html", pData);
/* 3. release the data tree */
xrtValueRelease(pData);
Template syntax (a real sample from template/page.html):
<h1>{$title}</h1>
<ul>
{#foreach:list}
<li>{$this}</li>
{/foreach}
</ul>
What just happened: xrtTemplateCompile(xstrview) compiles the source text into an xtemplate* (immutable, reusable); xrtTemplateRender renders it with an xvalue data tree into a str. The demo's LoadTemplate packs "read + compile + render + output" into one call — the full pipeline runs per request (template edits apply immediately, at the cost of recompiling per request), or compile once in ServiceInit and cache (see 19.4).
19.3 Controlled pages & escape protection
Files under page/ are not URL-reachable (the static layer only serves wwwroot). The script's LoadPage reads through an xroot handle, with defenses shared with chapter 10's static layer:
- Root anchoring: reads can only land inside the page/ directory
- Rejecting
.., absolute paths and symlink escapes - Filenames coming from your code rather than request parameters are far safer — a request-supplied filename hands the trust boundary to the user (see the advanced exercise)
19.4 Compilation caching strategies
Two strategies, chosen by scenario:
| Strategy | How | Fits |
|---|---|---|
| Per-request pipeline | the demo today: LoadTemplate reads + compiles + renders each time | development (template edits apply instantly); low-traffic pages |
| ServiceInit cache | Compile in Init, store in a static; requests only Render | high-traffic pages; templates isolated per generation (rebuilt on swap — no stale cache) |
The caching strategy meshes with chapter 5's "statics isolated per generation": a cached xtemplate* is generation state, rebuilt on swap — the "edited template not taking effect" staleness problem doesn't exist (editing a template is content change; pair it with reload).
19.5 Contract
- Visibility: only wwwroot is URL-reachable; page/template go through script-controlled reads.
- Ownership: the xtemplate* is held by whoever compiles (Release frees it); the str from Render is freed by the caller.
- Caching: cached templates are generation state — rebuilt on swap, never shared across generations by hand.
Common mistakes
- Symptom
- Users download the raw template source (with its {$} markers).
- Cause
- Templates are not static assets — placing them in wwwroot publishes your source. Templates belong in template/.
- Symptom
- LoadPage(pReq, sUserInput) — even with root anchoring, it punches holes in the trust boundary (enumerating pages, probing file existence).
- Cause
- The "controlled" of controlled pages comes from you deciding what to read; handing that decision to the request leaves only the anti-escape layer standing.
LoadPage(pReq, sQueryPage);/* whitelist mapping: request picks the key, code picks the file */Exercises
Render a 20-row data table page with an xvalue array + foreach.
Implement /page/{name}: name maps through a whitelist to files under page/; anything else 404.
/page/../secret and /page/unknown both 404; whitelisted pages render.Add an "Init-time compile cache" switch to LoadTemplate and load-test the RPS difference between the strategies.
Summary
| Point | Conclusion |
|---|---|
| Pipeline | data tree (xvalue) → Compile → Render → str |
| Syntax | {$var} interpolation; {#foreach:list} loops |
| Visibility | wwwroot reachable; page/template script-controlled |
| Caching | per-request pipeline (dev) or Init cache (prod); generation isolation prevents staleness |
- xrt reference · template — Compile/Render and render configuration
- xrt reference · html — HTML escaping tools (safe template output)