Skip to main content

Contentful

Complete guide to getting started with Contentful, the leading enterprise-grade headless CMS with a powerful API and excellent developer experience.

What is Contentful?

Contentful is a headless, API-first content management system that separates content from presentation. It provides a flexible content infrastructure that allows you to deliver content to any platform—web, mobile, IoT devices, and more.

Key Features

  • API-First Architecture - RESTful and GraphQL APIs
  • Content Modeling - Flexible, structured content types
  • Multi-Platform Delivery - One content source for all platforms
  • Rich Media Management - Powerful image API and CDN
  • Localization - Built-in multi-language support
  • Webhooks - Real-time content update notifications
  • Apps Framework - Extend functionality with custom apps
  • Enterprise-Ready - Roles, workflows, and governance

Why Contentful?

Developer-Friendly - Excellent documentation and SDKs ✅ Scalable - Handles millions of API requests ✅ Performant - Global CDN and caching ✅ Flexible - Adapt to any content structure ✅ Reliable - 99.99% uptime SLA

Quick Start

Prerequisites

  • Node.js (v16+ recommended)
  • npm or yarn
  • Code editor (VS Code recommended)
  • Basic knowledge of JavaScript/TypeScript

1. Create a Contentful Account

# Visit https://www.contentful.com/sign-up/
# Choose the free tier to get started

Free Tier Includes:

  • 1 space
  • 25,000 content entries
  • 5 users
  • Community support

2. Create Your First Space

A "space" in Contentful is like a project or environment for your content.

  1. Log in to app.contentful.com
  2. Click "Add space"
  3. Choose "Start with an empty space" or use a template
  4. Name your space (e.g., "My Blog")
  5. Click "Create space"

3. Get Your API Keys

You'll need these to connect your application to Contentful.

  1. Go to SettingsAPI keys
  2. Click "Add API key"
  3. Name it (e.g., "Development")
  4. Save the following values:
Space ID: xxxxxxxxxxxxxx
Content Delivery API - access token: xxxxxxxxxxxxxxxxxxxxxx
Content Preview API - access token: xxxxxxxxxxxxxxxxxxxxxx

4. Install Contentful SDKs

# For JavaScript/TypeScript projects
npm install contentful

# For React projects, also install
npm install contentful-management

# For Next.js projects
npm install contentful @contentful/rich-text-react-renderer

Content Modeling

Content modeling is the foundation of your CMS. Define content types that match your needs.

Creating a Content Type

Let's create a "Blog Post" content type.

Via Web App:

  1. Navigate to Content model in the top navigation
  2. Click "Add content type"
  3. Enter details:
    • Name: Blog Post
    • API Identifier: blogPost (auto-generated)
    • Description: "Blog post articles"
  4. Click "Create"

Add Fields:

Field NameField TypeRequiredDescription
TitleShort textYesPost title
SlugShort textYesURL-friendly identifier
AuthorReferenceYesLink to Author content type
Publish DateDate & timeYesWhen to publish
Hero ImageMediaNoFeatured image
ExcerptLong textNoShort summary
BodyRich textYesMain content
TagsReferences (many)NoLink to Tag content type

Example JSON Model:

{
"name": "Blog Post",
"displayField": "title",
"fields": [
{
"id": "title",
"name": "Title",
"type": "Symbol",
"required": true,
"validations": [
{
"size": { "min": 1, "max": 200 }
}
]
},
{
"id": "slug",
"name": "Slug",
"type": "Symbol",
"required": true,
"validations": [
{
"unique": true
},
{
"regexp": {
"pattern": "^[a-z0-9-]+$"
}
}
]
},
{
"id": "publishDate",
"name": "Publish Date",
"type": "Date",
"required": true
},
{
"id": "heroImage",
"name": "Hero Image",
"type": "Link",
"linkType": "Asset",
"required": false
},
{
"id": "excerpt",
"name": "Excerpt",
"type": "Text",
"required": false
},
{
"id": "body",
"name": "Body",
"type": "RichText",
"required": true
},
{
"id": "author",
"name": "Author",
"type": "Link",
"linkType": "Entry",
"required": true,
"validations": [
{
"linkContentType": ["author"]
}
]
},
{
"id": "tags",
"name": "Tags",
"type": "Array",
"items": {
"type": "Link",
"linkType": "Entry",
"validations": [
{
"linkContentType": ["tag"]
}
]
}
}
]
}

Additional Content Types

Author:

{
name: "Author",
fields: [
{ id: "name", name: "Name", type: "Symbol", required: true },
{ id: "bio", name: "Bio", type: "Text" },
{ id: "avatar", name: "Avatar", type: "Link", linkType: "Asset" },
{ id: "socialLinks", name: "Social Links", type: "Object" }
]
}

Tag:

{
name: "Tag",
fields: [
{ id: "name", name: "Name", type: "Symbol", required: true },
{ id: "slug", name: "Slug", type: "Symbol", required: true }
]
}

Using the Contentful API

RESTful Content Delivery API

The Content Delivery API is read-only and optimized for fast content retrieval.

Basic Setup:

import { createClient } from 'contentful';

const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN
});

Fetch All Entries:

async function getAllBlogPosts() {
const response = await client.getEntries({
content_type: 'blogPost',
order: '-fields.publishDate'
});

return response.items;
}

Fetch Single Entry:

async function getBlogPostBySlug(slug) {
const response = await client.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
include: 2 // Include referenced entries (author, tags)
});

return response.items[0];
}

Fetch with Search:

async function searchBlogPosts(query) {
const response = await client.getEntries({
content_type: 'blogPost',
query: query,
limit: 10
});

return response.items;
}

Fetch with Filters:

async function getBlogPostsByTag(tagSlug) {
const response = await client.getEntries({
content_type: 'blogPost',
'fields.tags.sys.contentType.sys.id': 'tag',
'fields.tags.fields.slug': tagSlug,
include: 2
});

return response.items;
}

GraphQL API

Contentful also provides a GraphQL API for more flexible queries.

Setup:

const CONTENTFUL_GRAPHQL_URL = `https://graphql.contentful.com/content/v1/spaces/${spaceId}`;
const CONTENTFUL_ACCESS_TOKEN = process.env.CONTENTFUL_ACCESS_TOKEN;

async function fetchGraphQL(query, preview = false) {
const response = await fetch(CONTENTFUL_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${CONTENTFUL_ACCESS_TOKEN}`,
},
body: JSON.stringify({ query })
});

const json = await response.json();
return json.data;
}

Query Blog Posts:

query {
blogPostCollection(order: publishDate_DESC, limit: 10) {
items {
title
slug
publishDate
excerpt
heroImage {
url
title
width
height
}
author {
name
avatar {
url
}
}
tagsCollection {
items {
name
slug
}
}
}
}
}

Query Single Post:

query getBlogPost($slug: String!) {
blogPostCollection(where: { slug: $slug }, limit: 1) {
items {
title
slug
publishDate
excerpt
body {
json
links {
assets {
block {
sys { id }
url
title
width
height
}
}
}
}
heroImage {
url(transform: { width: 1200, height: 630, resizeStrategy: FILL })
title
}
author {
name
bio
avatar { url }
}
}
}
}

Integration with React/Next.js

Next.js Blog Example

Project Setup:

npx create-next-app my-blog
cd my-blog
npm install contentful @contentful/rich-text-react-renderer

Environment Variables (.env.local):

CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_access_token
CONTENTFUL_PREVIEW_ACCESS_TOKEN=your_preview_token

Contentful Client (lib/contentful.js):

import { createClient } from 'contentful';

export const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});

export const previewClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
host: 'preview.contentful.com',
});

export async function getAllBlogPosts(preview = false) {
const contentful = preview ? previewClient : client;

const response = await contentful.getEntries({
content_type: 'blogPost',
order: '-fields.publishDate',
include: 2
});

return response.items.map(parseBlogPost);
}

export async function getBlogPost(slug, preview = false) {
const contentful = preview ? previewClient : client;

const response = await contentful.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
include: 2
});

return parseBlogPost(response.items[0]);
}

function parseBlogPost(entry) {
return {
id: entry.sys.id,
title: entry.fields.title,
slug: entry.fields.slug,
publishDate: entry.fields.publishDate,
excerpt: entry.fields.excerpt,
body: entry.fields.body,
heroImage: entry.fields.heroImage?.fields,
author: entry.fields.author?.fields,
tags: entry.fields.tags?.map(tag => tag.fields) || []
};
}

Blog List Page (pages/blog/index.js):

import { getAllBlogPosts } from '../../lib/contentful';
import Link from 'next/link';
import Image from 'next/image';

export default function BlogIndex({ posts }) {
return (
<div className="container">
<h1>Blog</h1>

<div className="blog-grid">
{posts.map((post) => (
<article key={post.slug} className="blog-card">
{post.heroImage && (
<Image
src={`https:${post.heroImage.file.url}`}
alt={post.heroImage.title}
width={600}
height={400}
/>
)}

<h2>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h2>

<p className="meta">
By {post.author.name} on{' '}
{new Date(post.publishDate).toLocaleDateString()}
</p>

<p>{post.excerpt}</p>

<div className="tags">
{post.tags.map((tag) => (
<span key={tag.slug} className="tag-button">
{tag.name}
</span>
))}
</div>
</article>
))}
</div>
</div>
);
}

export async function getStaticProps({ preview = false }) {
const posts = await getAllBlogPosts(preview);

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

Blog Post Page (pages/blog/[slug].js):

import { getBlogPost, getAllBlogPosts } from '../../lib/contentful';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS, MARKS, INLINES } from '@contentful/rich-text-types';
import Image from 'next/image';

export default function BlogPost({ post }) {
const renderOptions = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node) => {
const { file, title } = node.data.target.fields;
return (
<Image
src={`https:${file.url}`}
alt={title}
width={file.details.image.width}
height={file.details.image.height}
/>
);
},
[BLOCKS.HEADING_2]: (node, children) => (
<h2 className="blog-h2">{children}</h2>
),
[BLOCKS.PARAGRAPH]: (node, children) => (
<p className="blog-paragraph">{children}</p>
),
[INLINES.HYPERLINK]: (node, children) => (
<a href={node.data.uri} target="_blank" rel="noopener noreferrer">
{children}
</a>
)
},
renderMark: {
[MARKS.BOLD]: (text) => <strong>{text}</strong>,
[MARKS.CODE]: (text) => <code>{text}</code>
}
};

return (
<article className="blog-post">
<header>
{post.heroImage && (
<Image
src={`https:${post.heroImage.file.url}`}
alt={post.heroImage.title}
width={1200}
height={630}
priority
/>
)}

<h1>{post.title}</h1>

<div className="meta">
<div className="author">
{post.author.avatar && (
<Image
src={`https:${post.author.avatar.file.url}`}
alt={post.author.name}
width={50}
height={50}
className="avatar"
/>
)}
<span>{post.author.name}</span>
</div>

<time dateTime={post.publishDate}>
{new Date(post.publishDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</div>

<div className="tags">
{post.tags.map((tag) => (
<span key={tag.slug} className="tag-button">
{tag.name}
</span>
))}
</div>
</header>

<div className="blog-content">
{documentToReactComponents(post.body, renderOptions)}
</div>
</article>
);
}

export async function getStaticProps({ params, preview = false }) {
const post = await getBlogPost(params.slug, preview);

if (!post) {
return { notFound: true };
}

return {
props: { post },
revalidate: 60
};
}

export async function getStaticPaths() {
const posts = await getAllBlogPosts();

return {
paths: posts.map((post) => ({
params: { slug: post.slug }
})),
fallback: 'blocking'
};
}

Integration with Angular

Install Dependencies:

npm install contentful

Contentful Service (services/contentful.service.ts):

import { Injectable } from '@angular/core';
import { createClient, Entry } from 'contentful';
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../environments/environment';

export interface BlogPost {
id: string;
title: string;
slug: string;
publishDate: string;
excerpt: string;
body: any;
heroImage?: any;
author: any;
tags: any[];
}

@Injectable({
providedIn: 'root'
})
export class ContentfulService {
private client = createClient({
space: environment.contentful.spaceId,
accessToken: environment.contentful.accessToken
});

getAllBlogPosts(): Observable<BlogPost[]> {
return from(
this.client.getEntries({
content_type: 'blogPost',
order: '-fields.publishDate',
include: 2
})
).pipe(
map(response => response.items.map(this.parseBlogPost))
);
}

getBlogPost(slug: string): Observable<BlogPost> {
return from(
this.client.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
include: 2
})
).pipe(
map(response => this.parseBlogPost(response.items[0]))
);
}

private parseBlogPost(entry: Entry<any>): BlogPost {
return {
id: entry.sys.id,
title: entry.fields.title,
slug: entry.fields.slug,
publishDate: entry.fields.publishDate,
excerpt: entry.fields.excerpt,
body: entry.fields.body,
heroImage: entry.fields.heroImage?.fields,
author: entry.fields.author?.fields,
tags: entry.fields.tags?.map((tag: Entry<any>) => tag.fields) || []
};
}
}

Component (blog-list.component.ts):

import { Component, OnInit } from '@angular/core';
import { ContentfulService, BlogPost } from '../services/contentful.service';

@Component({
selector: 'app-blog-list',
templateUrl: './blog-list.component.html',
styleUrls: ['./blog-list.component.css']
})
export class BlogListComponent implements OnInit {
posts: BlogPost[] = [];
loading = true;
error: string | null = null;

constructor(private contentfulService: ContentfulService) {}

ngOnInit(): void {
this.contentfulService.getAllBlogPosts().subscribe({
next: (posts) => {
this.posts = posts;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load blog posts';
this.loading = false;
console.error(err);
}
});
}
}

Image Optimization

Contentful's Images API provides powerful on-the-fly image transformations.

Image Parameters

// Original image URL
const imageUrl = post.heroImage.file.url;

// Resize to specific dimensions
const resized = `${imageUrl}?w=800&h=600`;

// Resize and crop
const cropped = `${imageUrl}?w=800&h=600&fit=fill`;

// Format conversion
const webp = `${imageUrl}?fm=webp&q=80`;

// Multiple parameters
const optimized = `${imageUrl}?w=1200&h=630&fit=fill&fm=webp&q=85`;

Common Transformations

ParameterDescriptionExample
wWidth in pixels?w=800
hHeight in pixels?h=600
fitResize strategy?fit=fill (crop), ?fit=pad (letterbox)
fmFormat?fm=webp, ?fm=jpg, ?fm=png
qQuality (1-100)?q=85
rCorner radius?r=20 (rounded corners)
bgBackground color?bg=rgb:ffffff

Responsive Images in Next.js

import Image from 'next/image';

function ContentfulImage({ src, alt, width, height }) {
// Contentful loader for Next.js Image component
const contentfulLoader = ({ src, width, quality }) => {
return `${src}?w=${width}&q=${quality || 75}&fm=webp`;
};

return (
<Image
loader={contentfulLoader}
src={src}
alt={alt}
width={width}
height={height}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}

Webhooks and Real-Time Updates

Webhooks notify your application when content changes in Contentful.

Setting Up Webhooks

  1. Go to SettingsWebhooks
  2. Click "Add webhook"
  3. Configure:
    • Name: "Production Deploy"
    • URL: Your webhook endpoint (e.g., https://your-site.com/api/webhook)
    • Triggers: Select events (publish, unpublish, delete)
    • Content type: Filter by specific content types
  4. Save

Webhook Handler (Next.js API Route)

// pages/api/webhook.js
import { revalidatePath } from 'next/cache';

export default async function handler(req, res) {
// Verify webhook signature (recommended for security)
const signature = req.headers['x-contentful-webhook-signature'];
// TODO: Verify signature

if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}

const { sys, fields } = req.body;

try {
// Handle different webhook events
switch (sys.type) {
case 'Entry':
if (fields.slug) {
// Revalidate the specific blog post page
await res.revalidate(`/blog/${fields.slug['en-US']}`);
await res.revalidate('/blog'); // Also revalidate blog index
}
break;

case 'Asset':
// Handle asset updates if needed
break;

case 'DeletedEntry':
// Handle entry deletion
await res.revalidate('/blog');
break;
}

return res.json({ revalidated: true });
} catch (err) {
console.error('Error revalidating:', err);
return res.status(500).json({ message: 'Error revalidating' });
}
}

Localization

Contentful has built-in support for multi-language content.

Enable Locales

  1. Go to SettingsLocales
  2. Click "Add locale"
  3. Select language (e.g., Spanish - es)
  4. Set fallback locale
  5. Save

Fetching Localized Content

// Fetch content in specific locale
const response = await client.getEntries({
content_type: 'blogPost',
locale: 'es'
});

// Fetch content in all locales
const response = await client.getEntries({
content_type: 'blogPost',
locale: '*'
});

Next.js i18n Integration

// next.config.js
module.exports = {
i18n: {
locales: ['en', 'es', 'fr'],
defaultLocale: 'en'
}
};

// pages/blog/[slug].js
export async function getStaticProps({ params, locale }) {
const post = await getBlogPost(params.slug, locale);
return { props: { post } };
}

Best Practices

1. Content Modeling

Do:

  • Start simple, iterate based on needs
  • Use references for reusable content
  • Validate fields (min/max length, patterns)
  • Use descriptive field names

Don't:

  • Over-engineer content types upfront
  • Duplicate content across entries
  • Use overly generic field names

2. API Usage

Do:

  • Cache responses when possible
  • Use include parameter to fetch linked entries
  • Implement pagination for large datasets
  • Use preview API for drafts

Don't:

  • Make excessive API calls
  • Fetch all entries without limits
  • Hardcode API keys in client-side code

3. Image Optimization

Do:

  • Use Contentful's image API for transformations
  • Serve images in modern formats (WebP)
  • Implement responsive images
  • Set appropriate quality levels

Don't:

  • Serve original, unoptimized images
  • Transform images client-side
  • Use oversized images

4. Performance

Do:

  • Use Static Site Generation (SSG) when possible
  • Implement ISR (Incremental Static Regeneration)
  • Cache API responses with CDN
  • Use webhooks for cache invalidation

Don't:

  • Fetch content on every request
  • Skip caching entirely
  • Ignore performance metrics

Common Pitfalls

1. API Rate Limits

Problem: Hitting rate limits during development

Solution:

  • Implement caching
  • Use preview API for development
  • Upgrade plan if needed

2. Circular References

Problem: Content types referencing each other

Solution:

  • Limit include depth
  • Handle circular refs in code
  • Flatten data structure

3. Large Media Files

Problem: Slow image loading

Solution:

  • Use Contentful's image API
  • Implement lazy loading
  • Optimize images before upload

Resources

Official Documentation

Starter Templates

Community

Next Steps

  1. Create your content model - Define your content types
  2. Add sample content - Create a few entries to test
  3. Build your frontend - Follow the integration guides above
  4. Optimize performance - Implement caching and ISR
  5. Set up webhooks - Auto-deploy on content changes
  6. Go live - Deploy your Contentful-powered site

Ready to empower your content team? Start building with Contentful today!