Krantho is in active development and not yet published to npm. Read what that means today.
esc

Search the documentation and product pages. Type “theme” to switch themes.

Features

Structured SEO

Every page carries a connected JSON-LD graph without anyone writing JSON-LD by hand.

Problem

Search engines and AI assistants read structured data, not only text. Handwritten JSON-LD drifts from the content, differs from page to page, and is rarely connected into one graph. Meta descriptions and social tags are left empty because nobody owns them.

Approach

Instead of writing JSON-LD, you choose Schema.org types, called archetypes, at four scopes, from the broadest to the narrowest: the site, a collection, a URL pattern, and a single document. The narrowest scope wins. The seo section of krantho.config.ts seeds the site identity and the types of each collection on startup; editors refine them in the admin.

The archetype catalog covers Google's rich result types, business subtypes and core Schema.org types. Properties a document does not set are filled from its content and metadata where the type allows it. Each page gets one @graph with the site and the document linked by @id.

In a page template, resolveSeoContent() returns the title, description, Open Graph and Twitter data, and <SeoJsonLd> renders the graph. Both read the published version in-process. The API also serves the sitemap and robots.txt.

AI enrichment fills missing meta descriptions and Schema.org properties, and never overwrites what an editor wrote. It runs through @krantho/ai, a client with no dependencies that speaks to Anthropic, OpenAI, Gemini, OpenRouter or a local Ollama, as the site configures. Anthropic and OpenRouter default to Claude Haiku 4.5; you can choose another model.

For AI assistants, the SEO pipeline also writes an llms.txt file, which your app serves from a route of its own.

Proof

A page template gets its meta tags and its JSON-LD graph from two calls:

import type { Metadata } from 'next'
import { resolveSeoContent, SeoJsonLd } from '@krantho/ui-react/server'

export async function generateMetadata(): Promise<Metadata> {
  // Also carries canonical, Open Graph (seo.og) and Twitter (seo.twitter) data
  const seo = await resolveSeoContent('pages', 'about')
  return { title: seo.title, description: seo.description }
}

export default function AboutPage() {
  return <SeoJsonLd collection="pages" slug="about" />
}

In the admin, each document has a JSON-LD graph browser, a search result and social preview, and a rich result checklist.

krantho.config.ts
import { defineKranthoConfig } from '@krantho/config'

export default defineKranthoConfig({
  site: { domain: 'krantho.com', name: 'Krantho', language: 'en', baseUrl: 'https://krantho.com' },
  // Seeded into the SEO configuration on startup. Rows already there are
  // kept, so edits made in /admin (Structure › Scopes & archetypes) survive.
  seo: {
    configDir: './krantho.seo',
  },
})

// krantho.seo/ holds one YAML file per entity:
//   site/organization.yaml, site/software-application.yaml, site/website.yaml
//   collections/docs/article.yaml, collections/docs/breadcrumb-list.yaml
//   collections/features/breadcrumb-list.yaml, collections/stacks/…
//
// krantho.seo/collections/docs/article.yaml — each docs page is an Article:
//   "@type": Article
//   role: primary
//   properties: {}
//
// Each page then renders its composed @graph as JSON-LD:
//   Next.js:            <SeoJsonLd collection="features" slug="seo" />
//   SvelteKit, Express: await loader.jsonLd({ collection: 'docs', slug: 'installation' })

Limits

  • Enrichment needs an LLM provider and its credential, and it costs whatever that provider charges. SEO works without it.
  • llms.txt is generated by the command-line SEO pipeline, not from the admin.
  • Types with a dedicated builder, such as Article, read only the fields that builder knows; authors fill in what needs human judgment.
  • Two open issues affect SEO today. Meta descriptions generated in the admin are saved as a new draft, so a published page needs publishing again before they go live. And a document's JSON-LD names its collection path, such as /pages/about, unless the document sets a route field, while the sitemap uses the page's own URL.