Contentful
Overview
The @universal-data-layer/plugin-source-contentful plugin sources all content types, entries, and assets from a Contentful space. It uses Contentful's Sync API for efficient incremental updates.
Installation
npm install @universal-data-layer/plugin-source-contentful
Basic Configuration
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
environment: 'master',
},
},
],
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
spaceId | string | required | Your Contentful space ID |
accessToken | string | required | Contentful Delivery API access token |
host | string | 'cdn.contentful.com' | API host. Use 'preview.contentful.com' for draft content |
environment | string | 'master' | Contentful environment |
nodePrefix | string | 'Contentful' | Prefix for generated node type names |
locales | string[] | all locales | Specific locales to fetch |
indexes | string[] | ['contentfulId'] | Fields to create indexes for O(1) lookups |
contentTypeFilter | function | - | Filter which content types to source |
useNameForId | boolean | true | Use content type name (vs ID) for node type names |
forceFullSync | boolean | false | Force full sync, ignoring stored sync token |
syncTokenStorage | SyncTokenStorage | file-based | Custom storage for sync tokens |
Field Indexes
The indexes option creates optimized lookup fields for single-item queries. Add field names that you'll use to fetch individual entries:
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
indexes: ['slug', 'contentfulId'],
},
},
],
});
This enables efficient queries like:
query GetProduct($slug: String!) {
contentfulProduct(slug: $slug) {
name
price
}
}
Generated Node Types
The plugin generates node types based on your Contentful content types:
- Entries:
{nodePrefix}{ContentTypeName}(e.g.,ContentfulProduct,ContentfulAuthor) - Assets:
{nodePrefix}Asset(e.g.,ContentfulAsset)
Each node includes:
contentfulId- Original Contentful sys.id (indexed by default)sys- Contentful system metadata (createdAt, updatedAt, revision, etc.)- All content type fields with transformed values
Querying Content
List All Entries
query {
allContentfulProducts {
name
slug
price
description
}
}
Single Entry by Index
query GetProduct($slug: String!) {
contentfulProduct(slug: $slug) {
name
slug
price
description
}
}
With Linked Assets
query {
allContentfulProducts {
name
image {
... on ContentfulAsset {
title
file {
url
details {
image {
width
height
}
}
}
}
}
}
}
Sync API
The plugin uses Contentful's Sync API for efficient incremental updates:
- Initial sync: Fetches all entries and assets
- Delta sync: Only fetches changes since the last sync
Sync tokens are stored in .udl/cache/contentful-sync-tokens.json by default.
Custom Token Storage
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
syncTokenStorage: {
async getSyncToken(key) {
// Return stored token or null
},
async setSyncToken(key, token) {
// Store the token
},
async clearSyncToken(key) {
// Clear the token (optional)
},
},
},
},
],
});
Preview Mode
To fetch draft/unpublished content, use the Preview API:
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_PREVIEW_TOKEN'],
host: 'preview.contentful.com',
},
},
],
});
Multiple Spaces
Use nodePrefix to source from multiple Contentful spaces:
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CMS_SPACE_ID'],
accessToken: process.env['CMS_ACCESS_TOKEN'],
nodePrefix: 'CMS',
},
},
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CRM_SPACE_ID'],
accessToken: process.env['CRM_ACCESS_TOKEN'],
nodePrefix: 'CRM',
},
},
],
});
This generates separate node types: CMSBlogPost, CRMCustomer, etc.
Content Type Filtering
Filter which content types to source:
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
contentTypeFilter: (contentType) => {
// Only source 'product' and 'category' content types
return ['product', 'category'].includes(contentType.sys.id);
},
},
},
],
});
References
Linked entries and assets are stored as references and resolved at query time. Use inline fragments to access referenced content:
query {
contentfulProduct(slug: "my-product") {
name
# Single reference
category {
... on ContentfulCategory {
name
slug
}
}
# Array of references
variants {
... on ContentfulVariant {
name
sku
price
}
}
# Asset reference
image {
... on ContentfulAsset {
file {
url
}
}
}
}
}
Rich Text
Rich text fields are stored with the raw JSON structure and extracted references:
query {
contentfulArticle(slug: "my-article") {
body {
raw
references {
... on ContentfulAsset {
contentfulId
file {
url
}
}
}
}
}
}
Error Handling
The plugin exports error classes for specific error handling:
import {
ContentfulConfigError,
ContentfulApiError,
ContentfulSyncError,
isRateLimitError,
isAuthError,
} from '@universal-data-layer/plugin-source-contentful';
try {
// ...
} catch (error) {
if (isRateLimitError(error)) {
// Handle rate limiting
}
if (isAuthError(error)) {
// Handle authentication errors
}
}
Next Steps
- Querying Data - Learn more query patterns
- Code Generation - Generate TypeScript types for your schema