GithubLinkedIn

How I Built This Personal Blog (Next.js + Markdown + Tailwind)

This site is a lightweight, static personal blog built with Next.js, Tailwind CSS, and Markdown. It’s optimized for fast loads, simple authoring, and an easy reading experience with light/dark themes.

Technology Stack

  • Next.js 16 (React 19) for the app framework and static generation (SSG)
  • Tailwind CSS 2 for utility‑first styling and theme tokens
  • Markdown for content, parsed by gray-matter and rendered with remark + remark-html
  • date-fns for date formatting
  • Vercel Analytics for privacy‑friendly traffic insights

Project Structure

personalblog/
├─ components/
│  ├─ layout.js          # Site chrome (header, avatar, footer), wraps pages
│  └─ Toggle.js          # Light/Dark theme switch
├─ context/
│  └─ themeContext.js    # Theme provider + SSR-safe hydration
├─ lib/
│  └─ posts.js           # Markdown loading, sorting, and HTML conversion
├─ pages/
│  ├─ index.js           # Home page listing all posts
│  └─ posts/[id].js      # Dynamic routes for individual posts
├─ posts/                # Markdown posts with frontmatter
├─ public/               # Images, icons (e.g., GitHub/LinkedIn SVGs)
├─ styles/               # Global/util styles (Tailwind consumed)
├─ tailwind.config.js    # Tailwind + theme tokens wired to CSS variables
└─ next.config.js        # Next.js config (kept minimal)

The repo started from the official Next.js “Learn” starter (see README.md), then added Tailwind, a theme system, Markdown parsing, and small UI refinements.

Content Pipeline (Markdown → HTML)

Posts live in posts/ as .md files with YAML frontmatter. The loader in lib/posts.js:

  1. Reads all files in posts/
  2. Uses gray-matter to extract frontmatter (title, date, etc.)
  3. Converts markdown content to HTML via remark + remark-html
  4. Sorts posts by date descending for the homepage

Core utilities (excerpt):

// lib/posts.js
import matter from 'gray-matter'
import remark from 'remark'
import html from 'remark-html'

export function getSortedPostData() {
  // read files, parse frontmatter, sort by date desc
}

export async function getPostData(id) {
  const matterResult = matter(fileContents)
  const processedContent = await remark().use(html).process(matterResult.content)
  return { id, contentHtml: processedContent.toString(), ...matterResult.data }
}

This keeps authoring simple: write Markdown with a short frontmatter block, commit, and the site rebuilds with your new post.

Pages and Routing (SSG)

  • pages/index.js uses getStaticProps to call getSortedPostData() and render a grid of posts.
  • pages/posts/[id].js implements dynamic routes with getStaticPaths + getStaticProps, rendering the converted HTML with dangerouslySetInnerHTML.

This approach fully statically generates all posts at build time for great performance and stability.

UI, Layout, and Theming

  • Layout lives in components/layout.js and wraps every page with:
    • A glass-style sticky header with GitHub/LinkedIn icons
    • An avatar + name banner
    • A simple footer
  • The theme system uses Tailwind’s darkMode: 'class' plus a small context provider (context/themeContext.js).
    • Default is dark during SSR to avoid mismatches.
    • On the client, the stored preference (or system preference) is applied.
    • components/Toggle.js switches between light/dark.

Tailwind tokens (e.g., bg-primary, text-secondary) are wired to CSS variables in tailwind.config.js, making theming clean and scalable without duplicating utility classes.

Analytics

@vercel/analytics is initialized on the homepage to capture high‑level usage without manual setup.

Local Development

npm install
npm run dev
# visit http://localhost:3000

To create a new post, add a Markdown file in posts/:

---
title: 'Your Post Title'
date: '2025-12-07'
---

Your content here in Markdown.

Commit and push; the post appears on the homepage after the next build.

Notable Implementation Details

  • Sorting by date ensures the newest post appears first on the homepage.
  • Dates are rendered via a small Date component (uses date-fns).
  • The theme toggle avoids hydration issues by rendering a placeholder until the client mounts.
  • The markdown converter is intentionally minimal (remark-html) to keep bundles small.

That’s it — a fast, minimal, and pleasant writing setup that stays out of the way.