Editable content collections
Mail bodies, canned replies, AI prompts, legal copy: text that should change without a deploy. The template-resolver stores all of it. A collection is what makes a slice of it editable — it appears in the navigation and comes with list, detail and save handlers.
Two decisions per collection: who owns the entries, and how they are edited.
Tenant-wide: one set everyone shares
Section titled “Tenant-wide: one set everyone shares”The default. Every user with the right role sees and edits the same entries — help texts, legal pages, mail templates for the whole tenant.
createTemplateResolverFeature({ collections: [ { id: "snippets", kind: "text-block", access: { roles: ["Admin", "Editor"] }, nav: { parent: "content" }, contentFormat: "rich", }, ],});access sits at the mount, not in the bundled feature — only your app
knows its own role names. Each collection gets its own
snippets-list / snippets-item / snippets-set handlers carrying
exactly that rule, so the dispatcher keeps two collections apart even
when both hold text-block records.
Per user: everyone keeps their own
Section titled “Per user: everyone keeps their own”Set ownership: "user" and each user edits their own entries. Mail
signatures are the obvious case — two agents both have a standard
signature, and neither sees the other’s.
createTemplateResolverFeature({ collections: [ { id: "signatures", kind: "text-block", ownership: "user", access: { roles: ["Agent"] }, nav: { parent: "settings" }, contentFormat: "rich", }, ],});Two things come with that choice:
- Those rows live in their own entity (
user-content-entry) and count as personal data. Mounttemplate-resolver-user-dataalongside, or the boot check stops the app. - The entity needs a migration on your side.
kumiko-schemagenerates it; nothing to write by hand.
Which editor the collection gets
Section titled “Which editor the collection gets”contentFormat decides:
plain— a text area. Right for AI prompts and anything where markup would only get in the way.rich— a small WYSIWYG: bold, italic, headings, lists, links. Stores HTML, because mail and PDF rendering want HTML anyway. No tables, no image uploads.
Both show the collection’s variableSchema as chips you click to
insert, and a preview rendered with sample data — an editor sees what
{{firstName}} turns into without sending a mail.
If neither fits, register your own component for a format in your client feature and it wins over the built-in one:
export const myClientFeature = { name: "my-app", contentEditors: { rich: MyEditor },};The labels are yours
Section titled “The labels are yours”You pick the collection id, so you also ship its navigation label. The
bundled feature cannot translate a name it never chose. Put the keys in
a feature that is part of APP_FEATURES — a client-side screens
feature never reaches the server-side boot validator, and the missing
key stays invisible until someone opens the nav.
r.translations({ keys: { "templateResolver:nav.signatures": { de: "Signaturen", en: "Signatures" }, },});