Markdown to HTML: Handling Code Blocks Correctly
Code Blocks Are the Trickiest Markdown Element
Paragraphs, headings, and lists convert predictably from Markdown to HTML. Code blocks do not. Between fenced blocks, indented blocks, language hints, syntax highlighting, and HTML escaping, there are plenty of ways the conversion can go wrong.
Understanding how Markdown parsers handle code blocks helps you write correct Markdown and debug unexpected HTML output.
Fenced Code Blocks
Basic Syntax
Fenced code blocks use triple backticks or tildes. This is the most common and recommended syntax.
```javascript
const greeting = "Hello, world!";
console.log(greeting);
Produces:
```html
<pre><code class="language-javascript">const greeting = "Hello, world!";
console.log(greeting);
</code></pre>
Language Hints
The text after the opening fence is the info string. Most parsers pass it as a CSS class with the language- prefix:
| Info String | HTML Class | Purpose |
|---|---|---|
javascript | language-javascript | Syntax highlighting hint |
python | language-python | Syntax highlighting hint |
json | language-json | Syntax highlighting hint |
| `` | (none) | No language specified |
The info string does not enable highlighting by itself. You need a syntax highlighting library like Prism.js, Highlight.js, or Shiki to read the class and apply colors.
Indented Code Blocks
Code indented by four spaces (or one tab) is treated as a code block in standard Markdown:
const x = 42;
console.log(x);
Produces:
<pre><code>const x = 42;
console.log(x);
</code></pre>
Problems with Indented Blocks
- No language hint: There is no way to specify the language
- Easy to trigger accidentally: Four spaces at the start of a paragraph converts it to code
- Hard to read in source: The indentation is not visually distinctive
- Removed in some flavors: GitHub Flavored Markdown deprecates indented code blocks in lists
Prefer fenced code blocks in all new Markdown content.
Inline Code
Single backticks wrap inline code:
Use the `console.log()` function to debug.
Produces:
<p>Use the <code>console.log()</code> function to debug.</p>
Escaping Inside Inline Code
Backticks inside inline code require double backtick delimiters:
`` `nested` `` — renders as: `nested`
HTML Escaping in Code Blocks
Code blocks must escape HTML special characters. Without escaping, the browser interprets tags inside the code instead of displaying them.
```html
<div class="container">
<p>Hello</p>
</div>
Produces:
```html
<pre><code class="language-html"><div class="container">
<p>Hello</p>
</div>
</code></pre>
The Markdown parser handles this escaping automatically. But if you are building a custom converter, you must escape <, >, and & yourself:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Always escape & first — otherwise < becomes &lt;.
Syntax Highlighting Options
Client-Side (Browser)
<!-- Include Highlight.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
Highlight.js reads the language-xxx class from Markdown output and applies <span> elements with color classes.
Build-Time (SSG/SSR)
For static sites, highlight at build time to avoid client-side JavaScript:
const { marked } = require('marked');
const { markedHighlight } = require('marked-highlight');
const hljs = require('highlight.js');
marked.use(markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
}));
const html = marked.parse(markdown);
// Output already contains highlighted spans
Shiki (VS Code Themes)
Shiki uses TextMate grammars for accurate highlighting with VS Code themes:
const shiki = require('shiki');
const highlighter = await shiki.getHighlighter({ theme: 'github-dark' });
const html = highlighter.codeToHtml(code, { lang: 'javascript' });
// Produces inline-styled spans — no CSS stylesheet needed
Common Problems
Code Blocks Inside Lists
Fenced code blocks inside list items require the fence to be indented to the list item level:
1. Install the package:
```bash
npm install my-package
- Import it: ...
The fence must align with the list content indentation (3 spaces for numbered lists in CommonMark).
### Empty Code Blocks
An empty fenced block renders as an empty `<pre><code></code></pre>`. Some CSS frameworks give this zero height, making it invisible. Add `min-height` to `<pre>` as a safeguard.
## Key Takeaways
- Use fenced code blocks (triple backticks) instead of indented blocks
- The info string after the fence becomes a `language-xxx` CSS class for syntax highlighting
- HTML special characters in code blocks are escaped automatically by parsers
- Escape `&` before `<` and `>` when doing manual escaping
- Choose between client-side (Highlight.js), build-time (marked-highlight), or inline-styled (Shiki) highlighting
- Indent fenced blocks to match list item nesting when embedding code in lists
## Try It Yourself
Convert Markdown with code blocks to highlighted HTML instantly using our free [Markdown to HTML Converter](/tools/markdown-to-html). Paste any Markdown to see properly escaped and formatted output — all processing happens locally in your browser.
[Try the Markdown to HTML Converter →](/tools/markdown-to-html)