Building a Modern Blog with MDX and Next.js 16
Complete guide to building a production-ready MDX blog with Next.js 16. Learn how to implement syntax highlighting, table of contents, SEO optimization, and more in this step-by-step tutorial.
Note for Business Owners: This article is for developers. If you're a business owner looking for technical help with your website, e-commerce store, mobile application, or web platform, contact us for a consultation - we'd love to help bring your vision to life.
Building a blog in 2025 doesn't have to mean dealing with complex CMSs, slow databases, or inflexible templates. If you're a developer looking to create a fast, SEO-friendly blog that's easy to maintain, MDX with Next.js 16 offers the perfect solution.
In this comprehensive tutorial, I'll walk you through building a production-ready blog system that combines the simplicity of Markdown with the power of React components. We'll implement syntax highlighting, automatic table of contents generation, reading time calculation, and comprehensive SEO optimizations - everything you need for a professional technical blog.
Whether you're building a personal blog, company blog, or documentation site, this guide will give you a solid foundation that's both developer-friendly and built for performance.
Why MDX for Blogging?
When building the Tech Pilot blog, we evaluated several content management solutions including traditional CMSs like WordPress, headless CMS platforms like Contentful, and various Markdown-based systems. After careful consideration, we chose MDX as our content format because it offers the perfect balance of simplicity and power for developer-focused content.
The Benefits of MDX for Technical Blogs
MDX (Markdown with JSX) combines the simplicity of Markdown with the full power of React components. This unique combination makes it ideal for technical blogs where you need to:
- Write content quickly: Use familiar Markdown syntax for text, headings, lists, and links
 - Add interactive elements: Embed React components like code playgrounds, demos, or custom callouts
 - Maintain version control: Store content as files in your Git repository alongside your code
 - Optimize for SEO: Generate static HTML at build time for fast loading and excellent search engine visibility
 - Eliminate infrastructure overhead: No database servers, no CMS hosting, no API rate limits
 
MDX checks all these boxes perfectly. It's Markdown with JSX support, meaning you can seamlessly mix standard Markdown with React components:
# My Blog Post
Here's some text, and here's a **custom component**:
<CustomCallout type="info">
  This is rendered as a React component!
</CustomCallout>
Project Structure
Before we dive into the implementation, let's understand how to organize a Next.js MDX blog for maintainability and scalability. A well-structured project makes it easier to add new features, debug issues, and onboard new team members.
Here's the recommended folder structure we use at Tech Pilot:
- 
content/blog/ - MDX blog posts (
*.mdxfiles)- Each post is a separate MDX file named with its slug (e.g., 
my-post.mdx) - Keep all blog content separate from application code for easy content management
 
 - Each post is a separate MDX file named with its slug (e.g., 
 - 
src/app/blog/ - Next.js App Router pages
page.tsx- Blog listing page that displays all published posts[slug]/page.tsx- Dynamic route for individual blog post pages
 - 
src/components/blog/ - Blog-specific React components
BlogCard.tsx- Post preview cards for the listing pageBlogList.tsx- Grid layout component for organizing post cardsBlogPost.tsx- Full post detail layout with metadata and contentCodeBlock.tsx- Custom syntax highlighting with copy-to-clipboardTableOfContents.tsx- Auto-generated table of contents with scroll trackingMDXComponents.tsx- Custom components available in MDX files
 - 
src/lib/blog.ts - Utility functions for blog operations
- Functions to fetch all posts, get posts by slug, calculate reading time
 - Centralized logic for parsing frontmatter and processing MDX content
 
 - 
src/types.ts - TypeScript type definitions
- Interfaces for frontmatter structure, blog posts, and related data
 
 - 
public/images/blog/ - Static blog assets
- Cover images, diagrams, screenshots, and other media files
 
 
This structure follows Next.js best practices and keeps concerns properly separated, making the codebase easy to navigate and maintain as your blog grows.
Step 1: Frontmatter Design
Every blog post starts with YAML frontmatter at the top of the file. Frontmatter is metadata that describes your post and controls how it's displayed and indexed by search engines. Think of it as the configuration file for each individual blog post.
Here's a comprehensive example of frontmatter with all the fields we use:
---
title: "Your Post Title"
slug: "your-post-slug"
date: "2025-11-03"
updated: "2025-11-03"
excerpt: "A short description of your post"
cover:
  src: "https://images.unsplash.com/photo-123..."
  alt: "Cover image description"
tags:
  - nextjs
  - mdx
  - blog
canonical: "https://www.yourtechpilot.com/blog/your-post-slug"
ogTitle: "SEO-optimized title for social sharing"
ogDescription: "Description for Open Graph"
linkedin:
  previewMaxChars: 950
  hashtags: ["Nextjs", "MDX"]
  utm: "utm_source=linkedin&utm_medium=post"
summaryBullets:
  - "Key takeaway one"
  - "Key takeaway two"
toc: true
schema:
  organization: "Your Company"
  author: "Author Name"
---
Understanding Frontmatter Fields
The frontmatter structure directly impacts your blog's SEO, social sharing, and user experience. Let's break down each field and understand its purpose:
Required Fields (Essential for every post):
- 
title: The main title of your blog post. This appears in the browser tab, search results, and as the main heading on your page. Keep it under 60 characters for optimal search engine display. - 
slug: A URL-friendly version of your title (e.g., "my-awesome-post"). This becomes part of your blog post URL like/blog/my-awesome-post. Use lowercase letters, numbers, and hyphens only. - 
date: The publication date in YYYY-MM-DD format. Search engines use this to understand content freshness, and it helps readers know if the information is current. - 
excerpt: A concise 150-160 character summary of your post. This appears in blog card previews and is used as the meta description for search engines. Write it to entice clicks! - 
tags: An array of relevant topics (e.g., ["nextjs", "typescript", "seo"]). Tags help with content organization, related post suggestions, and can improve your site's internal linking structure. 
Optional but Highly Recommended:
- 
cover: A hero image for your post withsrc(URL) andalt(description). Cover images improve engagement and are crucial for social media sharing. They also help with SEO when properly tagged. - 
canonical: The definitive URL for this content. Critical for preventing duplicate content penalties if you publish the same article on multiple platforms (like Medium or Dev.to). - 
ogTitle/ogDescription: Customized text for social media sharing (Open Graph protocol). These can be different from your regular title and excerpt, optimized specifically for social platforms. - 
toc: Set totrueto automatically generate a table of contents. Great for long-form content as it improves navigation and helps with featured snippets in search results. - 
schema: Structured data markup that helps search engines understand your content better. Can improve your chances of getting rich snippets in search results. 
Why Frontmatter Matters for SEO
Properly structured frontmatter is the foundation of technical SEO for your blog. Search engines like Google use this metadata to:
- Display rich snippets with titles, descriptions, and dates
 - Generate article cards in search results with images and authors
 - Understand content freshness for ranking purposes
 - Create knowledge graph connections between related content
 - Enable social media previews when your content is shared
 
Investing time in comprehensive frontmatter pays dividends in organic traffic over time.
Step 2: TypeScript Types
TypeScript types ensure your content structure remains consistent across all blog posts. They catch errors at build time rather than runtime, preventing broken pages or missing metadata from going live.
Here's how to define comprehensive types for your blog system:
// src/types.ts
export interface BlogPostCover {
  src: string
  alt: string
}
export interface BlogPostSchema {
  organization?: string
  author?: string
}
export interface BlogPostFrontmatter {
  title: string
  slug: string
  date: string
  updated?: string
  excerpt: string
  tags: string[]
  cover?: BlogPostCover
  canonical?: string
  ogTitle?: string
  ogDescription?: string
  summaryBullets?: string[]
  toc?: boolean
  schema?: BlogPostSchema
}
export interface BlogPost {
  slug: string
  frontmatter: BlogPostFrontmatter
  content: string
  readingTime: number
}
export interface BlogPostWithMDX extends BlogPost {
  mdxSource: string
}
These TypeScript interfaces provide strong typing for your entire blog system. They ensure that every blog post has the required metadata and that your code can safely access frontmatter properties without runtime errors.
Step 3: Blog Utilities
Now we need utility functions to interact with our MDX files. These functions handle reading files from the filesystem, parsing frontmatter, calculating reading times, and sorting posts by date.
The blog.ts utilities file is the backbone of your blog system. It provides a clean API for retrieving blog posts throughout your application. Here's the complete implementation:
// src/lib/blog.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import readingTime from 'reading-time'
import type { BlogPost, BlogPostFrontmatter } from '@/types'
const BLOG_DIR = path.join(process.cwd(), 'content', 'blog')
/**
 * Get all blog posts sorted by date (newest first)
 */
export async function getAllPosts(): Promise<BlogPost[]> {
  const files = fs.readdirSync(BLOG_DIR)
  
  const posts = files
    .filter((file) => file.endsWith('.mdx'))
    .map((file) => {
      const slug = file.replace('.mdx', '')
      const filePath = path.join(BLOG_DIR, file)
      const fileContents = fs.readFileSync(filePath, 'utf8')
      const { data, content } = matter(fileContents)
      
      return {
        slug,
        frontmatter: data as BlogPostFrontmatter,
        content,
        readingTime: Math.ceil(readingTime(content).minutes),
      }
    })
    .sort((a, b) => {
      return new Date(b.frontmatter.date).getTime() - 
             new Date(a.frontmatter.date).getTime()
    })
  
  return posts
}
/**
 * Get a single post by slug
 */
export async function getPostBySlug(
  slug: string
): Promise<BlogPost | null> {
  const filePath = path.join(BLOG_DIR, `${slug}.mdx`)
  
  if (!fs.existsSync(filePath)) {
    return null
  }
  
  const fileContents = fs.readFileSync(filePath, 'utf8')
  const { data, content } = matter(fileContents)
  
  return {
    slug,
    frontmatter: data as BlogPostFrontmatter,
    content,
    readingTime: Math.ceil(readingTime(content).minutes),
  }
}
/**
 * Extract headings for table of contents
 */
export function extractHeadings(content: string) {
  const headingRegex = /^(#{2,3})\s+(.+)$/gm
  const headings: Array<{ level: number; text: string; id: string }> = []
  
  let match
  while ((match = headingRegex.exec(content)) !== null) {
    const level = match[1].length
    const text = match[2]
    const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-')
    
    headings.push({ level, text, id })
  }
  
  return headings
}
Key Utility Functions Explained
The utility functions above provide everything you need to build a functional blog:
- 
getAllPosts(): Reads all MDX files from your content directory, parses their frontmatter, calculates reading times, and returns them sorted by date. This is used on your blog listing page. - 
getPostBySlug(): Retrieves a single post by its slug. This is used on individual blog post pages when a user clicks on a specific article. - 
extractHeadings(): Uses regex to find all H2 and H3 headings in your content. These headings are used to generate the table of contents sidebar, making it easy for readers to jump to specific sections. 
These functions leverage Node.js fs module, which only runs on the server during build time in Next.js, ensuring zero client-side JavaScript for content processing.
Step 4: Blog Listing Page
The blog listing page is where visitors discover your content. It should load fast, look attractive, and make it easy to find relevant articles. We use Next.js server components here for optimal performance.
This page fetches all posts at build time and displays them in an organized grid:
// src/app/blog/page.tsx
import type { Metadata } from 'next'
import { getAllPosts } from '@/lib/blog'
import BlogList from '@/components/blog/BlogList'
import Container from '@/components/Container'
export const metadata: Metadata = {
  title: 'Blog',
  description: 'Insights on software development and technology',
  alternates: {
    canonical: 'https://www.yourtechpilot.com/blog',
  },
}
export default async function BlogPage() {
  const posts = await getAllPosts()
  return (
    <main className="min-h-screen">
      <Container>
        <div className="py-16">
          <h1 className="text-4xl font-bold mb-8">Blog</h1>
          {posts.length > 0 ? (
            <BlogList posts={posts} />
          ) : (
            <p className="text-muted-foreground">No posts yet.</p>
          )}
        </div>
      </Container>
    </main>
  )
}
Notice how we export a metadata object - this is Next.js 16's App Router way of setting SEO metadata. The canonical URL helps prevent duplicate content issues, which is especially important for blogs.
Step 5: Blog Post Detail Page
The individual blog post page is where the magic happens. This dynamic route handles rendering any blog post based on its slug URL parameter. We need to implement several critical features here:
- Static generation at build time for maximum performance
 - Dynamic metadata generation for each post's unique SEO needs
 - 404 handling for non-existent posts
 - MDX rendering with custom components
 
Let's build the complete blog post page with proper SEO and performance optimizations:
// src/app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getAllPosts, getPostBySlug } from '@/lib/blog'
import BlogPost from '@/components/blog/BlogPost'
interface BlogPostPageProps {
  params: Promise<{ slug: string }>
}
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({
    slug: post.slug,
  }))
}
export async function generateMetadata({
  params,
}: BlogPostPageProps): Promise<Metadata> {
  const { slug } = await params
  const post = await getPostBySlug(slug)
  if (!post) {
    return { title: 'Post Not Found' }
  }
  const { frontmatter } = post
  const coverImage = frontmatter.cover?.src || '/og-default.png'
  return {
    title: frontmatter.title,
    description: frontmatter.excerpt,
    keywords: frontmatter.tags,
    openGraph: {
      title: frontmatter.ogTitle ?? frontmatter.title,
      description: frontmatter.ogDescription ?? frontmatter.excerpt,
      type: 'article',
      publishedTime: frontmatter.date,
      tags: frontmatter.tags,
      images: [
        {
          url: coverImage,
          width: 1200,
          height: 630,
          alt: frontmatter.cover?.alt ?? frontmatter.title,
        },
      ],
    },
    alternates: {
      canonical: frontmatter.canonical,
    },
  }
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params
  const post = await getPostBySlug(slug)
  if (!post) {
    notFound()
  }
  return <BlogPost post={post} />
}
Key Functions Explained
generateStaticParams(): Tells Next.js which blog posts to pre-render at build time. Returns all slug values so Next.js generates static HTML for each post, enabling instant loading.
generateMetadata(): Creates unique SEO metadata per post from frontmatter - title, description, Open Graph tags, and canonical URLs.
Error Handling: The notFound() function renders a proper 404 page for non-existent posts.
Step 6: MDX Rendering with Plugins
MDX rendering transforms markdown into interactive HTML using next-mdx-remote for server-side processing. We configure rehype and remark plugins to add heading IDs, syntax highlighting, and GitHub Flavored Markdown support:
// src/components/blog/BlogPost.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeHighlight from 'rehype-highlight'
import remarkGfm from 'remark-gfm'
import MDXComponents from './MDXComponents'
import TableOfContents from './TableOfContents'
import { extractHeadings } from '@/lib/blog'
import type { BlogPost as BlogPostType } from '@/types'
interface BlogPostProps {
  post: BlogPostType
}
export default function BlogPost({ post }: BlogPostProps) {
  const { frontmatter, content, readingTime } = post
  const headings = frontmatter.toc ? extractHeadings(content) : []
  return (
    <article className="max-w-7xl mx-auto py-8">
      <div className="flex gap-8">
        {/* Main Content */}
        <div className="flex-1 min-w-0">
          <header className="mb-8">
            <h1 className="text-4xl font-bold mb-4">
              {frontmatter.title}
            </h1>
            <p className="text-lg text-muted-foreground mb-4">
              {frontmatter.excerpt}
            </p>
            <div className="text-sm text-muted-foreground">
              {frontmatter.date} • {readingTime} min read
            </div>
          </header>
          {/* MDX Content */}
          <div className="prose prose-lg max-w-none">
            <MDXRemote
              source={content}
              options={{
                mdxOptions: {
                  remarkPlugins: [remarkGfm],
                  rehypePlugins: [
                    rehypeSlug,
                    [rehypeAutolinkHeadings, { behavior: 'wrap' }],
                    rehypeHighlight,
                  ],
                },
              }}
              components={MDXComponents}
            />
          </div>
        </div>
        {/* Table of Contents (Desktop) */}
        {headings.length > 0 && (
          <aside className="hidden lg:block w-64 shrink-0">
            <div className="sticky top-8">
              <TableOfContents headings={headings} />
            </div>
          </aside>
        )}
      </div>
    </article>
  )
}
MDX Plugins Explained
The plugins we configure significantly enhance your blog's functionality:
- 
remarkGfm(GitHub Flavored Markdown): Adds support for tables, strikethrough text, task lists, and autolinks. Essential for technical content. - 
rehypeSlug: Automatically generates IDs for all headings based on their text content. This enables deep linking (e.g.,#step-6-mdx-rendering) and is required for the table of contents to work. - 
rehypeAutolinkHeadings: Wraps heading text in clickable links, making it easy for users to copy direct links to specific sections. Great for sharing specific parts of your articles. - 
rehypeHighlight: Provides syntax highlighting for code blocks using highlight.js. Supports 180+ programming languages out of the box with beautiful color themes. 
These plugins run during the MDX compilation phase on the server, so they don't add any client-side JavaScript weight to your pages.
Step 7: Custom MDX Components
Override standard Markdown elements with custom components. Replace <img> with Next.js <Image> for optimization, style links consistently, and add interactive features:
// src/components/blog/MDXComponents.tsx
import Image from 'next/image'
import Link from 'next/link'
import CodeBlock from './CodeBlock'
const MDXComponents = {
  // Custom image with Next.js optimization
  img: ({ src, alt }: { src?: string; alt?: string }) => (
    <Image
      src={src || ''}
      alt={alt || ''}
      width={1200}
      height={630}
      className="rounded-lg"
    />
  ),
  
  // Custom links
  a: ({ href, children }: { href?: string; children: React.ReactNode }) => (
    <Link
      href={href || '#'}
      className="text-emerald-700 hover:underline"
    >
      {children}
    </Link>
  ),
  
  // Syntax highlighting with copy button
  code: CodeBlock,
  
  // Custom callout component
  Callout: ({ type = 'info', children }: {
    type?: 'info' | 'warning' | 'error'
    children: React.ReactNode
  }) => {
    const colors = {
      info: 'bg-blue-50 border-blue-200 text-blue-900',
      warning: 'bg-yellow-50 border-yellow-200 text-yellow-900',
      error: 'bg-red-50 border-red-200 text-red-900',
    }
    
    return (
      <div className={`p-4 rounded-lg border ${colors[type]} my-6`}>
        {children}
      </div>
    )
  },
}
export default MDXComponents
Step 8: Code Syntax Highlighting with Copy Button
Add one-click code copying to improve user experience. This CodeBlock component handles both inline code and multi-line blocks with a hover-activated copy button:
// src/components/blog/CodeBlock.tsx
'use client'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
export default function CodeBlock({
  children,
  className,
}: {
  children: string
  className?: string
}) {
  const [copied, setCopied] = useState(false)
  const handleCopy = () => {
    navigator.clipboard.writeText(children)
    setCopied(true)
    setTimeout(() => setCopied(false), 2000)
  }
  // Inline code
  if (!className) {
    return (
      <code className="px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono">
        {children}
      </code>
    )
  }
  // Code block with copy button
  return (
    <div className="relative group">
      <button
        onClick={handleCopy}
        className="absolute top-2 right-2 p-2 rounded bg-gray-700 hover:bg-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
        aria-label="Copy code"
      >
        {copied ? (
          <Check size={16} className="text-green-400" />
        ) : (
          <Copy size={16} className="text-gray-300" />
        )}
      </button>
      <code className={className}>{children}</code>
    </div>
  )
}
Step 9: Dynamic Table of Contents with Scroll Tracking
Enhance long-form content with a dynamic table of contents that tracks scroll position and highlights the current section using Intersection Observer:
// src/components/blog/TableOfContents.tsx
'use client'
import { useEffect, useState } from 'react'
interface Heading {
  level: number
  text: string
  id: string
}
export default function TableOfContents({ 
  headings 
}: { 
  headings: Heading[] 
}) {
  const [activeId, setActiveId] = useState<string>('')
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setActiveId(entry.target.id)
          }
        })
      },
      { rootMargin: '-80px 0px -80% 0px' }
    )
    headings.forEach(({ id }) => {
      const element = document.getElementById(id)
      if (element) observer.observe(element)
    })
    return () => observer.disconnect()
  }, [headings])
  return (
    <nav className="space-y-2">
      <h3 className="font-bold text-sm mb-4">Table of Contents</h3>
      {headings.map(({ level, text, id }) => (
        <a
          key={id}
          href={`#${id}`}
          className={`block text-sm hover:text-emerald-700 transition-colors ${
            activeId === id ? 'text-emerald-700 font-medium' : 'text-gray-600'
          }`}
          style={{ paddingLeft: `${(level - 2) * 12}px` }}
        >
          {text}
        </a>
      ))}
    </nav>
  )
}
Step 10: Installing Dependencies
Install the required packages:
pnpm add next-mdx-remote gray-matter reading-time rehype-slug rehype-autolink-headings rehype-highlight remark-gfm date-fns
Here's a breakdown of what each package does:
Core MDX Processing:
next-mdx-remote(^5.0.0): Server-side MDX rendering for Next.js, enabling React components in markdowngray-matter(^4.0.3): Parses YAML frontmatter from MDX filesreading-time(^1.5.0): Calculates estimated reading time from content length
Content Enhancement Plugins:
rehype-slug(^6.0.0): Automatically generates IDs for headingsrehype-autolink-headings(^7.1.0): Makes headings clickable with anchor linksrehype-highlight(^7.0.2): Syntax highlighting for code blocks via highlight.jsremark-gfm(^4.0.1): GitHub Flavored Markdown support (tables, task lists, etc.)
Utilities:
date-fns(^4.1.0): Date formatting and manipulation
Your package.json dependencies should look like this:
{
  "dependencies": {
    "next": "16.0.1",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "next-mdx-remote": "^5.0.0",
    "gray-matter": "^4.0.3",
    "reading-time": "^1.5.0",
    "rehype-autolink-headings": "^7.1.0",
    "rehype-highlight": "^7.0.2",
    "rehype-slug": "^6.0.0",
    "remark-gfm": "^4.0.1",
    "date-fns": "^4.1.0"
  }
}
What We Built: Complete Feature Set
✅ Content Features
- MDX support with custom React components
 - Syntax highlighting with copy-to-clipboard
 - Table of contents with active section highlighting
 - Reading time calculation (automatic)
 - Cover images with Next.js Image optimization
 
✅ SEO Features
- JSON-LD structured data for search engines
 - Open Graph tags for social sharing
 - Canonical URLs for duplicate content prevention
 - Dynamic sitemaps (auto-generated from posts)
 
✅ Developer Experience
- TypeScript for type safety
 - File-based content (no database needed)
 - Git-based workflow (write → commit → deploy)
 - Social media ready with Open Graph tags
 
Performance Results
Our blog achieves excellent performance:
- Lighthouse Score: 100/100
 - First Contentful Paint: 0.8s
 - Largest Contentful Paint: 1.2s
 - Build Time: ~5s for 10 posts
 - Bundle Size: ~95KB (First Load JS)
 
What's Next?
Potential improvements:
- Search functionality - Add full-text search with Algolia or Fuse.js
 - Related posts - Show similar content based on tags
 - Comments - Integrate Giscus or similar
 - Analytics - Track popular posts with Vercel Analytics
 - RSS feed - Generate RSS for feed readers
 - Draft posts - Support draft status in frontmatter
 
Conclusion
Congratulations! You've just built a modern, production-ready blog system with MDX and Next.js 16. This isn't just a proof-of-concept - it's a fully-featured blogging platform that can handle real-world traffic and deliver an excellent experience to both authors and readers.
What Makes This Blog System Production-Ready?
Performance: With static site generation, your blog posts load in milliseconds. Next.js 16's App Router optimizations ensure minimal JavaScript is sent to the browser, resulting in fast page loads and excellent Core Web Vitals scores. Our blog consistently achieves 100/100 Lighthouse scores.
SEO Excellence: Every post is optimized for search engines with proper meta tags, Open Graph images, canonical URLs, and JSON-LD structured data. The table of contents improves chances of featured snippets in search results.
Developer Experience: Write in Markdown, commit to Git, deploy automatically. No CMS, no database, no API maintenance. Version control your content alongside code for easy rollbacks and collaborative editing.
Type Safety: TypeScript validates frontmatter structure at build time, catching errors before production.
Flexibility: Embed React components directly in content - interactive demos, custom callouts, or any component your content needs.
Low Maintenance: No servers to patch, no databases to backup. Static files mean rock-solid reliability at minimal cost.
Real-World Results
The entire blog system you just built took approximately 4-5 hours to implement from scratch and is currently powering the production blog at yourtechpilot.com/blog.
Our metrics after launching:
- Perfect Lighthouse scores (100/100 across all categories)
 - Sub-second load times on repeat visits
 - Zero database queries or API calls at runtime
 - Deployment time: ~30 seconds for the entire site
 - Hosting cost: Essentially free on Vercel or Netlify
 - Maintenance time: Near zero after initial setup
 
Resources
Need help with a web or mobile application?
At Tech Pilot, we specialize in Next.js development, content systems, mobile application development, etc. Get in touch for a free consultation.
Found this helpful?
Share it with your network
Get Started
Let's discuss how we can help elevate your business with custom software solutions.