Syntax Highlighting
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
| Property | Value |
|---|---|
| Engine | syntect (Sublime Text–compatible grammars) |
| Execution | Build-time (hast visitor phase in crates/zfb-content) |
| Output | Inline HTML with class attributes — zero runtime JS |
| Unknown languages | Themed fallback: wrapped in <pre class="syntect-…">, code preserved |
mermaid blocks | Skipped — 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:
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
SyntectPluginfits into the hast-phase pipeline and how to swap or extend it. - Custom Directives —
mermaidblocks use this path instead of syntect. crates/— plugin source.zfb- content/ src/ plugins/ syntect_ plugin. rs