template-resolver
Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the kind (notification, mail-html, document-pdf, ai-prompt, text-block, image-snapshot).
Reading is one call — ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale }). It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes.
Text an editor should be able to change belongs in a collection, declared at mount: createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] }). It appears in the navigation, and access is part of the mount because a bundled feature cannot know the host’s roles. Each collection gets its own <id>-list / <id>-item / <id>-set handlers, so the dispatcher enforces the separation.
How a collection is edited follows from contentFormat: plain gives a text area, rich a small WYSIWYG (bold, italic, headings, lists, links). Both offer the collection’s variableSchema as insertable chips and a preview rendered with sample data — an editor sees what {{firstName}} becomes without sending a mail. An app can register its own editor for a format and wins over the built-in one.
A collection is tenant-wide by default. With ownership: "user" every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate user-content-entry entity and count as user data, so mounting one also requires the template-resolver-user-data feature and a migration on the app side.
Replaces the former text-content feature; its blocks now live here as kind text-block.
Quick example
Section titled “Quick example”From recipes-legal-pages — the smallest working mount:
// Legal-Pages Sample//// DACH-Apps (DE/AT/CH) sind verpflichtet ein Impressum (TMG/DDG §5) und// eine Datenschutzerklärung (DSGVO Art. 13) öffentlich zugänglich zu// haben. Das ist 1) für jede App identisch und 2) nervig manuell pro// App neu zu basteln.//// Lösung: zwei opt-in bundled-features kombinieren://// - `template-resolver` — der Content-Store (Entity `template-resource`,// kind `text-block` mit slug+locale+title+content, scoped per Tenant).// Auch nutzbar für FAQ, About, ToS, Marketing-Snippets — nicht// legal-spezifisch.//// - `legal-pages` — opt-in-Wrapper darauf, der vier feste Public-// Routes (`/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`,// `/legal/privacy`) registriert und Markdown→HTML rendered. Plus// Boot-Check der in Production hart fehlt wenn die DE-Pflicht-Blocks// fehlen.//// Tenant-Modell: 1 App = X Tenants = 1 Impressum. Alle Subdomains// teilen sich die SYSTEM_TENANT_ID-Version. Wer pro-Tenant-Impressums// braucht, muss sein eigenes Routing davorsetzen oder die by-slug-// query mit tenant-specific tenantId nutzen.//// Voraussetzungen für Production:// - `anonymousAccess` muss in runProdApp/runDevApp konfiguriert sein// (defaultTenantId = SYSTEM_TENANT_ID), sonst antworten die// legal-pages-Routes mit 503// - `extraContext.templateResolver = createTemplateResolverApi(db)` muss// gewired sein, sonst wirft der Boot-Check mit Wiring-Hinweis// - Beim ersten Boot müssen die TextBlocks geseedet sein —// template-resolver/seeding `seedTextBlock` oder via API// `template-resolver:write:set` mit TenantAdmin-Token
import { createLegalPagesFeature, LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES,} from "@cosmicdrift/kumiko-bundled-features/legal-pages";import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
// Beide Features aktivieren — template-resolver ist Foundation, legal-pages// requires sie. r.requires("template-resolver") greift automatisch im// legal-pages-Feature.export const templateResolverFeature = createTemplateResolverFeature();export const legalPagesFeature = createLegalPagesFeature();
// Re-exports für Tests + andere Demosexport { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES };📄 On GitHub: samples/recipes/legal-pages/src/feature.ts
Live preview
Section titled “Live preview”
How it fits
Section titled “How it fits”The write commands this feature provides (Provides).
flowchart TB
n_template_resolver["template-resolver"]
subgraph how_provides["Provides"]
n_cmd_template_resolver_write_archive(["archive"])
n_cmd_template_resolver_write_publish(["publish"])
n_cmd_template_resolver_write_reply_snippets_set(["reply-snippets-set"])
n_cmd_template_resolver_write_set(["set"])
n_cmd_template_resolver_write_signatures_set(["signatures-set"])
n_cmd_template_resolver_write_upsert_system(["upsert-system"])
n_cmd_more(["+1 more"])
end
n_template_resolver --> n_cmd_template_resolver_write_archive
n_template_resolver --> n_cmd_template_resolver_write_publish
n_template_resolver --> n_cmd_template_resolver_write_reply_snippets_set
n_template_resolver --> n_cmd_template_resolver_write_set
n_template_resolver --> n_cmd_template_resolver_write_signatures_set
n_template_resolver --> n_cmd_template_resolver_write_upsert_system
n_template_resolver --> n_cmd_more
Provides — write commands this feature registers (dispatch them through the command bus):
template-resolver:write:archivetemplate-resolver:write:publishtemplate-resolver:write:reply-snippets-settemplate-resolver:write:settemplate-resolver:write:signatures-settemplate-resolver:write:upsert-systemtemplate-resolver:write:upsert-tenant
Getting started
Section titled “Getting started”Start with recipes-legal-pages for a step-by-step walkthrough with runnable code and integration tests.
Dependencies
Section titled “Dependencies”- Requires: none
- Activation: always on (not toggleable)