Contentful Integration Guide

Connecting a frontend to Contentful means choosing between its Delivery API (published content) and Preview API (drafts), picking REST or GraphQL, and wiring deterministic cache invalidation around sys.updatedAt and webhooks. This guide covers those patterns framework-agnostically, with the schema-sync and secure-preview details that scale across Jamstack and hybrid rendering. It sits within Platform Integration Deep Dives.

API Architecture & Environment Configuration

Contentful exposes two read endpoints — the Delivery API for published states, the Preview API for drafts — and both speak REST and GraphQL. GraphQL eliminates over-fetching and resolves nested data in one request; REST gives simpler cache-key semantics and plays better with edge CDNs that penalize high query-string entropy. Standardize on GraphQL for multi-tenant schema validation; reach for REST when query-string cardinality would fragment your CDN cache.

Every request needs a Space ID and a scoped token. Delivery tokens are read-only and environment-bound; CMA writes require OAuth2 or a Personal Access Token with explicit write scopes. Never ship CMA credentials in a client bundle — route management and preview mutations through a server-side proxy.

Dotenv
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_delivery_token
CONTENTFUL_PREVIEW_TOKEN=your_preview_token
CONTENTFUL_ENVIRONMENT=master

A framework-agnostic client centralizes auth, error handling, and environment toggling. This TypeScript wrapper standardizes GraphQL execution across Vite, Astro, or custom SSR runtimes:

TypeScript
// lib/contentful-client.ts
const BASE_URL = 'https://graphql.contentful.com/content/v1/spaces';

export async function contentfulQuery<T>(query: string, variables = {}, preview = false) {
  const token = preview ? process.env.CONTENTFUL_PREVIEW_TOKEN : process.env.CONTENTFUL_ACCESS_TOKEN;
  const spaceId = process.env.CONTENTFUL_SPACE_ID;
  const env = process.env.CONTENTFUL_ENVIRONMENT || 'master';
  
  const res = await fetch(`${BASE_URL}/${spaceId}/environments/${env}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ query, variables }),
  });
  
  if (!res.ok) throw new Error(`Contentful API error: ${res.status} ${res.statusText}`);
  const json = await res.json();
  if (json.errors) throw new Error(json.errors.map((e: any) => e.message).join(', '));
  return json as { data: T };
}

Data Fetching & Caching Patterns

Contentful responses carry sys.updatedAt timestamps and etag headers, which give you deterministic invalidation. Layer three caches, aligned with the HTTP Caching reference:

  1. Edge/CDN: Apply Cache-Control: public, max-age=300, stale-while-revalidate=600 to published queries. Key REST by URL or query-hash, GraphQL by persisted query, so cache hits stay consistent.
  2. Application: Front high-frequency requests with an in-memory or Redis cache, keyed by operation name plus variables, invalidated on webhook.
  3. ISR/SSR: For hybrid renderers, drive on-demand revalidation from Contentful webhooks rather than polling sys.updatedAt on every build.

Verify webhook payloads with HMAC signatures before purging, and target specific content types or entry IDs — blanket invalidation defeats the cache. When designing persisted queries, follow the GraphQL API documentation so cache keys stay stable across environments.

The three cache layers sit between a request and Contentful, with publishes invalidating only what changed:

flowchart LR
  Req["Frontend request"] --> Edge["Edge/CDN cache"]
  Edge -->|miss| App["App cache (Redis)"]
  App -->|miss| Render["ISR/SSR render"]
  Render --> CF["Contentful Delivery API"]
  Hook["Contentful webhook (publish)"] -->|"HMAC verify"| Purge["Targeted purge by content type / entry ID"]
  Purge -.->|invalidate| Edge
  Purge -.->|invalidate| App

Schema Synchronization & Type Safety

Manually maintained interfaces drift the moment an editor renames a field. Generate types instead — from the CMA or GraphQL introspection — and treat the schema as a versioned contract: commit model changes with frontend code and fail CI when a required field goes missing. Setting up TypeScript Types from Headless CMS Schemas covers the extraction pipeline, including union types for rich-text nodes and compile-time asset-reference validation.

Preview & Editorial Workflow

Preview lets editors see drafts before publish. Point the Preview API at draft states, bypassing the CDN, behind a route that takes a secret, entry ID, and locale. Validate the secret server-side, fetch the draft, and inject it into the render path — for static frameworks, via a server-side preview handler so you don’t trigger a full rebuild.

Contentful differs from Sanity Studio Customization and Strapi Self-Hosted Setup, but the preview rules are the same everywhere: isolate draft traffic, validate the request, and never pollute the production cache.

Framework-Specific Implementation

These patterns map directly onto meta-frameworks. For App Router routing, middleware, and ISR specifics, see Integrating Contentful with Next.js step by step. The one rule that trips teams up: don’t mix client-side fetches across server-rendering boundaries, or you’ll lose the cache directives the framework relies on.

Conclusion

A solid Contentful integration comes down to environment isolation, deterministic caching keyed to sys.updatedAt, and generated schema types. Treat the CMS as a typed data layer, not a presentation engine, and editorial workflows scale without dragging down frontend performance or leaking credentials.