Skip to contentSkip to Content
Feedback
ThemingTheme Packages

Theme Packages

[!WARNING] Security A theme package is executed as trusted code during every build. The package files are overlaid onto the template that Folio then builds with pnpm install --frozen-lockfile (which executes dependency lifecycle scripts) and next build (which runs next.config.mjs in Node), so pointing theme.package at a directory is equivalent to running that code on your machine. Only point theme.package at frontend code you have reviewed and trust. Building an untrusted repository or branch in CI inherits the same remote-code-execution surface — treat a docs build like any other untrusted-code execution. Never put secrets, API tokens, or credentials in theme code or docs.yaml: static docs are published.

Theme packages are the middle ownership level. They let a project ship a theme overlay, local or fetched from git, that is copied over the bundled Folio template before generated content and metadata are injected.

Use a theme package when YAML personalization is too limited, but a full custom template would duplicate too much of Folio’s default docs runtime.

Configure a Package

theme: package: "docs/theme/acme" preset: "acme"

theme.package is resolved relative to the project directory. The directory is trusted frontend code: it can override template files, import dependencies already available to the template, and replace Folio’s default theme modules.

Install a Package Someone Else Published

theme.package also takes a mapping, which is how a theme you did not write reaches your project without copying a directory into it:

theme: package: git: "https://github.com/acme/folio-theme" rev: "v1.2.0" digest: "sha256:9f2c…"
KeyRequiredPurpose
gityesRepository to fetch: https://, ssh://, git@host:owner/repo or file://. There is no plain http.
revyesTag, branch or commit to read it at.
digestyessha256: and 64 hex characters: the digest of the package tree.
pathnoSubdirectory of the repository that holds the package; the root by default.

Folio fetches the revision into the theme cache, one directory per digest under <cache>/folio/themes/, hashes the tree it got, and refuses to use it unless the hash is the one you pinned. FOLIO_THEME_CACHE_DIR points that cache somewhere else, which is how a CI job pre-seeds it and how an air-gapped machine fills it by hand. The build then overlays it exactly as it overlays a local package. The second build reads the cache and makes no network call.

The digest is the pin, not the revision

A tag can be moved and a branch always moves. The digest cannot: it is the SHA-256 of every file in the package, so the same four lines of docs.yaml produce the same theme on every machine and in CI. When the hash does not match, the build fails and names both:

theme.package https://github.com/acme/folio-theme at v1.2.0 does not match its digest. expected sha256:9f2c… found sha256:41ab… Update theme.package.digest if the change is one you reviewed.

Take a new digest by reviewing the change and reading the value out of that message. Folio never updates the pin for you. Pinning a package for the first time works the same way: put any 64 hex characters in digest, run the build, and copy the found value once you have read what you are installing.

What a fetched package may not own

A theme package is executed as trusted code, and a fetched one was written by someone else. Two rules follow. Every package, local or fetched, is refused if it ships package.json, pnpm-lock.yaml or pnpm-workspace.yaml, because the frontend build installs against Folio’s own pinned lockfile. A fetched package is refused if it ships next.config.mjs: owning the Next config is the shortest path from a one-line change in docs.yaml to arbitrary code in your build, and it stays a decision a project makes about itself.

Everything else the digest covers: you are trusting a tree you pinned, not a repository that can change under you. Fetching needs git on the machine.

Ownership Model

OwnerResponsibilities
FolioCopy the bundled template, apply the theme package overlay, generate content, write metadata, inject fallback theme config, run the frontend build, and export static files.
Theme packageOwn selected frontend files such as layouts, global CSS, the theme model behind the picker, project header actions, or theme/project-theme.ts.

Folio still writes generated docs content and reserved build files into .build/. Do not edit .build/ directly; change the package source instead.

Package Anatomy

A package can be small and only override the files it owns:

docs/theme/acme/ app/ globals.css docs/ layout.tsx components/ theme-configurator.tsx project-header-actions.tsx theme/ project-theme.ts

Common override points:

FilePurpose
app/layout.tsxOwn font loading, metadata shell, providers, and global layout behavior.
app/docs/layout.tsxOwn the docs navbar, sidebar placement, search slot, and ThemeGallery mount.
components/theme-configurator.tsxReplace the bundled theme model, its defaults and its bootstrap with a project-specific implementation.
components/project-header-actions.tsxReplace the default generated header action component.
theme/project-theme.tsOwn all presets, controls, variants, defaults, and resolved tokens in TypeScript.
app/globals.cssOverride or replace template-level CSS when a project needs exact visual parity.
app/styles/<family>.cssRestyle one component family (shell, callout, disclosure, code, shapes, surfaces or components) without copying globals.css, which imports these files last.

A package that replaces components/theme-provider.tsx has to keep the const darkModeEnabled: boolean = true // __FOLIO_DARK_MODE__ line for theme.dark_mode: false to take effect. Without it the build warns and dark mode stays available.

YAML and TypeScript Together

If the package supplies theme/project-theme.ts, Folio does not overwrite it with a YAML-generated module. The package owns the preset implementation.

If the package omits theme/project-theme.ts, Folio still emits the safe docs.yaml-driven project preset described in Personalization. This lets a project start with YAML and graduate individual surfaces to TypeScript only when needed.

Register a Custom Preset

A theme package can register a custom preset without forking presets.ts. From theme/project-theme.ts, import registerPreset and call it before exporting the projectThemePreset.

import type { ThemePreset } from "./preset-types" import { registerPreset } from "./preset-registry" const acmePreset: ThemePreset = { id: "acme", name: "Acme", description: "Custom preset for Acme's design system", scene: "A developer browses API documentation with Acme brand colors.", preview: { light: "oklch(0.50 0.10 210)", dark: "oklch(0.70 0.08 210)" }, defaultOptions: {}, controls: [], resolve: (options) => ({ preview: { light: "oklch(0.50 0.10 210)", dark: "oklch(0.70 0.08 210)" }, radius: "0.5rem", style: { /* ThemeStyle fields */ }, light: { /* ThemeVars for light mode */ }, dark: { /* ThemeVars for dark mode */ }, }), } registerPreset(acmePreset, "project") export const projectThemePreset = acmePreset export const projectThemeDefaultConfig = { /* Default config object */ }

The registerPreset function is the supported extension point. The optional second argument ("project") adds the preset to that display group in the theme picker. If a preset with the same id already exists, it will be replaced with a console warning.

theme.preset may name a preset the package declares. Folio reads the id: "…" values in the .ts and .tsx files under the package’s theme/ directory and accepts those ids next to the bundled ones, so preset: "acme" above builds. Any other id stops the build with the list of valid ids and the nearest one. A package that ships its own components/theme-configurator.tsx owns preset selection, and Folio does not check theme.preset then.

The theme picker and the root layout import from components/theme-configurator.tsx, so a package that replaces it keeps these exports: ThemeStyleBootstrap, DEFAULT_CONFIG, the ThemeConfig type, configForPreset, configVars, getPresetDefaults, readThemeConfig, saveThemeConfig, previewThemeConfig, and the option lists fontOptions, colorOptions, surfaceColorOptions, shellPaddingOptions, rhythmOptions, borderOptions, codeTreatmentOptions and radiusOptions. ThemeConfigurator renders nothing and stays exported for layouts that still mount it.

The root layout mounts ThemeStyleBootstrap once for every page, so a package’s page or layout needs no copy of its own; a copy it still carries re-applies the stored theme and changes nothing else.

See Theme picker for the full ThemePreset contract, including controls, resolve, and all theme fields.

What Packages Should Not Do

  • Do not assume files written under .build/ are stable source files.
  • Do not put secrets in theme code or docs.yaml; static docs are public.
  • Do not use a package when the project needs a different app structure, dependency graph, route model, or product shell. Use custom templates for that level.

Validation Checklist

Folio validates theme packages at build time before any overlay. If validation fails, the build halts with one error listing all violations.

Reserved Paths

A theme package must NOT contain any of these paths. Folio generates them at build time, and including them will cause a validation error:

  • content/ — Generated docs pages and metadata live here
  • lib/folio-template.ts — Folio internal contract file
  • lib/folio-mdx-contract.ts — Folio internal contract file
  • theme/theme-contract.generated.ts — Folio internal contract file
  • .next/ — Next.js build cache
  • node_modules/ — Dependency install directory
  • package.json — The frontend installs against Folio’s own manifest
  • pnpm-lock.yaml — The install is --frozen-lockfile against Folio’s lockfile
  • pnpm-workspace.yaml — Same reason
  • next.config.mjs — Fetched packages only; a local package may own it

Required Exports

If a theme package includes theme/project-theme.ts, that file MUST export both:

  • projectThemePreset — The preset definition (colors, spacing, variants)
  • projectThemeDefaultConfig — The default configuration object

Folio checks for export const <name> or export { <name> } forms. Missing exports fail validation.

Example Validation Error

Theme package validation failed: - Theme package must not contain reserved path 'content/'. Folio generates this at build time. - theme/project-theme.ts must export 'projectThemePreset'. Expected: export const projectThemePreset = ...

Why This Fails Early

Validation runs before any file overlay. You get one clear error with actionable messages instead of a cryptic Next.js build failure or runtime import breakage.