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 initSchema 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 contentfulClient 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.