Building Technical Blogs with Markdown
Markdown is the standard for technical writing. It is version-controllable, portable, and converts easily to HTML. Building a blog that renders markdown well requires handling code blocks, syntax highlighting, custom elements, and consistent typography.
Problem
Basic markdown rendering produces unstyled output:
- Code blocks lack syntax highlighting
- No support for custom elements like callouts
- Typography is inconsistent
- No anchor links for headings
- Tables may overflow on mobile
A technical blog needs more than marked(content).
Why It Happens
Markdown parsers vary in feature support and output. The core spec handles basics like headings, lists, and code blocks. Enhanced features like syntax highlighting, custom containers, and smart typography require additional plugins or post-processing.
Solution
Use a capable markdown parser with plugins for syntax highlighting, custom containers, and typography enhancements.
Implementation
Choosing a Parser
For React applications, popular options include:
- react-markdown: Pure React, renders to components
- markdown-it: Plugin-rich, renders to HTML
- unified/remark: Highly extensible ecosystem
Example with react-markdown and syntax highlighting:
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
interface MarkdownContentProps {
content: string;
}
export function MarkdownContent({ content }: MarkdownContentProps) {
return (
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
if (!inline && match) {
return (
<SyntaxHighlighter
style={oneDark}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{content}
</ReactMarkdown>
);
}
Custom Callouts
Transform blockquotes with specific prefixes:
blockquote({ children }) {
const text = children?.toString() || '';
if (text.startsWith('TIP:')) {
return (
<div className="callout callout-tip">
<span className="callout-icon">💡</span>
{text.replace('TIP:', '')}
</div>
);
}
if (text.startsWith('WARNING:')) {
return (
<div className="callout callout-warning">
<span className="callout-icon">⚠️</span>
{text.replace('WARNING:', '')}
</div>
);
}
return <blockquote>{children}</blockquote>;
}
Heading Anchors
Add ID attributes and links to headings:
h2({ children }) {
const id = children
?.toString()
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-');
return (
<h2 id={id}>
<a href={`#${id}`} className="anchor-link">
{children}
</a>
</h2>
);
}
Example
Complete prose styling:
.prose {
font-size: 1.125rem;
line-height: 1.75;
color: var(--text-body);
}
.prose h2 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: var(--text-heading);
}
.prose h3 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
}
.prose p {
margin-bottom: 1.25rem;
}
.prose code {
background: var(--bg-elevated);
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.875em;
}
.prose pre {
margin: 1.5rem 0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
}
.prose pre code {
background: none;
padding: 0;
}
/* Callouts */
.callout {
padding: 1rem;
border-radius: 8px;
margin: 1.5rem 0;
}
.callout-tip {
background: rgba(16, 185, 129, 0.1);
border-left: 4px solid rgb(16, 185, 129);
}
.callout-warning {
background: rgba(245, 158, 11, 0.1);
border-left: 4px solid rgb(245, 158, 11);
}
TIP: Use CSS custom properties for colors so your prose integrates with light/dark theme switching.
Common Mistakes
Not escaping user content
If rendering user-submitted markdown, sanitize output:
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(renderedHtml);
Or use react-markdown which outputs React elements (inherently safe).
Code blocks overflowing
Long lines in code blocks overflow on mobile:
.prose pre {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Ensure container does not expand viewport */
.prose {
min-width: 0;
}
Missing responsive images
Images in markdown can break layouts:
.prose img {
max-width: 100%;
height: auto;
}
Inconsistent spacing
Markdown renderers output different element combinations. Normalize margins:
.prose > * + * {
margin-top: 1.25rem;
}
.prose h2 + * {
margin-top: 0.75rem;
}
WARNING: Test your prose styles with various content: long code blocks, nested lists, tables, and images. Edge cases reveal spacing issues.
Conclusion
Build technical blogs by combining a capable markdown parser with syntax highlighting, custom component renderers for callouts and anchors, and comprehensive prose CSS. Test with diverse content types and ensure proper overflow handling for code blocks on mobile.