docs(agents): rendering-ownership rule — templ renders, JS decides what shows

Codifies the standing rule discussed for the templ/JS split: content
derived from server data is rendered by templ and refreshed via templ
partials (htmx.ajax), pure client-local state may render in TS/Alpine,
web/src is TypeScript-only, and inline <script> in templ files requires
a pre-paint justification (theme_flash_guard.templ only). Lists the
sanctioned JS islands (reader, Chart.js, toasts, password validation).

Also refreshes gotcha #1: the 'harmlessly masked' aside is stale — the
quoted-attribute sweep fixed every occurrence; the note now points at
templates.JSONArg for free text.
This commit is contained in:
John O'Keefe committed 2026-10-18 12:28:25 -04:00
1 parent b89eac22e3
commit 1fdf306f2d
1 file changed
+33 -3
+33 -3
View File
@@ -4,6 +4,34 @@ Standing rules for any agent or developer working in this repo.
Project-wide context lives in the sibling repos' AGENTS.md files Project-wide context lives in the sibling repos' AGENTS.md files
(../bookhoard-app/AGENTS.md is the Android client's). (../bookhoard-app/AGENTS.md is the Android client's).
## Rendering ownership (templ vs TypeScript) — ALWAYS apply
The point of templ + htmx + Alpine is: the server renders the UI, and
JavaScript is kept minimal. The rule that keeps it that way:
> **JavaScript decides what shows. templ decides what it looks like.**
- Content derived from **server data** (DB rows, search results, match
lists, conflict cards, any list/card/badge/table) is rendered by
templ. When it changes client-side, fetch a **templ partial** from an
SSR route and swap it in (`htmx.ajax(...)`), never rebuild it from a
JSON payload with `innerHTML` string templates.
- **Pure functions of client-local state** (open/closed panels, tabs,
password strength from a typed value, toasts) may render in
TS/Alpine — no server round trip needed.
- `web/src` is **TypeScript only** — no new `.js` source files.
Vendored third-party and vite build output under `web/static` are
exempt.
- Inline `<script>` in a `.templ` file needs a reason a bundled module
cannot do the job (pre-paint execution). Currently sanctioned:
`theme_flash_guard.templ` only.
Sanctioned JS islands (engines, not UI renderers — exempt from the
above): the reader (foliate-js + `web/src/reader/`), Chart.js graphs in
stats, toasts, password validation. Anything else that renders app UI
in TypeScript is a defect; if a templ twin of it exists, one of the two
must be deleted.
## Library visibility (family-lens rule) — ALWAYS apply ## Library visibility (family-lens rule) — ALWAYS apply
Per-user library visibility (`library_visibility` table, default-ALLOW: Per-user library visibility (`library_visibility` table, default-ALLOW:
@@ -96,9 +124,11 @@ of a server bug: stop and flag the user.
values** — it emits the literal text. `x-init="f('{ col.ID }')"` values** — it emits the literal text. `x-init="f('{ col.ID }')"`
shipped a request to `/api/collections/{ collection.ID }` found by shipped a request to `/api/collections/{ collection.ID }` found by
the request logs; the whole attribute value must BE an expression: the request logs; the whole attribute value must BE an expression:
`x-init={ "f('" + col.ID + "')" }`. Note `x-init={ "f('" + col.ID + "')" }`. (Every occurrence of this bug —
`class="theme-{ user.Theme }"` has always been literal (harmlessly including `class="theme-{ user.Theme }"` on 22 page bodies and 26
masked by the JS theme engine). attribute interpolations — was swept and fixed in 2026-10; when
adding attributes with dynamic values, the whole value is an
expression, and free text goes through `templates.JSONArg`.)
2. **SettingsRegistry caches**: writes must go through the settings 2. **SettingsRegistry caches**: writes must go through the settings
API (Reload on save). Direct SQL writes to system_settings leave API (Reload on save). Direct SQL writes to system_settings leave
the cache stale until the next API-path write — never "preview" the cache stale until the next API-path write — never "preview"