Skip to content

Editable content collections

Use a template-resolver collection for text that editors should change without a deploy: mail bodies, prompts, legal copy, or canned replies. A collection appears in navigation and gets list, detail, and save handlers.

The collection definition controls the editor and access boundary. Individual entries do not redefine variableSchema; declare variables at the collection mount so every editor sees the same chips and preview examples.

Two decisions per collection: who owns the entries, and how they are edited.

The default is one shared set per tenant. Every user with the configured role sees the same entries.

createTemplateResolverFeature({
collections: [
{
id: "snippets",
kind: "text-block",
access: { roles: ["Admin", "Editor"] },
nav: { parent: "content" },
contentFormat: "rich",
},
],
});

access belongs to the mount because only the app knows its role names. Each collection receives its own handlers and access rule.

Set ownership: "user" when each user should edit a private set of entries. Mail signatures are one example.

createTemplateResolverFeature({
collections: [
{
id: "signatures",
kind: "text-block",
ownership: "user",
access: { roles: ["Agent"] },
nav: { parent: "settings" },
contentFormat: "rich",
},
],
});

Two things come with that choice:

  • The rows live in user-content-entry and count as personal data. Mount template-resolver-user-data as well, or the boot check stops the app.
  • The entity needs a schema migration. Generate it with kumiko-schema.

contentFormat selects the editor:

  • plain renders a text area.
  • rich renders the built-in WYSIWYG with bold, italic, headings, lists, and links. It stores HTML. Tables and image uploads are not included.

Both editors can show variables from variableSchema as insertable chips and preview the result with sample data.

The rich editor: bold/italic/heading/list toolbar, a Preview button, and the collection's variables as chips below the content field. The sidebar shows both collections, Snippets shared by the tenant, Signatures owned per user.

If neither editor fits, register a component for that format in a client feature:

export const myClientFeature = {
name: "my-app",
contentEditors: { rich: MyEditor },
};

The collection ID is app-owned, so its navigation label is app-owned too. Put the translation keys in a feature included in APP_FEATURES; a client-only screen feature does not reach the server-side boot validator.

r.translations({
keys: {
"templateResolver:nav.signatures": { de: "Signaturen", en: "Signatures" },
},
});