esc

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

@krantho/static

Build a krantho site from its markdown to static files, with each page's structured data, the sitemap and llms.txt.

IntermediateApplies toNext.js

The static mode of krantho. A site reads its markdown files at build time and writes every page, its structured data and its SEO files once. It has no database, no API and no Studio. Deploy to GitHub Pages shows a whole site.

Install#

pnpm add @krantho/static @krantho/config @krantho/schema @krantho/ui-react @krantho/seo zod

pnpm create krantho with Static site (Next.js, GitHub Pages) sets it up for you.

createStaticSite#

lib/krantho.ts
import { createStaticSite } from '@krantho/static'
import { initServerLoader } from '@krantho/ui-react/server'
import config from '../krantho.config'
import { collections } from '../krantho.collections'

export const site = createStaticSite({ config, collections })
initServerLoader(site.loader, site.rendering)

It takes the configuration and collections the server takes, and returns:

MemberWhat it is
loaderThe content loader pages read through: the live version of each document, and each page's JSON-LD composed from the seo section of the configuration. Pass it to initServerLoader()
params(collection, locale?)One { slug } for each published document of the collection, for generateStaticParams
sitemapXml()sitemap.xml: every published, indexable page, dated by its file's last commit (see Requirements)
llmsTxt()llms.txt: the site's name and summary, then each published, indexable page under its collection's heading, with a link and its description
robotsTxt()robots.txt, allowing every crawler and naming the sitemap
basePathThe path of site.baseUrl (/docs-site), or '' at a host's root
renderingHow pages render their markdown: basePath, resolveUrl (see Links between files) and atNotation (content.atNotation). Pass it to initServerLoader()
configThe resolved configuration

Serve each file from a route written once at build:

app/sitemap.xml/route.ts
import { site } from '../../lib/krantho'

export const dynamic = 'force-static'

export async function GET(): Promise<Response> {
  return new Response(await site.sitemapXml(), {
    headers: { 'content-type': 'application/xml; charset=utf-8' },
  })
}

Requirements#

createStaticSite() throws a KranthoConfigError naming each problem when:

  • persistence.provider is not filesystem: the markdown files are the content;
  • cache.pages is not none: every page is rendered once, at build;
  • a collection with pages has no path, or one that does not end in /: each page is written as a folder, and the sitemap, llms.txt and canonical URLs must name the address it is served at. Write path: '/docs/:slug/'.

Pages are dated by their files' last git commit. Without the repository's full history, such as a shallow CI checkout, the build warns once and dates them by modification time: check out with fetch-depth: 0.

site.rendering.resolveUrl lets a folder of markdown read the same on GitHub and on the site. A URL written relative to a document's file is rewritten when the page renders:

Written in docs/setup.mdRendered (site at /docs-site)
[Apps](apps.md#limits)/docs-site/apps/#limits
[Home](README.md), with indexFile: 'README.md'/docs-site/
![Flow](images/flow.png)/docs-site/images/flow.png

The same holds for the src and href of raw <img> and <a> tags, such as <img src="images/flow.png" width="340">. A link to a markdown file goes to that document's page, in whichever collection reads the file. Any other file inside the collection's directory is served from the site's root, so copy it into public/ at the same path (public/images/flow.png). Absolute URLs, URLs that start with / or #, and files outside the collection's directory are left as written. createLinkResolver(collections) builds the same resolver for a site of your own.

basePathOf#

import { basePathOf } from '@krantho/static/base-path'

basePathOf('https://owner.github.io/docs-site') // '/docs-site'
basePathOf('https://docs.example.com') // ''

The path a site is served under, from its address. next.config.ts takes it as basePath; it is imported from @krantho/static/base-path, which loads nothing else, so the configuration does not load the server.

Next.js settings#

next.config.ts
import { resolveConfig } from '@krantho/config'
import { basePathOf } from '@krantho/static/base-path'
import type { NextConfig } from 'next'
import kranthoConfig from './krantho.config'

const nextConfig: NextConfig = {
  output: 'export',
  basePath: basePathOf(resolveConfig(kranthoConfig, process.env).site.baseUrl),
  trailingSlash: true,
  images: { unoptimized: true },
  serverExternalPackages: ['@krantho/static', '@krantho/server'],
}

export default nextConfig

serverExternalPackages keeps the server out of the bundle: it loads its optional packages (other databases, media stores) only when a configuration asks for them, and a bundler would look for them all.