Make your apex/content pages SEO/AEO/GEO discoverable
The seo feature reference documents what
fields exist. This guide is the missing how: mount it, fill in the five
config values, wire real JSON-LD into your page head, and check the output
the way a crawler or an answer engine would — before you assume it’s done.
Prerequisites
Section titled “Prerequisites”configfeature mounted (a hard requirement ofseo).- A marketing/apex page rendered via
renderApexPage(ApexHead) or a CMS page viawrapInLayout(legal-pages/managed-pages). If neither exists yet,seostill works —sitemapEntries()accepts any list of URLs your app wants indexed.
1. Mount the feature
Section titled “1. Mount the feature”import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
createSeoFeature({ sitemapEntries: (host) => [{ loc: `https://${host}/` }], includeLegalPages: true, // only if createLegalPagesFeature() is also mounted managedPages: { resolveApexTenant }, // only if createManagedPagesFeature() is also mounted robotsPolicy: (host) => ({ allow: true, sitemapUrl: `https://${host}/sitemap.xml` }),});sitemapEntries is the one thing only your app knows: the list of public
pages worth indexing. includeLegalPages/managedPages are opt-in merges —
set them only if you’ve actually mounted those features, seo can’t detect
that on its own. robotsPolicy is optional; skip it if a static
public/robots.txt already covers your case (the common case).
2. Set the config values
Section titled “2. Set the config values”Five tenant-scoped config keys feed the Organization JSON-LD helper and
the llms.txt summary line: seo-organization-name,
seo-organization-logo-url, seo-twitter-site, seo-llms-summary,
seo-default-og-image. Nothing renders without them — they default to
empty strings, not placeholders.
For values that are the same for every tenant (typical for a single-brand
app), set them once as app-wide config overrides at boot — the same pattern
file-foundation:config:provider already uses:
import { createConfigResolver } from "@cosmicdrift/kumiko-bundled-features/config";import { SEO_CONFIG_QN } from "@cosmicdrift/kumiko-bundled-features/seo";
const configResolver = createConfigResolver({ appOverrides: new Map([ [SEO_CONFIG_QN.organizationName, "Your Product"], [SEO_CONFIG_QN.twitterSite, "@yourhandle"], [ SEO_CONFIG_QN.llmsSummary, "One or two sentences: what the product does and who it's for. " + "This is the line an LLM answer engine quotes back — write it " + "like a pitch, not a tagline.", ], ]),});For a multi-tenant app where each tenant has its own brand (an agency
platform, say), skip the override and let tenants set these through
whatever config-write path your app already exposes — they’re regular
createTenantConfig keys, admin-writable by default.
What makes a good seo-llms-summary: answer engines quote this text
directly. “Cashcolt” is not a summary; “a free loan and amortization
calculator, hosted in Germany, no tracking” is. Write it the way you’d
answer “what does this do?” to someone who’s never heard of the product.
3. Feed it real JSON-LD
Section titled “3. Feed it real JSON-LD”What JSON-LD actually is, in one paragraph: it’s a <script type="application/ld+json"> block in your page’s <head> containing a
plain JSON object that describes the page using schema.org’s
vocabulary — a shared dictionary of types (Organization, WebPage,
FAQPage, Product, …) and their fields, that both search engines and
LLM crawlers already know how to parse. @context says “I’m using
schema.org’s vocabulary”; @type says which entry in that vocabulary this
object is; everything else is that type’s fields. @graph is just “here’s
an array of several such objects on one page” instead of one.
seo exports three pure builder functions for the types you need most
often — plug their output into ApexHead.schemaJson (apex pages) or
wrapInLayout({ seo: { schemaJson } }) (CMS pages):
import { organizationSchema, webPageSchema } from "@cosmicdrift/kumiko-bundled-features/seo";
const head: ApexHead = { // ...title, description, canonicalUrl, etc. schemaJson: { "@context": "https://schema.org", "@graph": [ organizationSchema({ name: "Your Product", url: "https://example.com/" }), webPageSchema({ name: pageTitle, url: canonicalUrl, description: pageDescription, }), ], },};Got an FAQ section on the page? faqPageSchema(items) is the most directly
LLM-legible shape there is — question/answer pairs an answer engine can
quote verbatim. Don’t invent FAQ content to get the badge; an empty or
padded FAQ block reads as spam to both Google and an LLM crawler.
Need a type the three builders don’t cover — Product, Article,
BreadcrumbList, LocalBusiness, SoftwareApplication, whatever fits your
page — there’s no fourth builder for it, and there doesn’t need to be:
schemaJson accepts any plain object, so write the node by hand against
schema.org’s own type list and drop it
into the same @graph array:
schemaJson: { "@context": "https://schema.org", "@graph": [ organizationSchema({ name: "Your Product", url: "https://example.com/" }), { "@type": "Product", name: "Pro Plan", description: "...", offers: { "@type": "Offer", price: "19.00", priceCurrency: "EUR" }, }, ],},Two ways to check the shape is right before it ships: schema.org’s own type pages list every field per type (required vs. recommended), and the Rich Results Test in step 4 below tells you immediately if Google can’t parse what you wrote.
4. Verify
Section titled “4. Verify”Don’t trust that it compiles — check what actually gets served:
curl -s https://your-app.example/sitemap.xmlcurl -s https://your-app.example/llms.txtcurl -s https://your-app.example/robots.txt # only if robotsPolicy was setcurl -s https://your-app.example/ | grep -A2 'application/ld+json'Confirm: the sitemap lists the URLs you expect (and nothing from a draft/
unpublished page), llms.txt has your actual summary — not an empty
Blockquote — and the JSON-LD <script> block parses as valid JSON with a
non-empty name.
Then check it the way the outside world does:
- Google Rich Results Test — paste your page URL, confirm the Organization/WebPage/FAQPage nodes are recognized without errors.
- Google Search Console — submit
sitemap.xmlunder Sitemaps once the domain is verified. Same idea for Bing Webmaster Tools.
Reality check
Section titled “Reality check”Sitemap, robots.txt, and JSON-LD make your pages findable and
parseable — they are necessary, not sufficient. They don’t move a ranking
on their own; that still comes down to content quality, real backlinks, and
page performance. What seo buys you is: nothing standing in a crawler’s or
an answer engine’s way. The rest is still writing.
See also
Section titled “See also”seofeature reference — full options, config keys, dependencies.- Content & SEO bucket overview —
how
seorelates totext-content/legal-pages/managed-pages. recipes-apex-landing— a full working example:organizationSchema/webPageSchemainApexHead, pluscreateSeoFeaturemounted alongside the landing route.