Skip to content

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.

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 Demos
export { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES };

📄 On GitHub: samples/recipes/legal-pages/src/feature.ts

template-resolver feature preview

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):

Start with recipes-legal-pages for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: none
  • Activation: always on (not toggleable)