How to Build a Blog with Next.js, TailwindCSS, and Sanity CMS - Nayaka Yoga Pradipta
ID | EN

How to Build a Blog with Next.js, TailwindCSS, and Sanity CMS

Sunday, Dec 22, 2024

Building a modern blog is no longer as complicated as it used to be. With the combination of Next.js, TailwindCSS, and Sanity CMS, you can create a fast, SEO-friendly, and easy-to-manage blog. In this tutorial, I’ll guide you step-by-step from zero to deployment.

What Will We Build?

We’ll create a blog with these features:

  • Homepage with article list
  • Article detail page with content from Sanity
  • Modern styling with TailwindCSS
  • CMS dashboard for content management

Prerequisites

  • Node.js version 18 or later
  • Text editor (VS Code recommended)
  • Sanity account (free)
  • Basic knowledge of React and JavaScript

Step 1: Setup Next.js Project

Open your terminal and run the following command to create a new Next.js project:

npx create-next-app@latest my-blog
cd my-blog

Next.js Installation

When prompted, select these options:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes

Step 2: Setup Sanity CMS

Install Sanity CLI and create a Sanity project inside the Next.js folder:

npm create sanity@latest -- --template clean --create-project "My Blog" --dataset production --output-path sanity

Sanity Homepage

This will create a sanity folder inside your project with Sanity Studio.

Step 3: Create Schema for Blog Post

Create a schema file for blog posts at sanity/schemaTypes/post.ts:

import { defineField, defineType } from 'sanity'

export default defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
        maxLength: 96,
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'excerpt',
      title: 'Excerpt',
      type: 'text',
      rows: 3,
    }),
    defineField({
      name: 'mainImage',
      title: 'Main image',
      type: 'image',
      options: {
        hotspot: true,
      },
    }),
    defineField({
      name: 'publishedAt',
      title: 'Published at',
      type: 'datetime',
    }),
    defineField({
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [
        { type: 'block' },
        { type: 'image' },
      ],
    }),
  ],
})

Sanity Schema

Then register the schema in sanity/schemaTypes/index.ts:

import post from './post'

export const schemaTypes = [post]

Step 4: Run Sanity Studio

Navigate to the sanity folder and run the studio:

cd sanity
npm run dev

Sanity Studio

Open http://localhost:3333 and create some posts for testing.

Step 5: Install Sanity Client in Next.js

Go back to the project root and install dependencies:

cd ..
npm install @sanity/client @sanity/image-url @portabletext/react

Create file src/lib/sanity.ts for client configuration:

import { createClient } from '@sanity/client'
import imageUrlBuilder from '@sanity/image-url'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: 'production',
  useCdn: true,
  apiVersion: '2024-01-01',
})

const builder = imageUrlBuilder(client)

export function urlFor(source: any) {
  return builder.image(source)
}

Add environment variable in .env.local:

NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id

Step 6: Create Blog List Page

Edit src/app/page.tsx to display the blog list:

import { client, urlFor } from '@/lib/sanity'
import Link from 'next/link'
import Image from 'next/image'

async function getPosts() {
  const query = `*[_type == "post"] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    mainImage,
    publishedAt
  }`
  return client.fetch(query)
}

export default async function Home() {
  const posts = await getPosts()

  return (
    <main className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">My Blog</h1>
      <div className="grid gap-8">
        {posts.map((post: any) => (
          <Link 
            key={post._id} 
            href={`/blog/${post.slug.current}`}
            className="group"
          >
            <article className="border rounded-lg overflow-hidden hover:shadow-lg transition">
              {post.mainImage && (
                <Image
                  src={urlFor(post.mainImage).width(800).height(400).url()}
                  alt={post.title}
                  width={800}
                  height={400}
                  className="w-full h-48 object-cover"
                />
              )}
              <div className="p-6">
                <h2 className="text-2xl font-semibold group-hover:text-blue-600">
                  {post.title}
                </h2>
                <p className="text-gray-600 mt-2">{post.excerpt}</p>
              </div>
            </article>
          </Link>
        ))}
      </div>
    </main>
  )
}

Blog Demo Homepage

Step 7: Create Post Detail Page

Create file src/app/blog/[slug]/page.tsx:

import { client, urlFor } from '@/lib/sanity'
import { PortableText } from '@portabletext/react'
import Image from 'next/image'
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const query = `*[_type == "post" && slug.current == $slug][0] {
    _id,
    title,
    slug,
    mainImage,
    publishedAt,
    body
  }`
  return client.fetch(query, { slug })
}

export default async function BlogPost({ 
  params 
}: { 
  params: { slug: string } 
}) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()
  }

  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      {post.mainImage && (
        <Image
          src={urlFor(post.mainImage).width(1200).height(600).url()}
          alt={post.title}
          width={1200}
          height={600}
          className="w-full rounded-lg mb-8"
        />
      )}
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <time className="text-gray-500 mb-8 block">
        {new Date(post.publishedAt).toLocaleDateString('en-US', {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        })}
      </time>
      <div className="prose prose-lg max-w-none">
        <PortableText value={post.body} />
      </div>
    </article>
  )
}

Step 8: Styling with TailwindCSS

Install the typography plugin for content styling:

npm install @tailwindcss/typography

Tailwind Typography

Add it to tailwind.config.ts:

import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}

export default config

Step 9: Configure Next.js for Images

Add Sanity domain in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
      },
    ],
  },
}

module.exports = nextConfig

Step 10: Run the Project

Now run the Next.js project:

npm run dev

Open http://localhost:3000 and see your blog!

Conclusion

Congratulations! You’ve successfully built a blog with Next.js, TailwindCSS, and Sanity CMS. This combination is very powerful because:

  1. Next.js provides excellent performance and SEO
  2. TailwindCSS makes styling fast and consistent
  3. Sanity CMS provides a user-friendly dashboard for content management

Have questions? Reach out to me on Twitter @nayakayp!