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 Settings → API 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 Settings → Webhooks
  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 Settings → Locales
  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!