Writing a theme

On this page

A local theme

npx kilim theme new my-theme

writes a working theme to themes/my-theme/: layouts, partials, a component, blocks, settings and CSS files. Use it by folder name:

export default defineConfig({ theme: "my-theme" });

Kilim compiles a local theme's TSX on every build, and kilim dev rebuilds when you save it. Its public/ folder is copied to the site.

defineTheme

index.tsx exports the theme:

import { defineTheme, z } from "@kilim-dev/core";
import critical from "./styles/critical.css";
import main from "./styles/main.css";

export default defineTheme({
  name: "my-theme",
  // The Kilim versions the theme is made for; other versions fail with a clear error.
  kilim: "^0.1",
  settings: z.object({ menu: z.array(z.object({ label: z.string(), url: z.string() })) }),
  layouts: { page: Page, post: Post, list: List },
  partials: { header: Header, footer: Footer },
  components: { callout },
  blocks: { hero },
  styles: { critical, main },
});
  • Layouts get page (title, content, data, headings, meta), site (title, settings, collections), entries and pagination on list pages, and partial(name).
  • styles.critical is inlined in every page; styles.main becomes a hashed stylesheet loaded at the end of the body. .css files are imported as text.
  • settings is a schema for settings/<name>.yaml, named after the theme's name. Give every setting a default (e.g. .default([]), or .prefault({}) for an object), so a site builds without the file; the type flows into layouts as site.settings.
  • collections changes pages and posts, which every theme has, and adds the theme's own, e.g. { posts: { layout: "post", list: { pageSize: 10 } }, projects: {...} }. A function gets the config, e.g. for list titles in the site's languages with localizedTitle. A site's config goes over these option by option. Lay out pages and posts so a plain post (title, date) and a plain page build without any field only your theme knows.
  • ogImage renders the Open Graph image from TSX with inline styles.
  • plugins lets a theme bring plugins, such as one that checks references between entries.

<HeadTags page={page} site={site} /> from @kilim-dev/core writes the title, description, canonical link, Open Graph and JSON-LD tags; put it in every layout's <head>.

Extending a theme

extends builds on another theme, a package or a local folder name, and replaces only what you give:

import { defineTheme } from "@kilim-dev/core";
import base from "@kilim-dev/theme-default";
import { Header } from "./header.js";
import critical from "./styles/critical.css";

export default defineTheme({
  name: "my-brand",
  extends: base,
  partials: { header: Header },
  styles: { critical },
});

Layouts, partials, components and blocks replace the base's by name. Settings, image sizes and the Open Graph template are the base's unless you set them. Styles are added after the base's; styles: { inherit: false, ... } drops the base CSS entirely. This site's theme is exactly this: the default theme with the Kilim header and colors, in sites/docs/themes/kilim/ of the repository.

Sharing a theme

Publish the theme folder as a GitHub repository or an npm package. Others add it with:

npx kilim theme add github:user/repo
npx kilim theme add kilim-theme-paper@1.2.0
npx kilim theme add ./paper-1.2.0.tgz

theme add copies the theme into themes/<name>/, where it can be read and changed, and tells you how to use or extend it. It checks npm packages against their integrity hash, reads .npmrc for private registries and GITHUB_TOKEN or GH_TOKEN for private repositories. A theme whose kilim range does not match the installed Kilim gets a warning.

A packaged theme ships static files with publicDir: new URL("../public/", import.meta.url). A local theme always uses its own public/ folder and ignores publicDir, so a packaged theme copied into themes/ works as it is.