Rendering code blocks: options and trade-offs
On this page
Technical posts live or die by their code samples. A good code block is highlighted correctly, readable in light and dark mode, and easy to copy. There are three common ways to get there on a static site.
The three options
| Option | Highlights | JS sent to readers | Extras |
|---|---|---|---|
| Plain Shiki (built into Astro) | At build time | None | None out of the box |
| Expressive Code | At build time | A tiny copy-button script | Titles, terminal frames, marked lines, diffs, copy |
| Client-side (Prism, highlight.js) | In the browser | The highlighter + grammars | Depends on plugins |
- Plain Shiki uses the same grammars as VS Code, so highlighting is accurate. It adds nothing to the page, but you get bare blocks: no file names, no copy button.
- Expressive Code wraps Shiki and adds the features deep dives need. It still renders at build time.
- Client-side highlighters ship JavaScript and grammars to every reader, and code flashes unstyled until they run. They made sense before build-time highlighting was easy; on a static site they rarely do.
Side by side
The same snippet, rendered both ways. First plain Shiki:
export function total(items: { price: number }[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
Then Expressive Code, with a title and a marked line:
export function total(items: { price: number }[]): number { return items.reduce((sum, item) => sum + item.price, 0);}The colours match because both use Shiki underneath. Expressive Code adds the frame, the title, the marked line, and the copy button.
This blog uses Expressive Code. The rest of this post shows what it can do — each feature is a few characters in the Markdown.
A file with a title
Add title="..." after the language:
export function greet(name: string): string { return `Hello, ${name}!`;}A terminal
Shell languages get a terminal frame:
pnpm installpnpm devMarked lines
Mark lines with {2-3}, or show insertions and deletions with ins={...} and del={...}:
function total(items) { // These two lines are highlighted. return items.reduce((sum, item) => sum + item.price, 0);}function total(items) { return items.map((item) => item.price).reduce((a, b) => a + b); return items.reduce((sum, item) => sum + item.price, 0);}A diff
Use the diff language with a lang hint to keep syntax colours:
const config = { syntaxHighlight: "shiki", integrations: [expressiveCode()],};Marked text
Mark a word or phrase inside a line with "...":
import expressiveCode from "astro-expressive-code";Every block also has a copy button in its top-right corner — hover over one to see it. Light and dark themes follow the site’s theme switch.
The trade-off we accepted
Expressive Code must run before MDX and claims every fenced block. That is why Mermaid diagrams need a small plugin to step aside first.