Using Plugins

Contentful

Source content from Contentful CMS

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

Terminal
npm install @universal-data-layer/plugin-source-contentful

Basic Configuration

udl.config.ts
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

OptionTypeDefaultDescription
spaceIdstringrequiredYour Contentful space ID
accessTokenstringrequiredContentful Delivery API access token
hoststring'cdn.contentful.com'API host. Use 'preview.contentful.com' for draft content
environmentstring'master'Contentful environment
nodePrefixstring'Contentful'Prefix for generated node type names
localesstring[]all localesSpecific locales to fetch
indexesstring[]['contentfulId']Fields to create indexes for O(1) lookups
contentTypeFilterfunction-Filter which content types to source
useNameForIdbooleantrueUse content type name (vs ID) for node type names
forceFullSyncbooleanfalseForce full sync, ignoring stored sync token
syncTokenStorageSyncTokenStoragefile-basedCustom 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:

udl.config.ts
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:

  1. Initial sync: Fetches all entries and assets
  2. 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

udl.config.ts
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:

udl.config.ts
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:

udl.config.ts
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:

udl.config.ts
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