Choosing a Markdown Code Highlighting Strategy That Won't Regret Later
Fenced code blocks are standard, but language tokens lock you into a highlighter's vocabulary. Server-side (Shiki) gives zero-JS output but locks theme/languages at build; client-side (Prism) enables runtime switching at a runtime cost. Pick one, standardize tokens, verify before commit.
25 Jan 2026, 18:38 UTC

The problem: highlighted code that works today but breaks tomorrow
You add a fenced code block with ```typescript to your Markdown file, deploy, and the syntax highlighting looks great. Six months later you switch from Astro to Next.js, or you decide to support a new language like Gleam, and suddenly half your code blocks render as plain text. The root cause isn't Markdown — it's the implicit contract between your language tokens and the highlighter you chose at build time.
What the spec actually guarantees
CommonMark 0.30+ defines fenced code blocks as three or more backticks (or tildes), an optional info string after the opening fence, and content until a closing fence of equal or greater length. The spec says nothing about what the info string means. GitHub Flavored Markdown (GFM) standardizes it as a language token for highlighters, but there is no universal registry. ts, typescript, typescriptreact — each highlighter maps these differently, and a mismatch silently falls back to plain text in most renderers.
Server-side vs client-side: the real trade-off
Server-side highlighters (Shiki, Pygments, highlight.php) emit inline <span class="token keyword"> markup at build time. You ship zero runtime JavaScript for highlighting, avoid layout shift, and get consistent output across environments. The cost: HTML payload grows, and your theme plus language set are locked at build. Adding a language or switching from github-dark to dracula means a full rebuild.
Client-side libraries (Prism, Highlight.js) keep HTML lean — roughly 15–60 kB of JS/CSS — and allow on-the-fly theme switching or lazy-loaded languages. The downside: a hydration step, potential flash of unstyled code, and runtime dependency on the browser's JavaScript engine.
Worked example: Astro 4.x with Shiki (server-side)
In astro.config.mjs:
import { defineConfig } from 'astro/config';
import rehypeShiki from 'rehype-shiki';
export default defineConfig({
markdown: {
rehypePlugins: [
[rehypeShiki, { theme: 'github-dark' }]
]
}
});
Write a Markdown file:
```ts
function greet(name: string) {
return `Hello, ${name}!`;
}
```
The build emits fully highlighted HTML with token spans and no client-side JavaScript. Verify by running npm run build and inspecting the output HTML — you should see <span class="token function">greet</span> and similar tokens.
Where portability breaks
- Language aliases: Shiki expects TextMate grammar names (
typescript), Prism uses short aliases (ts). If you author```tsand switch highlighters, highlighting disappears. - Line numbers and highlights: Plugins like
rehype-shikitransformers add non-standard attributes (data-line,data-highlighted-lines). These are not in CommonMark or GFM and will not survive a renderer change. - Large blocks: Files over ~500 lines noticeably slow down server-side builds. Consider a client-side fallback or lazy highlighting for generated code.
- CSS conflicts: Highlighter-generated class names (
.token.keyword) can clash with CSS-in-JS solutions. Use Shadow DOM or scoped CSS to isolate token styles.
Security reminder
Raw HTML inside fenced blocks is escaped per spec. However, some markdown-it configurations with html: true can leak unescaped content. Always sanitize rendered HTML with DOMPurify (or equivalent) when accepting untrusted Markdown.
Actionable checklist
- Pick one highlighter and standardize on its language tokens across the codebase. Document the mapping in your contributor guide.
- If you need theme switching without rebuilds, choose client-side (Prism/Highlight.js) and accept the runtime cost.
- If you prioritize zero-JS delivery and can rebuild for theme/language changes, choose server-side (Shiki).
- Test a sample block with
npx shiki --list-langsorprismjs.com/#supported-languagesto confirm your tokens exist. - Run a local build and inspect the output HTML for expected token spans before committing.
The highlighting decision is reversible, but the token contract you write into hundreds of Markdown files is not. Choose the strategy that matches your deployment cadence and stick to its vocabulary.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.