Contentful Integration with Frontend (React/Next.js/Gatsby)

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Contentful Integration with Frontend (React/Next.js/Gatsby)
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Contentful Frontend Integration (React/Next.js/Gatsby)

Contentful as a headless CMS delivers content via JSON API, and the integration task is not just to "fetch data" but to properly organize typing, caching, incremental regeneration, and preview mode. Approaches for Next.js, Gatsby, and plain React differ significantly.

Typing from Content Types

Manually writing TypeScript interfaces for Contentful models is error-prone. Use @contentful/rich-text-types and type generation via cf-content-types-generator:

npx cf-content-types-generator \
  --spaceId $CONTENTFUL_SPACE_ID \
  --token $CONTENTFUL_MANAGEMENT_TOKEN \
  --out src/types/contentful.ts \
  --v10  # compatibility with contentful SDK v10+

Result — strictly typed interfaces for all Content Types:

// Auto-generated type
export interface TypeBlogPostFields {
  title: EntryFieldTypes.Symbol;
  slug: EntryFieldTypes.Symbol;
  body: EntryFieldTypes.RichText;
  heroImage: EntryFieldTypes.AssetLink;
  author: EntryFieldTypes.EntryLink<TypeAuthorSkeleton>;
  publishedAt: EntryFieldTypes.Date;
  tags: EntryFieldTypes.Array<EntryFieldTypes.Symbol>;
}

export type TypeBlogPostSkeleton = EntrySkeletonType<TypeBlogPostFields, 'blogPost'>;

Next.js App Router: Server Components + ISR

// lib/contentful.ts
import { createClient, type Entry } from 'contentful';
import type { TypeBlogPostSkeleton } from '@/types/contentful';

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
});

export async function getBlogPosts() {
  const entries = await client.getEntries<TypeBlogPostSkeleton>({
    content_type: 'blogPost',
    order: ['-fields.publishedAt'],
    include: 2, // depth of related entries
  });
  return entries.items;
}

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // ISR: regenerate hourly

export async function generateStaticParams() {
  const posts = await getBlogPosts();
  return posts.map((post) => ({ slug: post.fields.slug }));
}

export default async function BlogPostPage({ params }) {
  const post = await getPostBySlug(params.slug);
  return <BlogPost post={post} />;
}

Rich Text Rendering

Rich Text field returns AST structure, not HTML. Use @contentful/rich-text-react-renderer for rendering:

import { documentToReactComponents, Options } from '@contentful/rich-text-react-renderer';
import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';
import Image from 'next/image';

const renderOptions: Options = {
  renderNode: {
    [BLOCKS.EMBEDDED_ASSET]: (node) => {
      const asset = node.data.target;
      return (
        <Image
          src={`https:${asset.fields.file.url}`}
          width={asset.fields.file.details.image.width}
          height={asset.fields.file.details.image.height}
          alt={asset.fields.description || asset.fields.title}
          className="rounded-lg my-6"
        />
      );
    },
    [BLOCKS.EMBEDDED_ENTRY]: (node) => {
      const entry = node.data.target;
      if (entry.sys.contentType.sys.id === 'codeBlock') {
        return <CodeBlock code={entry.fields.code} lang={entry.fields.language} />;
      }
      return null;
    },
    [INLINES.HYPERLINK]: (node, children) => (
      <a href={node.data.uri} target="_blank" rel="noopener noreferrer">
        {children}
      </a>
    ),
  },
  renderMark: {
    [MARKS.CODE]: (text) => <code className="bg-muted px-1 rounded">{text}</code>,
  },
};

export const RichText = ({ document }) =>
  documentToReactComponents(document, renderOptions);

Gatsby: Source Plugin

// gatsby-config.ts
{
  resolve: 'gatsby-source-contentful',
  options: {
    spaceId: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
    enableTags: true,
    // Separate token for preview environment in Gatsby Cloud
    host: process.env.GATSBY_CONTENTFUL_HOST || 'cdn.contentful.com',
  },
}

GraphQL query in Gatsby Page:

query BlogPostQuery($slug: String!) {
  contentfulBlogPost(slug: { eq: $slug }) {
    title
    publishedAt
    body {
      raw
      references {
        ... on ContentfulAsset {
          contentful_id
          __typename
          url
          width
          height
          description
        }
      }
    }
    heroImage {
      gatsbyImageData(
        width: 1200
        placeholder: BLURRED
        formats: [AUTO, WEBP, AVIF]
      )
    }
  }
}

Webhook + On-demand ISR

To update pages without deployment, set up Contentful Webhook for Next.js revalidation endpoint:

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const secret = request.headers.get('x-contentful-webhook-secret');
  if (secret !== process.env.CONTENTFUL_WEBHOOK_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();
  const contentType = body.sys?.contentType?.sys?.id;
  const slug = body.fields?.slug?.['en-US'];

  if (contentType === 'blogPost' && slug) {
    revalidatePath(`/blog/${slug}`);
    revalidateTag('blog-posts');
  }

  return Response.json({ revalidated: true });
}

Image Optimization

Contentful Images API supports transformations via URL parameters. Integration with Next.js Image:

// next.config.ts
images: {
  remotePatterns: [
    { protocol: 'https', hostname: 'images.ctfassets.net' },
    { protocol: 'https', hostname: 'downloads.ctfassets.net' },
  ],
}

// Component
const contentfulLoader = ({ src, width, quality }) =>
  `${src}?w=${width}&q=${quality || 75}&fm=webp`;

<Image
  loader={contentfulLoader}
  src={`https:${asset.fields.file.url}`}
  alt={alt}
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
/>

Timeline for Typical Integration

Task Time
Basic client setup + typing 0.5 days
List and pages for one Content Type 1 day
Rich Text renderer with custom nodes 0.5–1 days
ISR + Webhook revalidation 0.5 days
Preview Mode (Draft Mode) 0.5 days
Full integration (5–10 Content Types) 3–5 days