zfb

Type to search...

to open search from anywhere

Syntax Highlighting

CreatedJun 1, 2026Takeshi Takatsudo

zfb ships syntect-backed server-side syntax highlighting. This page explains the built-in behaviour, how to use custom .tmTheme files, and two supplementary patterns for client-side or theme-customised highlighting.

zfb ships server-side syntax highlighting via syntect — a Rust library that runs at build time inside crates/zfb-content. When the pipeline encounters a fenced code block, SyntectPlugin looks up the language tag, highlights the source with the configured theme, and replaces the <pre><code> element with a <pre class="syntect-…"><code>…</code></pre> HTML fragment baked into the output. No JavaScript is shipped to the browser for highlighting.

Built-in behaviour

PropertyValue
Enginesyntect (Sublime Text–compatible grammars)
ExecutionBuild-time (hast visitor phase in crates/zfb-content)
OutputInline HTML with class attributes — zero runtime JS
Unknown languagesThemed fallback: wrapped in <pre class="syntect-…">, code preserved
mermaid blocksSkipped — routed to MermaidPlugin instead

Customising the theme (built-in themes)

The simplest way to change the colour scheme is to pick one of syntect’s bundled themes in zfb.config.ts:

// zfb.config.ts
export default {
  codeHighlight: {
    theme: "Solarized (light)",
  },
};

Built-in theme names: "base16-ocean.dark" (default), "base16-ocean.light", "InspiredGitHub", "Solarized (dark)", "Solarized (light)".

These are not Shiki theme names. Using a name like "dracula" without loading it via themesDir will produce an unknown theme error at build time.

Using custom .tmTheme files

Syntect is compatible with Sublime Text’s .tmTheme format. You can load any .tmTheme file (Dracula, One Dark, Catppuccin, …) by dropping it into a directory and pointing codeHighlight.themesDir at it:

// zfb.config.ts
export default {
  codeHighlight: {
    themesDir: "./themes",   // relative to the project root
    theme: "Dracula",        // the `name` declared inside the .tmTheme file
  },
};

Directory layout:

my-project/
├── themes/
│   └── dracula.tmTheme      ← drop your .tmTheme files here
├── pages/
├── content/
└── zfb.config.ts

The .tmTheme filename does not matter — the name you pass to theme must match the <string> value of the name key inside the plist. For Dracula, the declared name is "Dracula".

Downloading Dracula: the official Dracula .tmTheme is available at https://draculatheme.com/sublime or directly from the Dracula GitHub repository.

Error reporting: if themesDir points at a missing directory or any .tmTheme file is malformed, zfb surfaces a clear error at build start (before rendering any pages) that includes the file path and the parse error.

Supplementary pattern 1 — additional client-side highlighting

If you want interactive theme switching or per-user preferences, layer a client-side highlighter on top of the server-rendered output as a client island:

"use client";

import { useEffect, useRef } from "preact/hooks";
import Prism from "prismjs";
import "prismjs/components/prism-typescript";

export default function PrismRoot({ children }) {
  const ref = useRef(null);
  useEffect(() => { Prism.highlightAllUnder(ref.current); }, []);
  return <div ref={ref}>{children}</div>;
}

Wrap your article body in <PrismRoot> and the island will re-highlight the pre-rendered blocks in place. This is useful for themes that depend on media queries or user preference, but it adds JavaScript and causes a brief re-paint after hydration.

Supplementary pattern 2 — post-build script for custom grammars

syntect uses Sublime Text–compatible grammars. If you need a grammar that syntect does not bundle (an internal DSL, a niche language), you can run a post-build Node script to replace specific blocks after zfb build:

// post-build/highlight-custom.ts — runs after `zfb build`
import { glob } from "glob";
import { readFile, writeFile } from "node:fs/promises";
import { codeToHtml } from "shiki";

for (const file of await glob("dist/**/*.html")) {
  const html = await readFile(file, "utf8");
  const next = await highlightCustomBlocks(html, codeToHtml);
  if (next !== html) await writeFile(file, next);
}

This only makes sense for grammars absent from syntect’s bundled set. For all standard languages (Rust, TypeScript, Python, Go, etc.), the built-in pipeline handles them without an extra step.

See also

  • Extending the Markdown Pipeline — how SyntectPlugin fits into the hast-phase pipeline and how to swap or extend it.
  • Custom Directivesmermaid blocks use this path instead of syntect.
  • crates/zfb-content/src/plugins/syntect_plugin.rs — plugin source.

Revision History