Skip to content

Content & SEO

Four features for shipping public-facing content — from raw editable strings to full tenant-branded pages — plus the site-discovery layer search engines and LLM answer engines need to find and cite them.

Status: ✅ Stable

What: Generic Markdown key-value store — one read_text_blocks row per (tenantId, slug, lang). 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.textContent (createTextContentApi), never a code import — the same decoupling pattern every other feature in this bucket follows.

Example:

import {
createTextContentFeature,
createTextContentApi,
} from "@cosmicdrift/kumiko-bundled-features/text-content";
await runDevApp({
features: [createTextContentFeature(), myFeature],
extraContext: ({ db }) => ({ textContent: createTextContentApi(db) }),
});

Status: ✅ Stable

What: Opt-in wrapper around text-content 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 text-content: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: [createTextContentFeature(), 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-landingorganizationSchema/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,
],
});