Back to Blog
CMS18 min read

Building with Headless CMS: Sanity and Contentful Integration

Complete guide to integrating headless CMS platforms like Sanity and Contentful for content-driven websites.

Headless CMS platforms provide flexible content management while maintaining developer control. This guide covers integrating Sanity and Contentful into Next.js applications.

Sanity CMS Integration

Setup and Configuration

bash
# Install Sanity CLI
npm install -g @sanity/cli

# Initialize Sanity project
sanity init

Schema Definition

typescript
// schemas/post.ts
export default {
  name: 'post',
  title: '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: 'content',
      title: 'Content',
      type: 'array',
      of: [
        {
          type: 'block',
        },
        {
          type: 'image',
          fields: [
            {
              type: 'text',
              name: 'alt',
              title: 'Alt Text',
            },
          ],
        },
      ],
    },
  ],
};

Next.js Integration

typescript
// lib/sanity.ts
import { createClient } from '@sanity/client';

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

// Fetch posts
export async function getPosts() {
  const query = `*[_type == "post"] | order(_createdAt desc) {
    _id,
    title,
    slug,
    content,
    _createdAt
  }`;
  
  return await client.fetch(query);
}

// Fetch single post
export async function getPostBySlug(slug: string) {
  const query = `*[_type == "post" && slug.current == $slug][0]`;
  return await client.fetch(query, { slug });
}

Rendering Sanity Content

typescript
// components/SanityContent.tsx
import { PortableText } from '@portabletext/react';
import { urlFor } from '@/lib/sanity-image';

export function SanityContent({ content }: { content: any }) {
  return (
    <PortableText
      value={content}
      components={{
        types: {
          image: ({ value }: any) => (
            <img
              src={urlFor(value).width(800).url()}
              alt={value.alt || 'Image'}
            />
          ),
        },
      }}
    />
  );
}

Contentful Integration

Setup

bash
npm install contentful

Client Configuration

typescript
// lib/contentful.ts
import { createClient } from 'contentful';

export const client = createClient({
  space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
  accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN!,
});

// Fetch entries
export async function getEntries(contentType: string) {
  const response = await client.getEntries({
    content_type: contentType,
    order: '-sys.createdAt',
  });
  
  return response.items;
}

// Fetch single entry
export async function getEntryBySlug(slug: string, contentType: string) {
  const response = await client.getEntries({
    content_type: contentType,
    'fields.slug': slug,
    limit: 1,
  });
  
  return response.items[0];
}

TypeScript Types

typescript
// types/contentful.ts
export interface BlogPost {
  fields: {
    title: string;
    slug: string;
    content: Document;
    featuredImage: Asset;
    publishedDate: string;
  };
  sys: {
    id: string;
    createdAt: string;
    updatedAt: string;
  };
}

ISR with Headless CMS

typescript
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({
    slug: post.slug.current,
  }));
}

export const revalidate = 3600; // Revalidate every hour

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return <SanityContent content={post.content} />;
}

Preview Mode

typescript
// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get('secret');
  const slug = searchParams.get('slug');
  
  if (secret !== process.env.SANITY_PREVIEW_SECRET) {
    return new Response('Invalid token', { status: 401 });
  }
  
  draftMode().enable();
  redirect(`/blog/${slug}`);
}

Image Optimization

typescript
// lib/sanity-image.ts
import imageUrlBuilder from '@sanity/image-url';
import { client } from './sanity';

const builder = imageUrlBuilder(client);

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

// Usage with Next.js Image
<Image
  src={urlFor(post.image).width(1200).url()}
  alt={post.image.alt}
  width={1200}
  height={600}
/>

Build scalable, content-driven websites with headless CMS integration.