Use when building a server-side web application with DelphiMVCFramework — HTML pages and HTMX fragments rendered by TemplatePro. Covers view engine setup, template inheritance and blocks, ViewData and its ownership rules, fragments, custom filters, cookie/JWT login, static files, and the HTMX request/response helpers on the Delphi side. Triggers on "TemplatePro", "RenderView", "ViewData", "baselayout", "template inheritance", "HTML fragment", "web controller", "HTMX", "hx-get", "server-side v...
Installs into .claude/skills of the current project.
Are you the author of Dmvcframework Webapp?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/danieleteti-dmvcframework-webapp)
---
name: dmvcframework-webapp
description: Use when building a server-side web application with DelphiMVCFramework — HTML pages and HTMX fragments rendered by TemplatePro. Covers view engine setup, template inheritance and blocks, ViewData and its ownership rules, fragments, custom filters, cookie/JWT login, static files, and the HTMX request/response helpers on the Delphi side. Triggers on "TemplatePro", "RenderView", "ViewData", "baselayout", "template inheritance", "HTML fragment", "web controller", "HTMX", "hx-get", "server-side view", "web application", "cookie auth", "static files".
---
# DMVCFramework — Server-Side Views with TemplatePro + HTMX
Reference project: `webapplication_with_htmx` wizard template.
TemplatePro repo: https://github.com/danieleteti/templatepro
HTMX docs: https://htmx.org/docs/
**REQUIRED COMPANION SKILL:** `dmvcframework` — covers the REST/ActiveRecord/middleware
layer that underpins every web application. If the user asks about controllers, entities,
JWT middleware configuration, or engine setup beyond what is in this skill, invoke `dmvcframework`.
**COMPANION SKILL (on demand):** `htmx-skill` — indexes every page of the official htmx.org
documentation. For any attribute, trigger/swap modifier, event, header or extension beyond the few
patterns in Section 7, invoke it and read the linked page. Do not write HTMX from memory.
**COMPANION SKILL (on demand):** `dmvcframework-ui` — the presentation layer the wizard generates:
Bootstrap 5.3, `baselayout.html` blocks, `style.css` brand tokens, dark mode via `data-bs-theme`, toasts.
Invoke it before writing markup or CSS.
---
**REQUIRED REFERENCE — `dmvcframework-security`.** Any endpoint that accepts input from a client (body,
query string, header, cookie, upload, URL) must follow it: access control and IDOR, mass assignment,
SQL injection, XSS, CSRF, path traversal, uploads, security headers, JWT, secrets. Invoke it whenever you
write or review such an endpoint — not only when the user says "security".
---
## When in doubt about an API — verify it, never guess
Never invent an identifier or answer from memory. If you need a signature this skill does not cover, ask the
user for the path to their **DelphiMVCFramework checkout** and read `sources/` (and the matching `samples/`
project); failing that, read the official repository —
[sources](https://github.com/danieleteti/delphimvcframework/tree/master/sources) ·
[samples](https://github.com/danieleteti/delphimvcframework/tree/master/samples).
If you still cannot verify it, **say so**. Where a skill and a sample disagree, **the sample wins**.
---
## 1. STOP — this skill works inside a wizard-generated project
**Never create the project from scratch. Never hand-write the `.dpr` or the base templates.**
The wizard generates a running web application — layout, static files, session, view engine, HTMX — and your
job is to add pages to it.
### Step 1 — Detect the project in the current folder
The user is expected to have run the wizard and started the agent **from inside the project folder**. A web
wizard project has:
```
*.dpr Boot + RunServer (Indy Direct by default; a WebModule here means WebBroker/ISAPI/Apache — also fine)
BootConfigU.pas dotEnv, LoggerPro, TemplatePro init
EngineConfigU.pas view engine, middleware/filters, UseExceptionHandler
Controllers.HomeU.pas web controller — actions returning RenderView(...)
Controllers.APIU.pas JSON sidecar (if present)
TemplateProHelpersU.pas custom TemplatePro filters
ServicesU.pas DI registrations
bin/.env port, secrets, view settings
bin/templates/ baselayout.html + pages
bin/www/ static files (css/, favicon, manifest)
```
Read `EngineConfigU.pas`, the web controller and `bin/templates/baselayout.html` **before writing anything**.
### Step 2 — If there is no wizard project, stop and say so
Do not scaffold one. Tell the user:
> This skill works on a project generated by the DMVCFramework IDE wizard, and I do not see one in this
> folder. Please create it first:
>
> **Delphi IDE → File → New → Other → Delphi Projects → DelphiMVCFramework → New DMVCFramework Application**
>
> Pick **Web Application** (or **Minimal API WebApp** for lambda handlers). Accept the defaults — the server
> backend is **Indy Direct**. Compile and run it once and check the page opens in the browser. Then `cd` into
> the project folder, start me there, and tell me what page to add.
Then wait. Do not generate project files in the meantime.
**The host may be WebBroker (ISAPI, Apache) — that is fine.** Keep it, do not migrate it, do not suggest
migrating it. Everything above the host is identical on every backend. See `dmvcframework`,
`reference/servers.md`.
### Step 3 — Follow the scaffolding workflow for new pages
See Section 16 for the step-by-step workflow.
---
## 2. Architecture Overview
```
Browser
│ HTML pages + HTMX attributes
│
├─ GET /web/... → TWebController → RenderView('path/template')
│ ViewData['key'] := value
│
├─ GET /web/... → TWebController → same action, fragment template
│ (HTMX request, IsHTMX and not HXIsBoosted → RenderView('x/table')
│ not boosted) otherwise → RenderView('x/index')
│
└─ GET /api/... → TAPIController → OkResponse(StrDict(...))
(JSON sidecar)
```
Key invariants:
- All controller actions are **functions**, never procedures.
- `[MVCProduces(TMVCMediaType.TEXT_HTML)]` is **optional** — the samples omit it and set the default
content type once in the engine config (`Config[TMVCConfigKey.DefaultContentType] := TMVCMediaType.TEXT_HTML`)
or in `OnBeforeAction`. Use it only when one action must differ from the app default.
- Full-page actions call `Result := RenderView('folder/template')`.
- **Fragment actions also call `RenderView`**: the same action renders `x/table` or `x/index` (§7). Do not
hand-build HTML strings.
- `procedure` is allowed for redirects (`Redirect('/people')`), which produce no body.
- The JSON API sidecar (`/api`) uses standard `IMVCResponse` factory methods.
---
## 3. Engine Configuration (`EngineConfigU.pas`)
```delphi
procedure ConfigureEngine(AEngine: TMVCEngine);
var
LJWTClaimsSetup: TJWTClaimsSetup;
LWwwPath: string;
begin
LWwwPath := TPath.Combine(AppPath, 'www');
// Controllers
AEngine.AddController(TWebController);
AEngine.AddController(TAPIController);
// Server-Side View Engine
AEngine.SetViewEngine(TMVCTemplateProViewEngine);
// JWT claims setup (what goes into every issued token)
LJWTClaimsSetup := procedure(const JWT: TJWT)
begin
JWT.Claims.Issuer := dotEnv.Env('JWT_ISSUER', 'MyApp');
JWT.Claims.ExpirationTime := Now + OneHour;
JWT.Claims.NotBefore := Now - OneMinute * 5;
JWT.Claims.IssuedAt := Now;
end;
// Middleware — order matters
AEngine.AddMiddleware(TMVCRedirectMiddleware.Create(['/'], '/web'));
AEngine.AddMiddleware(
UseJWTCookieAuthentication(
TAuthentication.Create,
LJWTClaimsSetup,
dotEnv.Env('JWT_SECRET'), // no fallback: a missing key must fail, not sign with a public string
'/login', // POST credentials here
'/logout', // GET or POST to log out
[TJWTCheckableClaim.ExpirationTime,
TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt],
300 // Leeway seconds
)
.SetCookieSecure(dotEnv.Env('JWT_COOKIE_SECURE', True))
);
AEngine.AddMiddleware(
UseFileSessionMiddleware(0, True, TPath.Combine(AppPath, 'sessions'),
dotEnv.Env('SESSION_COOKIE_SECURE', True))); // HttpOnly, Secure
AEngine.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', LWwwPath));
AEngine.AddMiddleware(TMVCCompressionMiddleware.Create);
// Browser-friendly error pages: renders templates/error.html for clients that prefer HTML,
// leaves the JSON error body untouched for API clients. One call — no handler to hand-write.
AEngine.UseExceptionHandler('error', 'MyApp');
end;
```
`UseExceptionHandler(AErrorViewName = 'error'; AAppName = ''; AOptions = [])` renders
`<ViewPath>/error.<DefaultViewFileExtension>` and exposes `status`, `statustext`, `app_name`,
`dmvc_version`, `current_year` to the template. **`error` is empty unless you pass `ehShowDetails`** in
`AOptions` — which you want in development and almost certainly not in production, since it leaks the
exception message to the browser. Reach for `SetExceptionHandler(proc)` only when you need
behaviour this cannot express.
Required units for `EngineConfigU.pas`:
```delphi
uses
TemplatePro,
MVCFramework.View.Renderers.TemplatePro,
MVCFramework.Middleware.JWT,
MVCFramework.Middleware.Session,
MVCFramework.Middleware.Redirect,
MVCFramework.Middleware.StaticFiles,
MVCFramework.Middleware.Compression,
MVCFramework.JWT,
System.IOUtils,
System.DateUtils;
```
---
## 4. Web Controller Pattern
```delphi
unit Controllers.HomeU;
interface
uses MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons;
type
[MVCPath('/web')]
TWebController = class(TMVCController)
protected
procedure OnBeforeAction(Context: TWebContext;
const AActionName: string; var Handled: Boolean); override;
public
[MVCPath]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function Index: String;
[MVCPath('/about')]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function About: String;
// Fragment endpoint — a partial TEMPLATE, rendered with RenderView (see section 9)
[MVCPath('/fragment/clock')]
[MVCHTTPMethod([httpGET])]
function GetClockFragment: String;
end;
implementation
uses System.SysUtils, System.DateUtils, MVCFramework.Logger;
procedure TWebController.OnBeforeAction(Context: TWebContext;
const AActionName: string; var Handled: Boolean);
begin
inherited;
// Set ViewData available to ALL views via baselayout.html
ViewData['app_name'] := 'MyApp';
ViewData['dmvc_version'] := DMVCFRAMEWORK_VERSION;
ViewData['current_year'] := YearOf(Now);
ViewData['page_id'] := AActionName.ToLower; // for aria-current="page"
end;
function TWebController.Index: String;
begin
ViewData['current_date'] := FormatDateTime('dddd, dd mmmm yyyy', Now);
Result := RenderView('home/index'); // renders templates/home/index.html
end;
function TWebController.About: String;
begin
Result := RenderView('about/index');
end;
function TWebController.GetClockFragment: String;
begin
ViewData['time'] := FormatDateTime('hh:nn:ss', Now);
ViewData['date'] := FormatDateTime('dddd, dd mmmm yyyy', Now);
Result := RenderView('home/_clock'); // templates/home/_clock.html — no {{extends}}, so no chrome
end;
```
### Passing objects and lists to templates
```delphi
// Scalar values
ViewData['title'] := 'My Page';
ViewData['count'] := 42;
// Object — template accesses properties with dot notation
ViewData['person'] := lPerson;
// List — template iterates with {{for item in list}}
ViewData['people'] := lPeople; // TObjectList<T>
```
**`ViewData` does NOT own what you put in it.** `TMVCViewDataObject` is a
`TObjectDictionary<string, TValue>` constructed with **no ownership flags**
(`MVCFramework.Commons.pas`) — nothing is freed for you. Free objects yourself, after the render:
```delphi
function TWebController.Customers: String;
var
lCustomers: TObjectList<TCustomer>;
begin
lCustomers := GetCustomers;
try
ViewData['customers'] := lCustomers;
Result := RenderView('customers'); // render happens here, inside the try
finally
lCustomers.Free;
end;
end;
```
A `TDataSet` (e.g. a `TFDMemTable`) can be passed as `ViewData` the same way, and iterated with `{{for}}`.
### Accessing session data and current user in a controller
```delphi
// After UseJWTCookieAuthentication, the session is validated automatically.
// Access claims from the JWT token:
var lUsername := Context.LoggedUser.UserName;
var lRoles := Context.LoggedUser.Roles; // TList<string>
var lCustom := Context.LoggedUser.CustomData['key'];
```
### Form posts — validate and re-render with errors
A posted form is **not** validated automatically (only `[MVCFromBody]` is). The framework sample
`samples/webapp_htmx_forms` shows the pattern (`ReadPerson` in `PeopleSampleU.pas`):
1. Copy `Context.Request.ContentFields` (keys are **lowercase** field names) into an object whose
**properties** carry validator attributes (`MVCFramework.Validators`).
2. `TMVCValidationEngine.Validate(lModel, lErrors)` (`MVCFramework.ValidationEngine`): `lErrors` is nil when
valid, otherwise a `TDictionary<string, string>` you free, keyed by **property name** (`Email`).
3. Copy the messages into your error map with `LowerCase(Key)` — the forms library (`lib/forms_bootstrap5.tpro`)
looks them up by the control's `name`.
4. Valid → save, then `RedirectResponse(...)` (Post/Redirect/Get). Invalid → `ViewData['formModel'] :=
Context.Request.ContentFields` (shows what was typed), `ViewData['formErrors'] := lErrors`, render the same
view with status 422 (`TMVCHTMLResponse` with `StatusCode := HTTP_STATUS.UnprocessableEntity`).
`MVCRequired` passes on `Integer`/`TDate` (0 is a value): parse those fields yourself and add a message when
the text does not convert. Full code (controller and Minimal API): the `dmvcframework` skill,
`reference/validation.md`, "HTML form post".
---
## 5. TemplatePro Template Syntax
### Output a variable
```html
{{:variable_name}}
{{:person.FirstName}}
{{:person.Address.City}}
```
### Template inheritance
Base layout (`baselayout.html`) defines named blocks:
```html
<!DOCTYPE html>
<html>
<head>
<title>{{block "title"}}{{:app_name}}{{endblock}}</title>
</head>
<body>
<main>{{block "body"}}{{endblock}}</main>
{{block "scripts"}}{{endblock}}
</body>
</html>
```
Child template overrides blocks:
```html
{{extends "../baselayout.html"}}
{{block "title"}}My Page{{endblock}}
{{block "body"}}
<h1>Hello from {{:app_name}}</h1>
{{endblock}}
```
- `{{extends "path"}}` — path is relative to the current template file.
- `error.html` extends with `{{extends "baselayout.html"}}` (same folder).
- Subfolder templates use `{{extends "../baselayout.html"}}`.
### Conditionals
```html
{{if page_id|eq,"index"}}aria-current="page"{{endif}}
{{if user_logged_in}}
<p>Welcome, {{:username}}!</p>
{{else}}
<p><a href="/login">Log in</a></p>
{{endif}}
```
Available comparison operators for `|eq`, `|ne`, `|gt`, `|lt`, `|ge`, `|le`:
```html
{{if count|gt,0}}...{{endif}}
{{if role|eq,"admin"}}...{{endif}}
```
### Loops
```html
{{for person in people}}
<li>{{:person.FirstName}} {{:person.LastName}}</li>
{{endfor}}
{{# Loop index: it is a pseudo-property OF THE LOOP VARIABLE, and it is 1-based #}}
{{for item in items}}
<tr>
<td>{{:item.@@index}}</td>
<td>{{:item.Name}}</td>
</tr>
{{endfor}}
{{# ...so it composes with filters #}}
{{if item.@@index|lt,4}}<span class="badge">top</span>{{endif}}
```
### Filters (built-in)
```html
{{:name|uppercase}}
{{:name|lowercase}}
{{:name|capitalize}}
{{:name|rpad,10}} {{:code|lpad,3,"0"}}
{{:date|datetostr}} {{:ts|datetimetostr}} {{:ts|formatdatetime,"dd/mm/yyyy"}}
{{# Filters DMVCFramework adds on top of TemplatePro (MVCFramework.View.Renderers.TemplatePro) #}}
{{:obj|json}} {{:s|urlencode}} {{:list|count}} {{:name|fromquery}}
{{# Comparison filters work on dates too #}}
{{if cust.dob|ge,"2000/01/01"}}...{{endif}}
{{# Output is HTML-escaped by default. Suffix $ to emit raw HTML #}}
{{:myobj.rawhtml$}}
{{# Negation #}}
{{if !person}}<p>Not found</p>{{endif}}
{{:value|default,"N/A"}}
```
### Custom filters (registered in `TemplateProHelpersU.pas`)
```html
{{:value|MyHelper1}}
{{:value|MyHelper2}}
```
### Comments
```html
{{# This is a comment — not rendered in HTML output #}}
```
### Include (partial template)
```html
{{include "partials/card.html"}}
```
---
## 6. Template File Structure
```
bin/templates/
baselayout.html ← base layout; all pages extend this
error.html ← browser error page (extends baselayout.html)
home/
index.html ← extends "../baselayout.html"
about/
index.html ← extends "../baselayout.html"
myfeature/
index.html ← extends "../baselayout.html"
_card.html ← reusable partial (prefix _ by convention)
```
The view path is configured in `.env`:
```ini
dmvc.view_path=templates
dmvc.default.view_file_extension=html
dmvc.view_cache=false # set to true in production for performance
```
`RenderView('home/index')` resolves to `templates/home/index.html`.
---
## 7. HTMX — the patterns this stack actually uses
HTMX is loaded in `baselayout.html`; child templates need no setup.
**For any attribute, trigger modifier, swap mode, event or extension: invoke the `htmx-skill` and read
the linked htmx.org page.** It indexes every page of the official docs. Do not write HTMX attributes
from memory — inheritance rules and swap/trigger modifiers are easy to get subtly wrong. What follows is
only the handful of patterns that carry a *Delphi-side* implication.
### The one pattern that shapes your controller: page or fragment, one action
The same action serves the full page and the HTMX fragment — the shape of the People table in
`samples/webapp_htmx_forms` (`Controllers.PeoplePagesU`, `people/index.html` + `people/table.html`).
Below with customers. Two templates: `customers/index.html` is the page (it `{{extends
"../baselayout.html"}}` and `{{include "table.html"}}`), `customers/table.html` is the part that changes and
has no `{{extends}}`. You do **not** write a separate fragment endpoint, and you do **not** build HTML strings.
```delphi
function TWebController.Customers: String;
var
lCustomers: TObjectList<TCustomer>;
begin
lCustomers := FCustomerService.GetAll;
try
ViewData['customers'] := lCustomers;
// HTMX asks for the table only; a normal request gets the whole page
if Context.Request.IsHTMX and not Context.Request.HXIsBoosted
and not Context.Request.HXIsHistoryRestoreRequest then // uses MVCFramework.HTMX
Result := RenderView('customers/table')
else
Result := RenderView('customers/index');
finally
lCustomers.Free; // ViewData owns nothing
end;
end;
```
A plain browser navigation gets the whole document; an `hx-get` gets just the table. **Test `HXIsBoosted`
too:** a link under `hx-boost` is an HTMX request that expects the whole page — checking `IsHTMX` alone
serves it a fragment without the layout. **And `HXIsHistoryRestoreRequest`:** on a history-cache miss (Back
after the cache is full) htmx re-requests the URL with `HX-Request: true` and swaps the answer into `body`, so
it needs the whole page too. Because the same URL answers with a page or a fragment, also send
`Vary: HX-Request`.
### Triggering the request
```html
<!-- click -->
<button hx-get="/web/customers" hx-target="#list" hx-swap="innerHTML">Load</button>
<!-- on load / polling -->
<div hx-get="/web/stats" hx-trigger="load"></div>
<div hx-get="/web/stats" hx-trigger="every 5s"></div>
<!-- form post: response replaces the form -->
<form hx-post="/web/customers" hx-target="#result" hx-swap="outerHTML">…</form>
<!-- destructive action -->
<button hx-delete="/web/customers/1" hx-confirm="Delete this customer?" hx-target="closest tr"
hx-swap="outerHTML">Delete</button>
```
`hx-put`/`hx-patch`/`hx-delete` reach the matching `[MVCHTTPMethod]` action directly — no method-override
hidden field, unlike a plain HTML form.
### URL in the address bar
```html
<a hx-get="/web/customers" hx-target="#main" hx-push-url="true">Customers</a>
```
Without `hx-push-url`, the fragment loads but the URL does not change and the page is not bookmarkable.
### Loading indicator
`htmx-request` is added to the element for the duration of the request; style it in `style.css`:
```css
.htmx-request { opacity: 0.5; transition: opacity 0.3s; }
```
### Live updates: SSE, not polling
For server-pushed updates use Server-Sent Events. Do not hand-roll the stream in the action — inherit
`TMVCSSEController` (see the `dmvcframework` skill, `reference/sse.md`) and consume it with the htmx SSE
extension (`https://htmx.org/extensions/sse/`).
## 8. HTMX response helpers (from the Delphi side)
`uses MVCFramework.HTMX;` — it adds typed helpers to `Context.Request` / `Context.Response`.
**Use them; do not hand-write `HX-*` header strings** (a typo in a header name fails silently).
```delphi
// full page reload, like window.location — e.g. after login
Context.Response.HXSetRedirect('/web/dashboard');
// HTMX-aware navigation: no full reload, content swapped, history updated
Context.Response.HXSetLocation('/web/products');
// URL bar only
Context.Response.HXSetPushUrl('/web/items/' + ItemID);
Context.Response.HXSetReplaceUrl('/web/items/' + ItemID);
// client-side events (see the dmvcframework-ui skill for showToast — it takes HTML)
Context.Response.HXTriggerClientEvent('refreshCart');
Context.Response.HXTriggerClientEvents(['refreshCart', 'updateBadge']);
// With a payload. It is serialized as a JSON *value*: a string arrives as evt.detail.value.
// Pass an object when you want named properties (evt.detail.FirstName, ...).
Context.Response.HXTriggerClientEvent('showMessage', 'Customer saved');
Context.Response.HXTriggerClientEvent('customerSaved', lCustomer);
// when it fires: etReceived (default), or after swap / after settle — pass the TClientEventType
Context.Response.HXTriggerClientEvent('initDatePicker', etSettled);
// override the swap / target the server chose on the client
Context.Response.HXSetReswap(TSwapOption.soOuterHTML);
Context.Response.HXSetRetarget('#other-element');
// tell the client to refresh the page
Context.Response.HXSetPageRefresh;
```
On the request side: `Context.Request.IsHTMX`, `HXIsBoosted`, `HXGetTarget`, `HXGetPrompt`,
`HXGetTriggeringEventAsJSON`.
To do nothing (leave the DOM as it is), answer 204:
```delphi
Context.Response.StatusCode := HTTP_STATUS.NoContent;
Result := '';
```
---
## 9. Fragments — a template, never a string
A fragment is a **template**, rendered by the same view engine as a full page. Never concatenate HTML in
Delphi, and never call `TTProCompiler.CompileAndRender` yourself — that bypasses the engine, its cache and
its filters, and it forces you to hand-escape every value.
Two ways to serve one, both `RenderView`:
**a) Same action, page or fragment.** Preferred: one URL, bookmarkable, works without JS.
```delphi
function TWebController.Items: String;
var
lItems: TObjectList<TItem>;
begin
lItems := FItemService.GetAll;
try
ViewData['items'] := lItems;
if Context.Request.IsHTMX and not Context.Request.HXIsBoosted then
Result := RenderView('items/table')
else
Result := RenderView('items/index');
finally
lItems.Free; // ViewData owns nothing
end;
end;
```
**b) A dedicated fragment action**, when the fragment has no page of its own (a row after an edit, a
search-results list). Point it at a partial template and skip the layout:
```delphi
[MVCPath('/fragment/item-list')]
[MVCHTTPMethod([httpGET])]
function TWebController.GetItemList: String;
var
lItems: TObjectList<TItem>;
begin
lItems := FItemService.GetAll;
try
ViewData['items'] := lItems;
Result := RenderView('items/_list'); // a template with no {{extends}} → no chrome
finally
lItems.Free;
end;
end;
```
```html
{{# templates/items/_list.html — the fragment, and reusable via {{include}} from the page #}}
<ul id="item-list">
{{for item in items}}
<li id="item-{{:item.ID}}">
{{:item.Name}}
<a hx-delete="/web/items/{{:item.ID}}" hx-target="#item-{{:item.ID}}"
hx-swap="outerHTML" hx-confirm="Delete?">[x]</a>
</li>
{{endfor}}
</ul>
```
TemplatePro escapes `{{:item.Name}}` for you — there is no `HTMLEscape` call to forget. The same partial
can be `{{include}}`d by the full page, so page and fragment never drift apart.
---
## 10. Cookie JWT Authentication
### `AuthenticationU.pas` — the three handler methods
```delphi
type
TAuthentication = class(TInterfacedObject, IMVCAuthenticationHandler)
protected
procedure OnRequest(
const AContext: TWebContext;
const ControllerQualifiedClassName, ActionName: string;
var AuthenticationRequired: Boolean);
procedure OnAuthentication(
const AContext: TWebContext;
const UserName, Password: string;
UserRoles: TList<string>;
var IsValid: Boolean;
const SessionData: TSessionData);
procedure OnAuthorization(
const AContext: TWebContext;
UserRoles: TList<string>;
const ControllerQualifiedClassName, ActionName: string;
var IsAuthorized: Boolean);
end;
```
#### `OnRequest` — which routes need auth
```delphi
procedure TAuthentication.OnRequest(
const AContext: TWebContext;
const ControllerQualifiedClassName, ActionName: string;
var AuthenticationRequired: Boolean);
begin
// Protect everything under TAdminController
AuthenticationRequired :=
ControllerQualifiedClassName.Contains('TAdminController');
end;
```
#### `OnAuthentication` — validate credentials
```delphi
procedure TAuthentication.OnAuthentication(
const AContext: TWebContext;
const UserName, Password: string;
UserRoles: TList<string>;
var IsValid: Boolean;
const SessionData: TSessionData);
begin
// validate the credentials against your user store
IsValid := ValidateFromDB(UserName, Password);
if IsValid then
begin
UserRoles.Add('user');
if IsAdmin(UserName) then
UserRoles.Add('admin');
SessionData.AddOrSetValue('username', UserName);
end;
end;
```
#### `OnAuthorization` — check roles per action
```delphi
procedure TAuthentication.OnAuthorization(
const AContext: TWebContext;
UserRoles: TList<string>;
const ControllerQualifiedClassName, ActionName: string;
var IsAuthorized: Boolean);
begin
if ActionName.StartsWith('Admin') then
IsAuthorized := UserRoles.Contains('admin')
else
IsAuthorized := True;
end;
```
### Login form pattern (browser-facing)
```html
<form hx-post="/login" hx-swap="none"
hx-on::after-request="if (event.detail.successful) window.location.href = '/web'">
<input name="jwtusername" type="text" required>
<input name="jwtpassword" type="password" required>
<button type="submit">Login</button>
</form>
```
The `/login` route is handled by `UseJWTCookieAuthentication`, which a plain HTML form does not fit:
- **Field names.** Form data is read only as `jwtusername` / `jwtpassword`
(`TMVCJWTDefaults.USERNAME_HEADER` / `PASSWORD_HEADER`); `username` / `password` are accepted only in a
JSON body. A form posting `username` gets **401**.
- **No redirect.** On success it sets the HTTP-only cookie and answers `200` with JSON `{"token": "..."}`.
The page must navigate by itself — hence `hx-swap="none"` and the `after-request` handler above. A plain
`<form method="POST">` would leave the user looking at the JSON. A failed login answers 401.
### Reading the current user in a controller
```delphi
function TWebController.Profile: String;
begin
ViewData['username'] := Context.LoggedUser.UserName;
ViewData['is_admin'] := Context.LoggedUser.Roles.Contains('admin');
Result := RenderView('profile/index');
end;
```
---
## 11. Custom TemplatePro Filters (`TemplateProHelpersU.pas`)
### Registering filters
The wizard already generates `TemplateProContextConfigure` in `TemplateProHelpersU.pas` and calls it once in
`Boot` (`BootConfigU.pas`). **Add your `AddFilter` calls inside its existing handler — never assign
`TTProConfiguration.OnContextConfiguration` a second time.** The generated handler also sets
`CompiledTemplate.IncludeRootPath`, which keeps a dynamic `{{include}}` built from request data inside the
views folder; a replacement handler silently drops that protection.
```delphi
TTProConfiguration.OnContextConfiguration := procedure(const CompiledTemplate: ITProCompiledTemplate)
begin
CompiledTemplate.IncludeRootPath := lViewPath; // generated - keep it
CompiledTemplate.AddFilter('currency', CurrencyFilter);
CompiledTemplate.AddFilter('timeago', TimeAgoFilter);
// ... rest of the generated handler
end;
```
### Writing a filter
```delphi
uses System.Rtti, TemplatePro, TemplatePro.Types;
function CurrencyFilter(const Value: TValue;
const Parameters: TArray<TFilterParameter>): TValue;
begin
Result := FormatCurr('€#,##0.00', Value.AsExtended);
end;
function TimeAgoFilter(const Value: TValue;
const Parameters: TArray<TFilterParameter>): TValue;
var
lDelta: TTimeSpan;
begin
lDelta := TTimeSpan.Subtract(Now, Value.AsExtended);
if lDelta.TotalMinutes < 1 then
Result := 'just now'
else if lDelta.TotalHours < 1 then
Result := Format('%d minutes ago', [Round(lDelta.TotalMinutes)])
else
Result := Format('%d hours ago', [Round(lDelta.TotalHours)]);
end;
```
Template usage:
```html
{{:price|currency}}
{{:created_at|timeago}}
```
### Dynamic external data source (`OnGetValue`)
```delphi
CompiledTemplate.OnGetValue :=
procedure(const DataSource, Members: string;
var Value: TValue; var Handled: Boolean)
begin
if SameText(DataSource, 'config') then
begin
Value := GetConfigValue(Members); // e.g. 'config.siteName'
Handled := True;
end;
end;
```
Template: `{{:config.siteName}}`
---
## 12. DI / Services Pattern (`ServicesU.pas`)
### Registering services
```delphi
procedure RegisterServices(Container: IMVCServiceContainer);
begin
Container.RegisterType(
TPeopleService, IPeopleService, TRegistrationType.SingletonPerRequest);
Container.RegisterType(
TProductService, IProductService, TRegistrationType.Singleton);
end;
```
Registration types:
- `SingletonPerRequest` — one instance per HTTP request (most common for web)
- `Singleton` — one instance for the lifetime of the process
- `Transient` — new instance on every `Resolve` call
### Resolving in a controller
```delphi
function TWebController.Products: String;
var
lService: IProductService;
lProducts: TObjectList<TProduct>;
begin
lService := Context.ServiceContainerResolver.Resolve(TypeInfo(IProductService)) as IProductService;
lProducts := lService.GetAll;
try
ViewData['products'] := lProducts;
Result := RenderView('products/index');
finally
lProducts.Free; // ViewData owns nothing
end;
end;
```
### Registering — once, in the `.dpr`
`TWebContext` has **no** `ServiceContainer` property (only the read-only `ServiceContainerResolver`).
Registration happens once, before the server starts, and `Build` is mandatory:
```delphi
RegisterServices(DefaultMVCServiceContainer);
DefaultMVCServiceContainer.Build;
```
Prefer injecting the service rather than resolving it by hand — `[MVCInject]` on the controller's
constructor or on an action parameter. See the `dmvcframework` skill, `reference/di-and-repository.md`.
---
## 13. API JSON Sidecar (`Controllers.APIU.pas`)
The web application includes a JSON API controller alongside the web controller.
```delphi
[MVCPath('/api')]
TAPIController = class(TMVCController)
public
[MVCPath('/server/info')]
[MVCHTTPMethod([httpGET])]
function GetServerInfo: IMVCResponse;
end;
function TAPIController.GetServerInfo: IMVCResponse;
begin
Result := OkResponse(
StrDict(
['application', 'serverTime', 'dmvcVersion'],
['MyApp', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Now), DMVCFRAMEWORK_VERSION]
)
);
end;
```
`StrDict(keys, values)` returns a **`TMVCStringDictionary`** (string→string), serialized as a flat JSON
object. For a dictionary of *objects*, use `ObjectDict()` — that is the one returning `IMVCObjectDictionary`.
---
## 14. Static Files
Static files are served from `bin/www/` at the `/static` URL prefix.
```
bin/www/
css/style.css → /static/css/style.css
favicon.ico → /static/favicon.ico
favicon-32x32.png → /static/favicon-32x32.png
site.webmanifest → /static/site.webmanifest
```
Middleware declaration:
```delphi
AEngine.AddMiddleware(
TMVCStaticFilesMiddleware.Create('/static', TPath.Combine(AppPath, 'www')));
```
In templates: `<link rel="stylesheet" href="/static/css/style.css">`
---
## 15. Environment Configuration (`bin/.env`)
Full reference — profiles, precedence, `${}`/`$[]` syntax, `RequireKeys`, the `Boot` order:
`dmvcframework` skill, `reference/dotenv.md`.
Key settings for the webapp template:
```ini
# Server
dmvc.server.port=8080
# View engine
dmvc.view_path=templates
dmvc.default.view_file_extension=html
dmvc.view_cache=false # true in production (caches compiled templates)
# JWT Cookie Auth - the wizard writes a random 384-bit key here; give every environment its own
# (e.g. openssl rand -hex 48) and keep .env out of version control
JWT_SECRET=<generated>
JWT_ISSUER=MyApp
# Set to false only for local development over plain HTTP.
JWT_COOKIE_SECURE=true
SESSION_COOKIE_SECURE=true
# Logging
logger.config.file=loggerpro.json
```
Profile-specific overrides: `.env.test`, `.env.prod` (layered by `UseProfile`).
---
## 16. Scaffolding Workflow — Adding a New Page
### Step 1 — Add actions to the web controller
In `Controllers.HomeU.pas` (or a new controller unit):
```delphi
[MVCPath('/products')]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function Products: String;
[MVCPath('/products/fragment/list')]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function GetProductListFragment: String;
```
```delphi
function TWebController.Products: String;
begin
ViewData['page_id'] := 'products';
Result := RenderView('products/index');
end;
function TWebController.GetProductListFragment: String;
var
lProducts: TObjectList<TProduct>;
begin
lProducts := fProductService.GetAll;
try
ViewData['products'] := lProducts; // ViewData owns nothing — free it below
Result := RenderView('products/_list'); // partial template, no {{extends}}
finally
lProducts.Free;
end;
end;
```
### Step 2 — Create the template
Create `bin/templates/products/index.html`:
```html
{{extends "../baselayout.html"}}
{{block "title"}}Products{{endblock}}
{{block "body"}}
<div class="hero">
<h1>Products</h1>
</div>
<div hx-get="/web/products/fragment/list"
hx-trigger="load"
hx-swap="innerHTML">
<p aria-busy="true">Loading products...</p>
</div>
{{endblock}}
```
### Step 3 — Add nav link in `baselayout.html`
```html
<li>
<a href="/web/products"
{{if page_id|eq,"products"}}aria-current="page"{{endif}}>
Products
</a>
</li>
```
### Step 4 — Register new controller (if separate unit)
In `EngineConfigU.pas`:
```delphi
AEngine.AddController(TProductsController);
```
---
## 17. Common Pitfalls
| Pitfall | Fix |
|---------|-----|
| Action is `procedure` instead of `function` | All web actions must be `function`: return `String` for HTML, `IMVCResponse` for JSON. |
| Freeing nothing after `ViewData['x'] := lObj` | `ViewData` owns nothing. Free the object yourself in a `finally` after `RenderView`. |
| Hand-building HTML for HTMX fragments | Same action, two templates: `IsHTMX and not HXIsBoosted and not HXIsHistoryRestoreRequest` → `RenderView('x/table')`, otherwise `RenderView('x/index')`. |
| Writing `HX-*` headers by hand | `uses MVCFramework.HTMX` — `Response.HXSetRedirect/HXSetPushUrl/HXSetReswap/HXSetRetarget/HXTriggerClientEvent`, `Request.IsHTMX/HXGetTarget`. |
| `ViewData` not set before `RenderView` | `OnBeforeAction` sets shared data; page-specific data must be set in the action function body before `Result := RenderView(...)`. |
| Template extends wrong relative path | `home/index.html` → `{{extends "../baselayout.html"}}`. `error.html` (same folder as baselayout) → `{{extends "baselayout.html"}}`. |
| `JWT_COOKIE_SECURE=true` with HTTP | Cookie won't be sent by the browser over plain HTTP. Use `false` during local dev, `true` in production with HTTPS. |
| View cache enabled during development | `dmvc.view_cache=true` caches compiled templates — template changes won't appear until restart. Use `false` in dev. |
| `SetExceptionHandler` handles all requests | Check `WebContext.Request.ClientPreferHTML` first and `Exit` if false — otherwise API clients get an HTML error page. |
| Static file not found | File must be under `bin/www/`. The URL prefix is `/static`, not `/www`. Check middleware order — static files should be declared after redirect and auth. |
| Template variable silently empty | TemplatePro doesn't error on missing keys by default. Add a breakpoint after `ViewData[...]` assignments to verify they are set. |
| `{{for item in list}}` doesn't iterate | The list must be a `TObjectList<T>` or array registered in ViewData. Ensure the service returns a non-nil, non-empty list. |
| HTMX response headers ignored | HTMX checks for `HX-*` headers only on responses to HTMX requests (requests with `HX-Request: true` header). Verify the button/div has a `hx-*` trigger attribute. |
| `hx-swap-oob` elements not swapped | Each oob element must have an `id` that matches an existing DOM element. HTMX swaps by ID. |
---
## 18. Key Units Reference
| Unit | Purpose |
|------|---------|
| `MVCFramework.View.Renderers.TemplatePro` | `TMVCTemplateProViewEngine` |
| `TemplatePro` | `TTProCompiler.CompileAndRender`, `TTProConfiguration`, `ITProCompiledTemplate` |
| `TemplatePro.Types` | `TFilterParameter` |
| `MVCFramework.Middleware.JWT` | `UseJWTCookieAuthentication`, `UseJWTMiddleware` |
| `MVCFramework.Middleware.Session` | `UseFileSessionMiddleware` |
| `MVCFramework.Middleware.Redirect` | `TMVCRedirectMiddleware` |
| `MVCFramework.Middleware.StaticFiles` | `TMVCStaticFilesMiddleware` |
| `MVCFramework.Middleware.Compression` | `TMVCCompressionMiddleware` |
| `MVCFramework.JWT` | `TJWT`, `TJWTClaimsSetup`, `TJWTCheckableClaim` |
| `MVCFramework.Commons` | `TMVCMediaType`, `AppPath`, `DMVCFRAMEWORK_VERSION` |
| `MVCFramework.Container` | `IMVCServiceContainer`, `TRegistrationType` |
| `MVCFramework.DotEnv` | `dotEnv`, `dotEnvConfigure`, `NewDotEnv` |
| `LoggerPro.Config` | `TLoggerProConfig.BuilderFromJSONFile` |
---
## 19. Sample Reference Project
In the DelphiMVCFramework repository (https://github.com/danieleteti/delphimvcframework/tree/master/samples):
| Sample | What it shows |
|--------|---------------|
| `samples/htmx_website_with_templatepro/` | TemplatePro + HTMX website: baselayout with blocks, partials, custom filters. Its `ispage` flag predates the wizard's page/table pattern and ignores `hx-boost` — follow the wizard |
| `samples/serversideviews_templatepro/` | The view engine in depth: inheritance, filters, `RenderViews`, CSV/non-HTML views, dataset in `ViewData` |
| `samples/wizard_showcase/web/` | Minimal-API flavour of a web app: `.AsWeb`, sessions, `RenderView` from lambda handlers |
Or generate one with the IDE wizard (preset **Web Application**) — that is the layout this skill assumes.