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.
- Log in to app.contentful.com
- Click "Add space"
- Choose "Start with an empty space" or use a template
- Name your space (e.g., "My Blog")
- Click "Create space"
3. Get Your API Keys
You'll need these to connect your application to Contentful.
- Go to Settings → API keys
- Click "Add API key"
- Name it (e.g., "Development")
- 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:
- Navigate to Content model in the top navigation
- Click "Add content type"
- Enter details:
- Name:
Blog Post - API Identifier:
blogPost(auto-generated) - Description: "Blog post articles"
- Name:
- Click "Create"
Add Fields:
| Field Name | Field Type | Required | Description |
|---|---|---|---|
| Title | Short text | Yes | Post title |
| Slug | Short text | Yes | URL-friendly identifier |
| Author | Reference | Yes | Link to Author content type |
| Publish Date | Date & time | Yes | When to publish |
| Hero Image | Media | No | Featured image |
| Excerpt | Long text | No | Short summary |
| Body | Rich text | Yes | Main content |
| Tags | References (many) | No | Link 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
| Parameter | Description | Example |
|---|---|---|
w | Width in pixels | ?w=800 |
h | Height in pixels | ?h=600 |
fit | Resize strategy | ?fit=fill (crop), ?fit=pad (letterbox) |
fm | Format | ?fm=webp, ?fm=jpg, ?fm=png |
q | Quality (1-100) | ?q=85 |
r | Corner radius | ?r=20 (rounded corners) |
bg | Background 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
- Go to Settings → Webhooks
- Click "Add webhook"
- 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
- 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
- Go to Settings → Locales
- Click "Add locale"
- Select language (e.g., Spanish - es)
- Set fallback locale
- 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
includeparameter 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
includedepth - 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
- Create your content model - Define your content types
- Add sample content - Create a few entries to test
- Build your frontend - Follow the integration guides above
- Optimize performance - Implement caching and ISR
- Set up webhooks - Auto-deploy on content changes
- Go live - Deploy your Contentful-powered site
Ready to empower your content team? Start building with Contentful today!