# Dmvcframework Webapp

> 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".

- Skill: `danieleteti/dmvcframework-webapp` (Agent Skill)
- Install (CLI): `npx skillmds@latest add danieleteti/dmvcframework-webapp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/danieleteti/dmvcframework-webapp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: danieleteti (https://skillmd.com/u/danieleteti)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/danieleteti/dmvcframework-webapp

---


# 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   → RenderView(same template)
  │    (HTMX partial)                          ViewData['ispage'] := not Request.IsHTMX
  │                                            → layout drops its own chrome
  │
  └─ 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 serves the full page and the HTMX fragment;
  the layout suppresses its own chrome:
  `ViewData['ispage'] := not Context.Request.IsHTMX;` + `{{if ispage}}<!DOCTYPE html>…{{endif}}` in
  `baselayout.html`. 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', 'change-me'),
      '/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', False))
  );
  AEngine.AddMiddleware(
    UseFileSessionMiddleware(0, False, TPath.Combine(AppPath, 'sessions')));
  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'];
```

---

## 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 controller flags which one; the layout
suppresses its own chrome. You do **not** write a separate fragment endpoint, and you do **not** build
HTML strings.

```delphi
function TWebController.Customers: String;
begin
  ViewData['ispage']    := not Context.Request.IsHTMX;   // uses MVCFramework.HTMX
  ViewData['customers'] := lCustomers;
  Result := RenderView('customers');
end;
```

```html
{{# baselayout.html #}}
{{if ispage}}<!DOCTYPE html><html><head>…</head><body><nav>…</nav>{{endif}}
{{block "body"}}{{endblock}}
{{if ispage}}</body></html>{{endif}}
```

A plain browser navigation gets the whole document; an `hx-get` gets just the block. One template, one action.

### 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)
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['ispage'] := not Context.Request.IsHTMX;
    ViewData['items']  := lItems;
    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 method="POST" action="/login">
  <input name="username" type="text" required>
  <input name="password" type="password" required>
  <button type="submit">Login</button>
</form>
```

The `/login` route is handled automatically by `UseJWTCookieAuthentication`.
On success, it sets an HTTP-only cookie and redirects to the referrer or `/web`.

### 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

```delphi
procedure TemplateProContextConfigure;
begin
  TTProConfiguration.OnContextConfiguration :=
    procedure(const CompiledTemplate: ITProCompiledTemplate)
    begin
      CompiledTemplate.AddFilter('currency', CurrencyFilter);
      CompiledTemplate.AddFilter('timeago', TimeAgoFilter);
    end;
end;
```

Call `TemplateProContextConfigure` once in `Boot` (inside `BootConfigU.pas`).

### 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;
begin
  lService := Context.ServiceContainerResolver.Resolve(TypeInfo(IProductService)) as IProductService;
  ViewData['products'] := lService.GetAll;
  Result := RenderView('products/index');
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
JWT_SECRET=                   # leave empty; set it in the deployment environment
JWT_ISSUER=MyApp
JWT_COOKIE_SECURE=false       # true in production (HTTPS only cookie)

# 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 | Use the same `RenderView` + `ViewData['ispage'] := not Request.IsHTMX` and let the layout drop its chrome. |
| 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: one action serves page and fragment via `ispage`, baselayout with blocks, partials, custom filters |
| `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.

