Implementing Server-Side Syntax Highlighting for Markdown Pipelines
Learn how to implement server-side syntax highlighting for Markdown to eliminate FOUC and improve SEO by generating semantic HTML during your build process.
01 Feb 2026, 20:28 UTC

The Problem: Client-Side Flash and SEO Gaps
Many documentation sites rely on client-side libraries like Prism or highlight.js to colorize code blocks. This creates a "Flash of Unstyled Content" (FOUC), where raw text appears for a fraction of a second before the JavaScript executes. More critically, search engines and accessibility tools may struggle with content that is dynamically modified after the initial page load.
The solution is to move syntax highlighting to the build step (server-side), producing semantic HTML with pre-applied styles. This ensures that the final output is a static <pre><code> block that renders instantly and remains accessible without JavaScript.
Prerequisites
- Markdown Parser: A CommonMark-compliant parser (e.g.,
markdown-it,Goldmark, orPandoc) that supports fenced code blocks. - Highlighter Library: A tool capable of running in a build environment. Shiki is recommended for static sites as it uses TextMate grammars (the same used by VS Code) and requires zero client-side runtime.
- Build Pipeline: A Node.js, Go, or Python environment that processes Markdown into HTML before deployment.
Implementation Procedure
1. Parser Configuration
Configure your parser to recognize fenced code blocks (```language). Ensure the parser captures the "info string" (the language identifier) and passes it to your highlighting function rather than simply wrapping it in a generic tag.
2. Integrating the Highlighter
If using Shiki (v1.x+), the process is asynchronous. You must initialize the highlighter and then transform the raw code string into HTML tokens.
// Example Node.js implementation using Shiki
import { getHighlighter } from 'shiki';
async function highlightCode(code, lang) {
try {
const highlighter = await getHighlighter({
themes: ['github-dark', 'github-light']
});
// Convert code to HTML with a specific theme
return highlighter.codeToHtml(code, {
lang: lang || 'text',
theme: 'github-dark'
});
} catch (err) {
console.warn(`Highlighting failed for ${lang}: ${err.message}`);
// Fallback to escaped plain text
return `${escapeHtml(code)}`;
}
}
3. Generating Semantic HTML
The output must follow a strict structure to ensure CSS targets the correct elements. The highlighter should produce:
- A wrapping
<pre>tag for whitespace preservation. - A
<code>tag with a class identifying the language (e.g.,class="language-javascript"). - Individual
<span>elements for tokens (keywords, strings, comments) with specific color classes.
4. CSS Theme Application
To support both light and dark modes without JS, use CSS variables or media queries. If the highlighter generates inline styles, be aware that strict Content Security Policies (CSP) may block them. In such cases, configure the highlighter to emit classes instead of style attributes.
/* Example CSS for theme switching */
@media (prefers-color-scheme: dark) {
.code-block {
background-color: #0d1117;
color: #c9d1d9;
}
}
@media (prefers-color-scheme: light) {
.code-block {
background-color: #ffffff;
color: #24292f;
}
}
Verification and Diagnostics
To verify the implementation, run your build and inspect the generated HTML. Check for the following:
| Check | Expected Result | Risk if Failed |
|---|---|---|
| XSS Safety | No raw < or > inside <code> |
Security vulnerability/Broken layout |
| Language Class | class="language-python" matches the Markdown tag |
Incorrect styling or missing tokens |
| Fallback | Unknown languages render as plain text without crashing | Build pipeline failure (CI/CD stop) |
| Contrast | Tokens meet WCAG AA contrast ratios in both themes | Accessibility non-compliance |
Limitations and Constraints
- Memory Overhead: Highlighting extremely large files (>500 KB) can lead to Out-of-Memory (OOM) errors in Node.js. Truncate or stream these blocks before processing.
- Case Sensitivity: While Markdown language tags are often case-insensitive, CSS classes are not. Always normalize language identifiers to lowercase (e.g.,
JS$\rightarrow$javascript). - Encoding Order: HTML escaping must occur before the highlighter processes the text. If you escape after, the highlighter's own
<span>tags will be escaped into visible text.
Recovery Options
If the highlighter throws an exception during the build process, do not allow the entire site build to fail. Implement a try-catch block that logs a warning containing the filename and line number, then returns the code block as a simple, escaped <code> element. This ensures the documentation remains deployable even if a specific grammar is corrupted.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.