Content & SEO
Four features for public-facing content, from editable strings to
tenant-branded pages. They also provide the sitemap, llms.txt, and
JSON-LD and Open Graph metadata that search and answer engines use.
template-resolver
Section titled “template-resolver”Status: ✅ Stable
What: The one content store, one read_template_resources row per
(tenantId, slug, kind, locale). Editable Markdown text lives here as kind
text-block; the same entity also holds mail and notification templates, PDF
document templates and AI prompts. The building block under legal-pages, and
the marketing-copy seam the apex-landing sample pulls its hero headline and
tagline from.
How it works: r.writeHandler/r.queryHandler convention CRUD, plus an
anonymous by-tenant listing query (visual-tree sidebar, marketing pages
that render editable blocks without a JWT). Cross-feature reads go through
extraContext.templateResolver (createTemplateResolverApi), never a code
import, the same decoupling pattern every other feature in this bucket
follows.
An app that wants an editable collection in its own navigation declares it at
the mount, with its own access rule, access belongs there because a bundled
feature does not know that an app calls its support staff “Agent”. Each
collection gets its own <id>-list / <id>-item / <id>-set handlers, so the
dispatcher enforces the separation.
Example:
import { createTemplateResolverFeature, createTemplateResolverApi,} from "@cosmicdrift/kumiko-bundled-features/template-resolver";
await runDevApp({ features: [ createTemplateResolverFeature({ collections: [ { id: "reply-snippets", kind: "mail-html", access: { roles: ["Agent", "TenantAdmin"] }, nav: { label: "mail:nav.snippets", parent: "mail:nav:root" }, }, ], }), myFeature, ], extraContext: ({ db }) => ({ templateResolver: createTemplateResolverApi(db) }),});Replaces the former text-content feature; its blocks now live here as kind
text-block.
legal-pages
Section titled “legal-pages”Status: ✅ Stable
What: Opt-in wrapper around template-resolver text-blocks for DACH compliance, four
fixed public HTML routes (/legal/impressum, /legal/datenschutz,
/legal/imprint, /legal/privacy), Markdown → HTML rendering, and a
boot-time job that hard-fails in production when the required DE blocks
(imprint/de, privacy/de) aren’t seeded in SYSTEM_TENANT.
How it works: the routes call template-resolver:query:by-slug via an
internal app.fetch (never a code import), always scoped to
SYSTEM_TENANT_ID regardless of the resolved Host, one Impressum per app,
not per tenant.
Recipe: recipes-legal-pages, seeds
the required blocks and asserts every route end-to-end.
Example:
import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
await runDevApp({ features: [createTemplateResolverFeature(), createLegalPagesFeature(), myFeature], anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },});managed-pages
Section titled “managed-pages”Status: ✅ Stable
What: Tenant-editable, server-rendered public pages, one Markdown
page per (tenantId, slug, lang), a published gate (drafts → 404), and
per-tenant branding (title/description/site URL/accent color/logo/layout
preset, optional tier-gated custom CSS).
How it works: an anonymous GET {basePath}/:slug route resolves the
tenant from the request Host via an app-supplied resolveApexTenant,
isolates content with Vary: Host, and ships TenantAdmin/SystemAdmin admin
screens (page-list/page-edit) backed by convention CRUD. Also exposes an
anonymous by-tenant-published listing query, SQL-filtered on published,
never leaking drafts, for site-discovery consumers like seo.
Recipe: recipes-managed-pages
Example:
import { createManagedPagesFeature } from "@cosmicdrift/kumiko-bundled-features/managed-pages";
await runDevApp({ features: [ createConfigFeature(), createManagedPagesFeature({ resolveApexTenant: (host) => resolveTenantIdFromHost(host), }), myFeature, ],});Status: 🚧 Beta
What: Site-discovery + SEO/AEO/GEO surface for apex/content pages, GET /sitemap.xml and GET /llms.txt (merging an app-supplied callback with
legal-pages’ fixed routes and/or managed-pages’ published slugs), an
opt-in GET /robots.txt, and an OG/JSON-LD/canonical extension for
wrapInLayout (the legal-pages/managed-pages render path). Also exports
pure schema.org JSON-LD builders, organizationSchema, webPageSchema,
faqPageSchema.
How it works: a boot-time job hard-fails in production when none of the
three entry sources (the sitemapEntries() callback, includeLegalPages,
managedPages) can supply anything, the routes would otherwise serve a
permanently empty document. Tenant-scoped config keys (organization
name/logo, Twitter handle, llms.txt summary, default OG image) feed the
Organization JSON-LD helper and the llms.txt summary line.
seo vs the apex renderer: if your marketing page goes through
renderApexPage, its head handling (Open Graph, Twitter card, canonical,
hreflang, JSON-LD) is already complete, seo only adds the site-discovery
routes and the schema builders there. The wrapInLayout({ seo }) extension
is for the legal-pages/managed-pages render path, which had no OG/JSON-LD
before.
Recipe: recipes-apex-landing :
organizationSchema/webPageSchema feeding ApexHead.schemaJson, plus a
separate integration test mounting createSeoFeature alongside the landing
route to exercise /sitemap.xml and /llms.txt as real HTTP requests.
Practical setup: this section covers what fields exist. For what to actually configure, config values, JSON-LD wiring, verifying the output : see Make your apex/content pages SEO/AEO/GEO discoverable.
Example:
import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
await runDevApp({ features: [ createConfigFeature(), createLegalPagesFeature(), createSeoFeature({ sitemapEntries: (host) => [{ loc: `https://${host}/` }], includeLegalPages: true, }), myFeature, ],});