Danniel Hansel.

(03) — Blogs — Note

How I Revamped My Blog — 2025 App Router Edition

A concise, practical walkthrough of the architecture, tech choices, and implementation patterns behind my rebuilt blog using the Next.js App Router, MDX, Tailwind, and modern React tooling.

Scroll ↓Fullstack Software Engineer — Dallas, TX

This post explains how I rebuilt this blog in 2025: the architecture decisions, file layout, and practical patterns I used so you can apply them to your own projects. I focus on the App Router, MDX content, layout composition, animation integration, and performance-minded choices.

Goals

  • Keep content authoring simple (MDX files in a content folder).
  • Use the Next.js App Router for scalable layouts and nested routing.
  • Make styling productive and consistent with Tailwind CSS.
  • Use MDX for rich posts and React components in content.
  • Keep the runtime fast and the developer experience delightful.

Tech stack (short)

  • Next.js (App Router)
  • React 18+
  • MDX for content
  • Tailwind CSS for styling
  • Framer Motion for animations
  • TanStack Query (React Query) for client data
  • Small utilities for reading content and generating metadata

Project structure (key folders)

Below is the subset of the structure I rely on for content and page composition:

content/
  blog/              # MDX posts (slug-based filenames)
src/
  app/               # Next.js App Router (layout.tsx, page.tsx, metadata.ts)
    layout.tsx
    page.tsx
    blog/
      [slug]/        # dynamic route for blog posts
        page.tsx
  components/        # shared UI (Header, Footer, BlogCard, FloatingSidebar)
  mdx-components.tsx # mappings for MDX (code blocks, images, CTA)
  lib/
    blog.ts          # helpers to load MDX, compute reading time, frontmatter
    utils.ts

How content is authored and loaded

  • Each post is an MDX file in content/blog/ with simple frontmatter (slug, title, date, description).
  • At build time (or on-demand), lib/blog.ts reads the MDX files, parses frontmatter, and compiles MDX into a renderable form the site can hydrate.
  • Example simple loader (pseudocode):
// lib/blog.ts (concept)
import fs from 'fs'
import {compileMDX} from 'mdx-compiler'

export async function getPost(slug) {
  const raw = fs.readFileSync(`content/blog/${slug}.mdx`, 'utf8')
  const {frontmatter, content} = parseFrontmatter(raw)
  const compiled = await compileMDX(content, {components: mdxComponents})
  return {frontmatter, compiled}
}

App Router patterns I used

  • Use app/layout.tsx for global providers (Theme, Analytics, QueryClient) and base HTML structure.
  • Use app/blog/[slug]/page.tsx to fetch the MDX content server-side (or via static generation), then render the compiled MDX component inside the post layout.
  • Provide metadata (title, description, open graph) per post by converting frontmatter to the App Router metadata object.

Example minimal page.tsx for a post:

// app/blog/[slug]/page.tsx (concept)
import {getPost} from '@/lib/blog'
import PostLayout from '@/components/PostLayout'

export default async function PostPage({params}) {
  const {frontmatter, compiled} = await getPost(params.slug)
  return <PostLayout frontmatter={frontmatter}>{compiled}</PostLayout>
}

export async function generateMetadata({params}) {
  const {frontmatter} = await getPost(params.slug)
  return {
    title: frontmatter.title,
    description: frontmatter.description,
  }
}

Layouts and providers

  • layout.tsx composes providers so pages are thin and focused:

    • Theme provider for dark/light mode
    • QueryClientProvider for client-side data needs (user likes, newsletter state)
    • Animation provider or global CSS for Framer Motion
  • Keep generic UI in components/ and post-specific UI in components/blog/.

Interactive bits (floating like button, newsletter popup)

  • Implement these as small, focused React components that receive the post slug as a prop.
  • Persist state (e.g., dismissed newsletter) in localStorage so popups remain non-intrusive.
  • For server-backed likes or counters, call an API route and cache client-side with TanStack Query.

Example pattern for a floating like button:

function FloatingLike({slug}) {
  const query = useQuery(['likes', slug], () => fetchLikes(slug))
  const mutation = useMutation(likePost)
  return (
    <button onClick={() => mutation.mutate(slug)}>
      👍 {query.data ?? 0}
    </button>
  )
}

MDX components and authoring ergonomics

  • Provide mdx-components.tsx that maps elements like h2, pre, and custom components (Callout, Demo) so authors can embed React components directly in posts.
  • Keep MDX files mostly prose; import small demo components only when needed.

Performance and build choices

  • Generate most pages statically where possible (Next.js static rendering). For very large sites, use incremental static regeneration or on-demand rendering.
  • Avoid shipping heavy JavaScript for readers: ensure MDX components that require interactivity are dynamically imported.
  • Use Tailwind's JIT (now default) and content scanning to keep CSS small.
  • Optimize images with Next.js next/image or a CDN.

Developer experience

  • Local workflow:
# install
npm install

# dev server
npm run dev

# build
npm run build
npm run start
  • Test writing a new post: add a file to content/blog/ with frontmatter and run the dev server — the new post should appear at /blog/<slug>.

Example authoring checklist

  1. Create content/blog/your-slug.mdx with frontmatter: slug, title, date, description.
  2. Keep post body in MDX; import components from src/mdx-components.tsx when needed.
  3. Use getPost / content loader helpers if you need computed data like reading time.
  4. Add images to public/assets and reference them via absolute paths or the image frontmatter key.

Final notes

This rebuild focuses on clear separation: content (MDX), presentation (components), and platform concerns (Next.js App Router). Following the patterns above keeps content easy to author and the application easy to maintain and scale.

If you'd like, I can:

  • Add this file to the site's index so it appears in the blog list (it should appear automatically if your blog list reads content/blog).
  • Create a small lib/blog.ts example implementation that your codebase can drop in.

Thanks for reading — let me know which next step you'd like me to take.

Next page — (03)Blogs