Docs / CMS & Website Platforms / Build a Website with Sanity CMS and Next.js Frontend

Build a Website with Sanity CMS and Next.js Frontend

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 238 views · 3 min read

Sanity is a flexible, real-time headless CMS with a powerful structured content platform. Paired with a Next.js frontend deployed on your VPS, you get a fast, customizable website with a world-class editing experience. This guide covers self-hosting the complete stack.

Architecture Overview

Unlike fully self-hosted CMSs, Sanity uses a hybrid approach:

  • Sanity Studio: The editing interface — deployed on your VPS or hosted
  • Content Lake: Sanity's hosted real-time database (free tier: 500K API requests/month)
  • Next.js Frontend: Your website, fully self-hosted on your VPS

Set Up the Sanity Project

# Create a new Sanity project
npm create sanity@latest -- --project-id YOUR_PROJECT_ID \
  --dataset production --template blog

cd my-sanity-project

# Define your content schema
# schemas/post.ts
export default {
  name: 'post',
  title: 'Blog Post',
  type: 'document',
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (Rule: any) => Rule.required(),
    },
    {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title', maxLength: 96 },
    },
    {
      name: 'mainImage',
      title: 'Main Image',
      type: 'image',
      options: { hotspot: true },
    },
    {
      name: 'body',
      title: 'Body',
      type: 'blockContent',
    },
    {
      name: 'publishedAt',
      title: 'Published At',
      type: 'datetime',
    },
  ],
}

Build the Next.js Frontend

# Create Next.js app
npx create-next-app@latest my-website --typescript --app
cd my-website

# Install Sanity client
npm install next-sanity @sanity/image-url @portabletext/react

# Configure Sanity client
# lib/sanity.ts
import { createClient } from 'next-sanity'

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

// Fetch posts
export async function getPosts() {
  return client.fetch(`
    *[_type == "post"] | order(publishedAt desc) {
      _id, title, slug, mainImage, publishedAt,
      "excerpt": array::join(string::split(pt::text(body), "")[0..200], "")
    }
  `)
}

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

Create Dynamic Pages

// app/blog/[slug]/page.tsx
import { getPost, getPosts } from '@/lib/sanity'
import { PortableText } from '@portabletext/react'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post: any) => ({
    slug: post.slug.current,
  }))
}

export default async function PostPage({
  params
}: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  if (!post) notFound()

  return (
    <article className="max-w-3xl mx-auto py-12 px-4">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <time className="text-gray-500">
        {new Date(post.publishedAt).toLocaleDateString()}
      </time>
      <div className="prose mt-8">
        <PortableText value={post.body} />
      </div>
    </article>
  )
}

Deploy Both on Your VPS

# Build Next.js
cd /opt/my-website
npm run build

# Create systemd service
sudo cat > /etc/systemd/system/nextjs-site.service         

Was this article helpful?