Building a Modern Web Blog: From Concept to Deployment

Jul 28, 2025

10 min read

title image

Building a modern web blog involves more than just writing content—it's about creating a fast, maintainable, and scalable platform that provides an excellent developer and user experience. In this post, I'll walk you through the journey of building this blog, from initial concept to production deployment.

The Vision

When I started this project, I had several goals in mind:

Technical Stack

Core Technologies

// The foundation of our modern blog
const techStack = {
  framework: "React Router v7", // Latest version with excellent DX
  language: "TypeScript", // Type safety and better developer experience
  styling: "Tailwind CSS v4", // Utility-first CSS with dark mode
  content: "MDX", // Markdown with React components
  deployment: "Fly.io", // Global edge deployment
  packageManager: "Bun", // Fast JavaScript runtime and package manager
};

Why This Stack?

React Router v7: The latest version brings significant improvements in performance and developer experience. The new data APIs and improved caching make it perfect for a content-heavy site.

TypeScript: Essential for maintaining code quality in a growing codebase. The type safety prevents bugs and improves developer productivity.

Tailwind CSS v4: The latest version with improved performance and better dark mode support. The utility-first approach makes styling consistent and maintainable.

MDX: Allows us to embed React components directly in markdown, enabling rich, interactive content.

Architecture Overview

Content Management

One of the most interesting aspects of this blog is how we handle content. Instead of a traditional CMS, we use GitHub as our content management system:

// Content is fetched directly from GitHub API
async function fetchMdxFromGitHub(path: string) {
  const url = `${GITHUB_API_BASE}/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents/${path}?ref=${GITHUB_BRANCH}`;
  const headers: Record<string, string> = {};
  if (GITHUB_TOKEN) headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`;
  const res = await fetch(url, { headers });
  if (!res.ok)
    throw new Error(`Failed to fetch MDX from GitHub: ${res.statusText}`);
  return await res.json();
}

This approach provides several benefits:

Image Handling

Images are also managed through GitHub, with automatic URL rewriting:

export async function getImageUrl(slug: string, imageName: string) {
  if (!imageName) return "";
 
  try {
    const imagePath = path.posix.join("content/posts", slug, imageName);
    const { data: fileData, error } = await tryCatch(
      fetchFileFromGitHub(imagePath),
    );
 
    if (error || !fileData || !fileData.download_url) {
      return "";
    }
 
    return fileData.download_url;
  } catch (error) {
    return "";
  }
}

Syntax Highlighting

For code blocks, we use rehype-pretty-code with custom styling:

export async function compileMDX(content: string): Promise<string> {
  const compiled = await compile(content, {
    outputFormat: "function-body",
    development: false,
    rehypePlugins: [
      [
        rehypePrettyCode,
        {
          theme: "github-dark",
          keepBackground: true,
          onVisitLine(node: any) {
            if (node.children.length === 0) {
              node.children = [{ type: "text", value: " " }];
            }
          },
          onVisitHighlightedLine(node: any) {
            node.properties.className.push("highlighted");
          },
          onVisitHighlightedWord(node: any) {
            node.properties.className = ["word"];
          },
        },
      ],
    ],
  });
 
  return String(compiled);
}

Key Features

1. Infinite Scroll Pagination

The blog implements infinite scroll for better user experience:

const loadMore = useCallback(async () => {
  if (loading || page * pageSize >= total) return;
  setLoading(true);
  void fetcher.load(`/writing?page=${page + 1}&pageSize=${pageSize}`);
}, [loading, page, pageSize, total, fetcher]);

2. Error Handling with tryCatch

We use our own tryCatch wrapper for robust error handling:

import { tryCatch } from "@herrlich-digital/trycatch-wrapper";
 
export const loader = async ({ request }: { request: Request }) => {
  const { data, error } = await tryCatch(getPosts(page, pageSize));
  if (error) {
    console.error("Error loading posts:", error);
    throw new Response("Posts not found!", { status: 404 });
  }
  return data;
};

3. Dark Mode Support

The blog features seamless dark mode with system preference detection:

const noFlashScript = `
  (function() {
    function applyTheme(themeValue) {
      const cl = document.documentElement.classList;
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      
      cl.remove('light', 'dark');
      
      if (themeValue === 'dark' || (themeValue === 'system' && prefersDark)) {
        cl.add('dark');
      }
    }
    
    let theme = 'system';
    try {
      const storedTheme = localStorage.getItem('theme');
      if (storedTheme && ['light', 'dark', 'system'].includes(storedTheme)) {
        theme = storedTheme;
      }
    } catch (e) {
      console.warn('Could not access localStorage for theme preference.');
    }
    applyTheme(theme);
  })();
`;

4. RSS Feeds

Automatic RSS feed generation for content syndication:

const feed = new Feed({
  title: "Herrlich Digital",
  description: "All Writings from Herrlich Digital",
  id: siteUrl,
  link: siteUrl,
  language: "en",
  favicon: `${siteUrl}/favicon-dark.svg`,
  copyright: `All rights reserved ${new Date().getFullYear()}, Christoph Planken`,
  updated: posts[0] ? new Date(posts[0].date) : new Date(),
  feedLinks: {
    rss2: `${siteUrl}/rss.xml`,
    atom: `${siteUrl}/atom.xml`,
    json: `${siteUrl}/feed.json`,
  },
  author: {
    name: "Christoph Planken",
    link: siteUrl,
  },
});

Development Challenges

1. MDX Runtime Compilation

One of the biggest challenges was implementing runtime MDX compilation. Since content is fetched from GitHub, we needed to compile MDX at runtime:

export default function Writing() {
  const { post, readingTime, compiledMDX } = useLoaderData<typeof loader>();
  const [MDXContent, setMDXContent] = useState<React.ComponentType | null>(null);
 
  useEffect(() => {
    void (async () => {
      const { default: Content } = await run(compiledMDX, { ...runtime });
      setMDXContent(() => Content);
    })();
  }, [compiledMDX]);
 
  if (!MDXContent) return <Spinner />;
 
  return (
    <div className="prose dark:prose-invert max-w-none flex-1 px-[10vw] py-10 md:px-[20vw]">
      <Title date={post.date} readingTime={readingTime || 0} image={post.image}>
        {post.title}
      </Title>
      <MDXProvider>
        <MDXContent />
      </MDXProvider>
    </div>
  );
}

2. GitHub API Rate Limiting

GitHub's API has rate limits, so we implemented proper error handling and caching:

const headers: Record<string, string> = {};
if (GITHUB_TOKEN) headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`;
const res = await fetch(url, { headers });
if (!res.ok)
  throw new Error(`Failed to fetch MDX from GitHub: ${res.statusText}`);

3. Image Optimization

Images are served directly from GitHub's CDN, but we needed to handle cases where images don't exist:

export async function rewriteImageUrls(
  content: string,
  slug: string,
): Promise<string> {
  const matches = [...content.matchAll(imageRegex)];
  let rewrittenContent = content;
  for (const match of matches) {
    const [fullMatch, alt, imagePath] = match;
    if (!imagePath.startsWith("http")) {
      const downloadUrl = await getImageUrl(slug, imagePath);
      if (downloadUrl) {
        rewrittenContent = rewrittenContent.replace(
          fullMatch,
          `![${alt}](${downloadUrl})`,
        );
      }
    }
  }
  return rewrittenContent;
}

Performance Optimizations

1. Lazy Loading

Components are loaded only when needed:

const MDXContent = lazy(() => import("./MDXContent"));

2. Image Optimization

Images are served from GitHub's global CDN with automatic format optimization.

3. Code Splitting

React Router automatically handles code splitting for different routes.

4. Caching Strategy

GitHub's CDN provides excellent caching for static content.

Deployment Strategy

Fly.io Deployment

We use Fly.io for global edge deployment:

# .github/workflows/fly-deploy.yml
name: Fly Deploy
on:
  push:
    branches: [main, develop]
    paths-ignore: ["content/**"]
 
jobs:
  deploy:
    name: 🚀 Deploy
    runs-on: ubuntu-latest
    needs: [lint, prettier, typecheck]
    if: ${{ github.event_name == 'push' }}

Environment Variables

Sensitive data is managed through environment variables:

const GITHUB_OWNER = process.env.GITHUB_OWNER || "your-username";
const GITHUB_REPO = process.env.GITHUB_REPO || "your-repo";
const GITHUB_BRANCH = process.env.GITHUB_BRANCH || "main";
const GITHUB_TOKEN = process.env.MY_GITHUB_TOKEN;

Lessons Learned

1. Content-First Architecture

Using GitHub as a CMS proved to be an excellent choice. It provides version control, collaboration features, and global CDN benefits without the complexity of a traditional CMS.

2. Type Safety Matters

TypeScript caught numerous potential bugs during development and made refactoring much safer.

3. Performance is User Experience

Fast loading times and smooth interactions are crucial for user engagement. The combination of React Router's optimizations and GitHub's CDN provides excellent performance.

4. Developer Experience is Key

Good tooling, clear error messages, and fast development cycles make the development process enjoyable and productive.

Future Enhancements

Planned Features

  1. Search Functionality: Full-text search across all posts
  2. Comment System: Disqus or custom comment integration
  3. Analytics: Privacy-focused analytics
  4. Email Newsletter: Automated email campaigns
  5. SEO Improvements: Better meta tags and structured data

Technical Improvements

  1. Caching Layer: Redis for better performance
  2. Image Optimization: WebP format and responsive images
  3. PWA Features: Offline support and app-like experience
  4. Internationalization: Multi-language support

Conclusion

Building this modern web blog has been an exciting journey that combines the best of modern web technologies with practical content management solutions. The result is a fast, maintainable, and scalable platform that provides an excellent experience for both developers and users.

The key to success was choosing the right tools for the job and focusing on user experience from the start. By leveraging GitHub's infrastructure for content management and modern web technologies for the frontend, we've created a blog that's both powerful and simple to maintain.

Whether you're building your own blog or working on a similar project, remember that the best architecture is one that serves your users while making your life as a developer easier. Sometimes the simplest solution—like using GitHub as a CMS—is the most effective.


This blog itself is a testament to the technologies and practices discussed in this post. Every feature mentioned here is actively used in the codebase, and the performance optimizations ensure a smooth reading experience.