Published on

Build a Modern Blog with Next.js + TailwindCSS

Authors

Building a Production-Ready Blog with Next.js & TailwindCSS

Next.js Blog

Want to create a blazingly fast, SEO-optimized blog that looks professional and loads in milliseconds? Let's build one with Next.js and TailwindCSS.


Why Next.js + TailwindCSS for Blogging?

Next.js Advantages

// Next.js gives you:
// ✅ Static Site Generation (SSG) - Pre-rendered at build time
// ✅ Server-Side Rendering (SSR) - Dynamic content
// ✅ API Routes - Backend functionality
// ✅ Image Optimization - Automatic image processing
// ✅ File-based Routing - No router configuration needed

export async function getStaticProps() {
  const posts = await getAllPosts()
  return { props: { posts } }
}
// This runs at BUILD TIME, creating static HTML
// Result: Lightning-fast page loads

Performance Benefits:

  • Sub-second page loads with static generation
  • Automatic code splitting - Only load what's needed
  • Prefetching - Next.js prefetches linked pages
  • Image optimization - WebP format, lazy loading
  • Bundle size optimization - Tree shaking, minification

TailwindCSS Benefits

<!-- Instead of writing CSS: -->
<style>
  .card {
    padding: 1rem;
    background: white;
    border-radius: 0.5rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  }
</style>

<!-- Use Tailwind utility classes: -->
<div class="p-4 bg-white rounded-lg shadow-md">Content here</div>

<!-- Benefits:
  ✅ No CSS file switching
  ✅ No naming conflicts
  ✅ Responsive design built-in
  ✅ Dark mode support
  ✅ Smaller bundle size
-->

Step 1: Create Your Project

Option A: Use the Template Repository

  1. Visit Tailwind Next.js Starter Blog
  2. Click "Use this template"
  3. Name your repository (e.g., my-awesome-blog)
  4. Click "Create repository from template"
Use Template

Option B: Clone the Repository

# Clone the template
git clone https://github.com/timlrx/tailwind-nextjs-starter-blog.git my-blog

# Navigate to the project
cd my-blog

# Remove the original git history
rm -rf .git

# Initialize your own repository
git init
git add .
git commit -m "Initial commit"
Git Clone

Step 2: Install Dependencies

# Using npm
npm install

# Using yarn
yarn install

# Using pnpm (faster)
pnpm install

What gets installed:

  • Next.js framework
  • React library
  • TailwindCSS
  • MDX processor
  • Image optimization tools
  • Analytics integrations
  • And more...

Step 3: Start Development Server

# Start the development server
npm run dev

# Or with yarn
yarn dev

# Or with pnpm
pnpm dev

Open http://localhost:3000 in your browser.

You should see your blog running! 🎉

Development Features:

  • Hot Reload - Changes appear instantly
  • Error Overlay - See errors in the browser
  • Fast Refresh - Preserves component state

Method 2: Building from Scratch (For Learning)

Step 1: Create Next.js Project

# Create new Next.js app with TypeScript
npx create-next-app@latest my-blog --typescript --tailwind --app

# Navigate to project
cd my-blog

Step 2: Install Additional Dependencies

npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install gray-matter reading-time
npm install rehype-prism-plus rehype-autolink-headings
npm install remark-gfm remark-math rehype-katex
npm install -D tailwindcss-typography

Step 3: Configure MDX Support

// next.config.js
const withMDX = require('@next/mdx')({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
    providerImportSource: '@mdx-js/react',
  },
})

module.exports = withMDX({
  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com'],
  },
})

Step 4: Create Blog Structure

# Create necessary directories
mkdir -p data/blog
mkdir -p public/static/images
mkdir -p components
mkdir -p lib
mkdir -p layouts

Project Structure:

my-blog/
├── app/
│   ├── blog/
│   │   └── [slug]/
│   │       └── page.tsx
│   ├── page.tsx
│   └── layout.tsx
├── components/
│   ├── Header.tsx
│   ├── Footer.tsx
│   └── PostCard.tsx
├── data/
│   └── blog/
│       ├── my-first-post.mdx
│       └── another-post.mdx
├── lib/
│   └── mdx.ts
└── public/
    └── static/
        └── images/

Customizing Your Blog

1. Update Site Configuration

// data/siteMetadata.js
const siteMetadata = {
  title: 'My Awesome Blog',
  author: 'Your Name',
  headerTitle: 'My Blog',
  description: 'A blog about web development and technology',
  language: 'en-us',
  theme: 'system', // system, dark or light
  siteUrl: 'https://yourblog.com',
  siteRepo: 'https://github.com/yourname/blog',
  siteLogo: '/static/images/logo.png',
  image: '/static/images/avatar.png',
  socialBanner: '/static/images/twitter-card.png',
  email: 'your@email.com',
  github: 'https://github.com/yourname',
  twitter: 'https://twitter.com/yourhandle',
  linkedin: 'https://www.linkedin.com/in/yourprofile',
  locale: 'en-US',
  analytics: {
    googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
    plausibleDataDomain: '', // e.g. yourdomain.com
  },
  newsletter: {
    provider: 'buttondown', // mailchimp, buttondown, convertkit, klaviyo, revue
  },
  comment: {
    provider: 'giscus', // giscus, utterances, disqus
    giscusConfig: {
      repo: process.env.NEXT_PUBLIC_GISCUS_REPO,
      repositoryId: process.env.NEXT_PUBLIC_GISCUS_REPOSITORY_ID,
      category: process.env.NEXT_PUBLIC_GISCUS_CATEGORY,
      categoryId: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
    },
  },
}

module.exports = siteMetadata

2. Customize Colors and Styling

// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './layouts/**/*.{js,ts,jsx,tsx}',
  ],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6', // Main brand color
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
        },
        gray: {
          // Custom gray scale
          900: '#1a202c',
          800: '#2d3748',
          700: '#4a5568',
          600: '#718096',
          500: '#a0aec0',
          400: '#cbd5e0',
          300: '#e2e8f0',
          200: '#edf2f7',
          100: '#f7fafc',
          50: '#ffffff',
        },
      },
      typography: (theme) => ({
        DEFAULT: {
          css: {
            color: theme('colors.gray.700'),
            a: {
              color: theme('colors.primary.500'),
              '&:hover': {
                color: theme('colors.primary.600'),
              },
            },
            h1: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.900'),
            },
            h2: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.900'),
            },
            code: {
              color: theme('colors.pink.500'),
            },
          },
        },
        dark: {
          css: {
            color: theme('colors.gray.300'),
            a: {
              color: theme('colors.primary.400'),
              '&:hover': {
                color: theme('colors.primary.300'),
              },
            },
            h1: {
              color: theme('colors.gray.100'),
            },
            h2: {
              color: theme('colors.gray.100'),
            },
            code: {
              color: theme('colors.pink.400'),
            },
          },
        },
      }),
    },
  },
  plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
}

3. Create Your First Blog Post

---
title: 'My First Blog Post'
date: '2024-01-24'
tags: ['nextjs', 'blogging', 'web-dev']
draft: false
summary: 'This is my first blog post using Next.js and MDX!'
images: ['/static/images/first-post.jpg']
authors: ['default']
---

# Welcome to My Blog!

This is my **first post** using Next.js and MDX.

## What I'll Write About

I plan to write about:

- Web development
- JavaScript and TypeScript
- React and Next.js
- Best practices

## Code Examples Work Too!

```javascript
function greet(name) {
  console.log(`Hello, ${name}!`)
}

greet('World')
```

Stay tuned for more posts! 🚀


Save this as `data/blog/my-first-post.mdx`

---

## Key Features Explained

### 1. MDX Support - Write JSX in Markdown

```mdx
---
title: 'Interactive Blog Post'
---

import { CustomButton } from '@/components/CustomButton'

# Regular Markdown

This is normal markdown content.

## Interactive Components

<CustomButton onClick={() => alert('Clicked!')}>
  Click Me!
</CustomButton>

You can mix **markdown** with React components!

<div className="bg-blue-100 p-4 rounded">
  This is a custom styled section
</div>

Why MDX is Powerful:

  • ✅ Write markdown naturally
  • ✅ Embed React components
  • ✅ Create interactive demos
  • ✅ Reuse components across posts

2. Automatic Image Optimization

// components/Image.tsx
import NextImage from 'next/image'

export default function Image({ src, alt, ...props }) {
  return (
    <NextImage
      src={src}
      alt={alt}
      width={800}
      height={450}
      quality={90}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
      {...props}
    />
  )
}

Benefits:

  • Automatically converts to WebP
  • Lazy loads images
  • Responsive sizing
  • Blur-up placeholder
  • 60% smaller file sizes on average

3. Dark Mode Support

// components/ThemeSwitch.tsx
'use client';

import { useTheme } from 'next-themes';

export default function ThemeSwitch() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="rounded p-2 hover:bg-gray-200 dark:hover:bg-gray-700"
    >
      {theme === 'dark' ? '🌞' : '🌙'}
    </button>
  );
}

4. SEO Optimization

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPostBySlug(params.slug)

  return {
    title: post.title,
    description: post.summary,
    authors: [{ name: post.author }],
    openGraph: {
      title: post.title,
      description: post.summary,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
      images: [
        {
          url: post.image,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.summary,
      images: [post.image],
    },
  }
}

SEO Features:

  • ✅ Automatic sitemap generation
  • ✅ RSS feed
  • ✅ Open Graph tags
  • ✅ Twitter cards
  • ✅ Structured data (JSON-LD)
  • ✅ Semantic HTML

5. Analytics Integration

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { GoogleAnalytics } from '@next/third-parties/google';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <GoogleAnalytics gaId="G-XXXXXXXXXX" />
      </body>
    </html>
  );
}

Supported Analytics:

  • Google Analytics 4
  • Plausible Analytics
  • Simple Analytics
  • Vercel Analytics
  • Custom solutions

6. Comments System

// components/Comments.tsx
import Giscus from '@giscus/react';

export default function Comments() {
  return (
    <Giscus
      repo="your-username/your-repo"
      repoId="YOUR_REPO_ID"
      category="Comments"
      categoryId="YOUR_CATEGORY_ID"
      mapping="pathname"
      reactionsEnabled="1"
      emitMetadata="0"
      theme="preferred_color_scheme"
      lang="en"
    />
  );
}

Options:

  • Giscus (GitHub Discussions)
  • Utterances (GitHub Issues)
  • Disqus
  • Custom solution

Advanced Features

1. Reading Time Estimation

// lib/mdx.ts
import readingTime from 'reading-time'

export function getReadingTime(content: string) {
  const stats = readingTime(content)
  return stats.text // "5 min read"
}

2. Table of Contents

// components/TOC.tsx
export default function TOC({ headings }) {
  return (
    <nav className="toc">
      <h3>Table of Contents</h3>
      <ul>
        {headings.map((heading) => (
          <li key={heading.id} style={{ marginLeft: `${heading.level * 1}rem` }}>
            <a href={`#${heading.id}`}>{heading.text}</a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

3. Code Syntax Highlighting

// next.config.js
const rehypePrism = require('rehype-prism-plus')

module.exports = withMDX({
  options: {
    rehypePlugins: [
      [
        rehypePrism,
        {
          showLineNumbers: true,
          showCopyButton: true,
        },
      ],
    ],
  },
})

Supported Languages:

  • JavaScript, TypeScript
  • Python, Java, C++
  • HTML, CSS, SCSS
  • Bash, Shell
  • And 150+ more!

4. Math Equations with KaTeX

When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$:

$$
x = {-b \pm \sqrt{b^2-4ac} \over 2a}
$$

5. Newsletter Integration

// components/NewsletterForm.tsx
export default function NewsletterForm() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  const subscribe = async (e: React.FormEvent) => {
    e.preventDefault();
    setStatus('loading');

    try {
      const response = await fetch('/api/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      });

      if (response.ok) {
        setStatus('success');
        setEmail('');
      } else {
        setStatus('error');
      }
    } catch (error) {
      setStatus('error');
    }
  };

  return (
    <form onSubmit={subscribe}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="your@email.com"
        required
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Subscribing...' : 'Subscribe'}
      </button>
      {status === 'success' && <p>Thanks for subscribing!</p>}
      {status === 'error' && <p>Something went wrong. Try again.</p>}
    </form>
  );
}

Deployment

# Install Vercel CLI
npm i -g vercel

# Login to Vercel
vercel login

# Deploy
vercel

# For production
vercel --prod

Or use the Vercel Dashboard:

  1. Push your code to GitHub
  2. Visit vercel.com
  3. Click "New Project"
  4. Import your GitHub repository
  5. Click "Deploy"

Automatic features with Vercel:

  • ✅ Automatic deployments on git push
  • ✅ Preview deployments for PRs
  • ✅ Edge network (fast globally)
  • ✅ Analytics included
  • ✅ Zero configuration

Deploy to Netlify

# Install Netlify CLI
npm i -g netlify-cli

# Login
netlify login

# Deploy
netlify deploy

# For production
netlify deploy --prod

Deploy to GitHub Pages

# Install gh-pages
npm install --save-dev gh-pages

# Add to package.json
{
  "scripts": {
    "export": "next build && next export",
    "deploy": "gh-pages -d out"
  }
}

# Deploy
npm run export
npm run deploy

Performance Optimization

1. Lighthouse Score

The template achieves near-perfect scores:

  • Performance: 100
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100

2. Bundle Size

# Analyze bundle size
npm install @next/bundle-analyzer

# Add to next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer(nextConfig);

# Run analysis
ANALYZE=true npm run build

3. Performance Tips

// 1. Use dynamic imports for heavy components
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
  loading: () => <p>Loading...</p>,
});

// 2. Optimize images
<Image
  src="/image.jpg"
  alt="Description"
  width={800}
  height={600}
  priority // For above-fold images
  quality={90}
/>

// 3. Use font optimization
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

// 4. Implement ISR (Incremental Static Regeneration)
export async function getStaticProps() {
  const posts = await getPosts();

  return {
    props: { posts },
    revalidate: 60, // Regenerate every 60 seconds
  };
}

Troubleshooting

Common Issues

1. Port 3000 already in use

# Kill process on port 3000
npx kill-port 3000

# Or use different port
npm run dev -- -p 3001

2. Module not found errors

# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install

3. Build errors

# Clear Next.js cache
rm -rf .next

# Rebuild
npm run build

4. MDX not rendering

# Check file extension is .mdx
# Verify frontmatter format
# Ensure MDX plugins are installed
npm install @next/mdx @mdx-js/loader @mdx-js/react

Conclusion

You now have a production-ready blog with:

Lightning-fast performance (100 Lighthouse score)
SEO optimization (meta tags, sitemap, RSS)
Modern features (dark mode, MDX, analytics)
Responsive design (mobile-first approach)
Easy content management (write in Markdown)
Professional appearance (TailwindCSS styling)

Next Steps:

  1. Customize the design to match your brand
  2. Write your first blog posts
  3. Set up analytics to track visitors
  4. Add a newsletter to grow your audience
  5. Deploy to production
  6. Share your blog with the world!

Additional Resources

Official Documentation:

Learning Resources:

Community:


"The best time to start a blog was yesterday. The second best time is now. Happy blogging!" 🚀