Skip to content

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.

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.

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 },
});

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,
],
});