Building a Modern Developer Blog with Markdown and Static Rendering

· 11 min read · Frontend Development

Build a modern developer blog with React, markdown rendering, syntax highlighting, table of contents, static pre-rendering, and SEO optimization.

Building a Modern Developer Blog with Markdown and Static Rendering

Developer blogs have specific requirements that generic blogging platforms do not handle well: syntax-highlighted code blocks, table of contents generation, frontmatter metadata, and fast static rendering. Building your own gives you full control over these features.

This guide covers building a developer blog using React, markdown, and static rendering — the same stack used to build this blog. For more on the markdown rendering side, see building technical blogs with markdown.

Problem

Developer blogs need features that WordPress and Medium do not provide:

  • Code blocks with language-specific syntax highlighting
  • Automatic table of contents from headings
  • Fast page loads (static rendering, not SSR on every request)
  • Markdown authoring (not WYSIWYG editors)
  • SEO optimization for technical content
  • RSS feeds for subscribers

Architecture

Authoring:  Markdown content → Database (Supabase)
                                    ↓
Build:      Fetch posts → React components → Static HTML
                                    ↓
Deploy:     Static files → CDN (Vercel/Cloudflare)
                                    ↓
Runtime:    Hydrate → SPA navigation between posts

Content lives in the database for easy CMS editing. The build step pre-renders every post to static HTML for SEO and performance. The client hydrates for smooth SPA navigation.

Markdown Rendering

Use react-markdown with plugins for the full feature set:

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";

function MarkdownContent({ content }: { content: string }) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeHighlight]}
      components={{
        h2: ({ children }) => {
          const id = slugify(children);
          return <h2 id={id}>{children}</h2>;
        },
        code: ({ className, children }) => {
          const language = className?.replace("language-", "");
          return (
            <pre>
              <code className={className}>{children}</code>
              <CopyButton text={String(children)} />
            </pre>
          );
        },
      }}
    />
  );
}

Custom components override default rendering for headings (adding IDs for anchor links) and code blocks (adding copy buttons).

Table of Contents Generation

Extract headings from the markdown source:

interface TocItem {
  id: string;
  text: string;
  level: number;
}

function extractHeadings(content: string): TocItem[] {
  const regex = /^(#{2,3})\s+(.+)$/gm;
  const headings: TocItem[] = [];

  let match;
  while ((match = regex.exec(content)) !== null) {
    headings.push({
      id: slugify(match[2]),
      text: match[2],
      level: match[1].length,
    });
  }

  return headings;
}

Render it as a sticky sidebar on desktop:

function TableOfContents({ content }: { content: string }) {
  const headings = extractHeadings(content);
  const [activeId, setActiveId] = useState("");

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setActiveId(entry.target.id);
          }
        });
      },
      { rootMargin: "-20% 0px -60% 0px" }
    );

    headings.forEach(({ id }) => {
      const el = document.getElementById(id);
      if (el) observer.observe(el);
    });

    return () => observer.disconnect();
  }, [headings]);

  return (
    <nav className="sticky top-32">
      {headings.map((h) => (
        <a
          key={h.id}
          href={`#${h.id}`}
          className={activeId === h.id ? "text-accent" : "text-muted"}
          style={{ paddingLeft: `${(h.level - 2) * 16}px` }}
        >
          {h.text}
        </a>
      ))}
    </nav>
  );
}

Static Pre-Rendering

Fetch all posts at build time and render them to static HTML:

async function prerender() {
  const posts = await fetchAllPosts();

  for (const post of posts) {
    const html = renderToString(
      <BlogPostPage post={post} />
    );

    const outputPath = `dist/blog/${post.slug}/index.html`;
    await writeFile(outputPath, wrapInShell(html));
  }
}

Each post gets its own index.html file. Search engines crawl the static HTML. Users get instant page loads.

SEO for Technical Content

Each post needs proper meta tags:

function SEO({ post }: { post: BlogPost }) {
  return (
    <Helmet>
      <title>{post.seo_title || post.title}</title>
      <meta name="description" content={post.seo_description} />
      <meta property="og:title" content={post.title} />
      <meta property="og:type" content="article" />
      <meta property="og:image" content={post.cover_image} />
      <link rel="canonical" href={`https://example.com/blog/${post.slug}`} />
    </Helmet>
  );
}

RSS Feed

Generate an RSS feed at build time:

function generateRSS(posts: BlogPost[]): string {
  const items = posts.map((post) => `
    <item>
      <title>${escapeXml(post.title)}</title>
      <link>https://example.com/blog/${post.slug}</link>
      <description>${escapeXml(post.excerpt)}</description>
      <pubDate>${new Date(post.created_at).toUTCString()}</pubDate>
    </item>
  `).join("");

  return `<?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0">
      <channel>
        <title>Developer Blog</title>
        ${items}
      </channel>
    </rss>`;
}

Common Mistakes

Mistake 1: Client-side only rendering. Search engines can execute JavaScript but prefer static HTML. Pre-render for SEO.

Mistake 2: No code copy button. Developers read your blog to use your code. Make copying easy.

Mistake 3: No table of contents on long posts. Technical articles over 1,500 words need navigation. A sticky TOC improves time-on-page.

Takeaways

A developer blog built with React and markdown gives you syntax highlighting, auto-generated table of contents, static rendering for SEO, and full control over the reading experience. Store content in a database for CMS editing, pre-render to static HTML for performance, and hydrate for SPA navigation.