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.tsreads 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.tsxfor global providers (Theme, Analytics, QueryClient) and base HTML structure. - Use
app/blog/[slug]/page.tsxto 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 Routermetadataobject.
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.tsxcomposes providers so pages are thin and focused:- Theme provider for dark/light mode
QueryClientProviderfor 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 incomponents/blog/.
Interactive bits (floating like button, newsletter popup)
- Implement these as small, focused React components that receive the post
slugas 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.tsxthat maps elements likeh2,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/imageor 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
- Create
content/blog/your-slug.mdxwith frontmatter:slug,title,date,description. - Keep post body in MDX; import components from
src/mdx-components.tsxwhen needed. - Use
getPost/ content loader helpers if you need computed data like reading time. - Add images to
public/assetsand reference them via absolute paths or theimagefrontmatter 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.tsexample implementation that your codebase can drop in.
Thanks for reading — let me know which next step you'd like me to take.