Building Technical Blogs with Markdown

· 9 min read · Backend

Build a technical blog renderer with markdown, syntax highlighting, custom callouts, and responsive prose styling.

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.