Skip to main content
Technology & EngineeringCms Services388 lines

Hygraph

Build with Hygraph (formerly GraphCMS) as a GraphQL-native headless CMS.

Quick Summary37 lines
You are an expert in integrating Hygraph as a headless CMS. Hygraph is a
GraphQL-native headless CMS that provides a fully auto-generated GraphQL API
from your content schema. It supports content federation, allowing you to
combine content from Hygraph with data from external systems in a single

## Key Points

- **Post** model: title (String), slug (String, unique), excerpt (String),
- **Author** model: name (String), bio (String), photo (Asset)
- **Category** model: name (String), slug (String, unique)
- Use the `stage: PUBLISHED` argument in queries for production and omit it (or use `DRAFT`) only behind preview mode
- Apply image transformations via the `url(transformation: ...)` argument in queries instead of client-side resizing
- Use Hygraph's built-in pagination (`first`, `skip`, and the `Connection` aggregate) instead of fetching all records at once
- Leverage content federation to join Hygraph content with external APIs (e.g., product data from Shopify) in a single schema
- Wrap `graphql-request` calls with Next.js `fetch` and `next.tags` for granular on-demand revalidation
- Exposing the permanent auth token on the client side — use a public Content API with read-only permissions for client-side fetching, or fetch exclusively on the server
- Querying `DRAFT` stage content in production — draft content is only visible to authenticated requests and should be gated behind preview mode
- Not handling Rich Text `json` output properly — use `@graphcms/rich-text-react-renderer` to render the AST; falling back to `html` loses embed and asset rendering
- Ignoring rate limits on the Content API — Hygraph enforces rate limits per second; use caching and ISR to reduce direct API calls

## Quick Example

```bash
npm install graphql graphql-request
# Or with urql / Apollo
npm install @urql/core graphql
npm install @apollo/client graphql
```

```env
HYGRAPH_ENDPOINT=https://your-region.hygraph.com/v2/your-project-id/master
HYGRAPH_TOKEN=your-permanent-auth-token
HYGRAPH_WEBHOOK_SECRET=your-webhook-secret
HYGRAPH_PREVIEW_TOKEN=your-draft-token
```
skilldb get cms-services-skills/HygraphFull skill: 388 lines
Paste into your CLAUDE.md or agent config

Hygraph — CMS Integration

You are an expert in integrating Hygraph as a headless CMS. Hygraph is a GraphQL-native headless CMS that provides a fully auto-generated GraphQL API from your content schema. It supports content federation, allowing you to combine content from Hygraph with data from external systems in a single GraphQL query.

Core Philosophy

Overview

Hygraph exposes three API endpoints: a high-performance Content API (read-only, CDN-backed), a Content API with draft stages, and a Management API for programmatic schema and content changes. All APIs are GraphQL. Content is organized into models, and each model auto-generates query, mutation, and subscription types.

Setup & Configuration

Install

npm install graphql graphql-request
# Or with urql / Apollo
npm install @urql/core graphql
npm install @apollo/client graphql

Environment variables

HYGRAPH_ENDPOINT=https://your-region.hygraph.com/v2/your-project-id/master
HYGRAPH_TOKEN=your-permanent-auth-token
HYGRAPH_WEBHOOK_SECRET=your-webhook-secret
HYGRAPH_PREVIEW_TOKEN=your-draft-token

Client configuration

// lib/hygraph.ts
import { GraphQLClient } from 'graphql-request';

export const hygraph = new GraphQLClient(process.env.HYGRAPH_ENDPOINT!, {
  headers: {
    Authorization: `Bearer ${process.env.HYGRAPH_TOKEN}`,
  },
});

// Preview client for draft content
export const hygraphPreview = new GraphQLClient(process.env.HYGRAPH_ENDPOINT!, {
  headers: {
    Authorization: `Bearer ${process.env.HYGRAPH_PREVIEW_TOKEN}`,
  },
});

Core Patterns

Content modeling

Define models in the Hygraph UI. A typical blog schema:

  • Post model: title (String), slug (String, unique), excerpt (String), content (Rich Text), coverImage (Asset), author (relation to Author), categories (relation to Category, many), publishedAt (DateTime)
  • Author model: name (String), bio (String), photo (Asset)
  • Category model: name (String), slug (String, unique)

Querying content

// lib/hygraph.queries.ts
import { gql } from 'graphql-request';

export const GET_POSTS = gql`
  query GetPosts($first: Int = 20, $skip: Int = 0) {
    posts(first: $first, skip: $skip, orderBy: publishedAt_DESC, stage: PUBLISHED) {
      id
      title
      slug
      excerpt
      publishedAt
      coverImage {
        url(transformation: { image: { resize: { width: 800, fit: crop } } })
        width
        height
      }
      author {
        name
        photo {
          url(transformation: { image: { resize: { width: 64, height: 64, fit: crop } } })
        }
      }
      categories {
        name
        slug
      }
    }
    postsConnection {
      aggregate {
        count
      }
    }
  }
`;

export const GET_POST_BY_SLUG = gql`
  query GetPostBySlug($slug: String!) {
    post(where: { slug: $slug }, stage: PUBLISHED) {
      id
      title
      slug
      content {
        html
        json
      }
      publishedAt
      coverImage {
        url
        width
        height
      }
      author {
        name
        bio
        photo {
          url
        }
      }
      categories {
        name
        slug
      }
      seo {
        title
        description
        image {
          url
        }
      }
    }
  }
`;

Data fetching in Next.js App Router

// app/blog/page.tsx
import { hygraph } from '@/lib/hygraph';
import { GET_POSTS } from '@/lib/hygraph.queries';
import Link from 'next/link';
import Image from 'next/image';

interface PostsResponse {
  posts: Array<{
    id: string;
    title: string;
    slug: string;
    excerpt: string;
    coverImage: { url: string; width: number; height: number };
  }>;
}

export default async function BlogPage() {
  const { posts } = await hygraph.request<PostsResponse>(GET_POSTS, {
    first: 20,
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>
            <Image
              src={post.coverImage.url}
              alt={post.title}
              width={800}
              height={450}
            />
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </Link>
        </li>
      ))}
    </ul>
  );
}

Rich Text rendering

// components/RichTextRenderer.tsx
import { RichText } from '@graphcms/rich-text-react-renderer';
import type { RichTextContent } from '@graphcms/rich-text-types';
import Image from 'next/image';

interface Props {
  content: RichTextContent;
}

export function RichTextRenderer({ content }: Props) {
  return (
    <RichText
      content={content}
      renderers={{
        h1: ({ children }) => <h1 className="text-4xl font-bold">{children}</h1>,
        h2: ({ children }) => <h2 className="text-3xl font-semibold">{children}</h2>,
        p: ({ children }) => <p className="leading-relaxed">{children}</p>,
        a: ({ children, href, openInNewTab }) => (
          <a
            href={href}
            target={openInNewTab ? '_blank' : '_self'}
            rel={openInNewTab ? 'noopener noreferrer' : undefined}
          >
            {children}
          </a>
        ),
        img: ({ src, altText, width, height }) => (
          <Image
            src={src!}
            alt={altText || ''}
            width={width || 800}
            height={height || 450}
          />
        ),
        code_block: ({ children }) => (
          <pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
            <code>{children}</code>
          </pre>
        ),
        embed: {
          Post: ({ nodeId, title, slug }) => (
            <a href={`/blog/${slug}`} className="embedded-post">
              {title}
            </a>
          ),
        },
      }}
    />
  );
}

Localization

// Fetching localized content
export const GET_LOCALIZED_PAGE = gql`
  query GetPage($slug: String!, $locale: Locale!) {
    page(where: { slug: $slug }, locales: [$locale, en]) {
      title
      content {
        html
      }
      localizations(includeCurrent: false) {
        locale
      }
    }
  }
`;

// Usage
const page = await hygraph.request(GET_LOCALIZED_PAGE, {
  slug: 'about',
  locale: 'de',
});

Webhook-triggered revalidation

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

interface HygraphWebhookPayload {
  operation: 'publish' | 'unpublish' | 'update' | 'delete';
  data: {
    __typename: string;
    slug?: string;
    id: string;
    stage: string;
  };
}

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('gcms-signature');

  const expectedSig = crypto
    .createHmac('sha256', process.env.HYGRAPH_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('base64');

  if (signature !== expectedSig) {
    return new Response('Invalid signature', { status: 401 });
  }

  const payload: HygraphWebhookPayload = JSON.parse(rawBody);
  const typeName = payload.data.__typename.toLowerCase();

  revalidateTag(typeName);

  if (payload.data.slug) {
    revalidateTag(`${typeName}:${payload.data.slug}`);
  }

  return Response.json({
    revalidated: true,
    type: typeName,
    now: Date.now(),
  });
}

Caching with Next.js fetch tags

// lib/hygraph.cached.ts
import { hygraph } from './hygraph';

export async function fetchHygraph<T>(
  query: string,
  variables?: Record<string, any>,
  tags?: string[]
): Promise<T> {
  // graphql-request uses fetch under the hood;
  // for cache tags, use native fetch with the Hygraph endpoint.
  const res = await fetch(process.env.HYGRAPH_ENDPOINT!, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.HYGRAPH_TOKEN}`,
    },
    body: JSON.stringify({ query, variables }),
    next: { tags: tags ?? ['hygraph'] },
  });

  const json = await res.json();

  if (json.errors) {
    throw new Error(json.errors.map((e: any) => e.message).join('\n'));
  }

  return json.data;
}

Best Practices

  • Use the stage: PUBLISHED argument in queries for production and omit it (or use DRAFT) only behind preview mode
  • Apply image transformations via the url(transformation: ...) argument in queries instead of client-side resizing
  • Use Hygraph's built-in pagination (first, skip, and the Connection aggregate) instead of fetching all records at once
  • Leverage content federation to join Hygraph content with external APIs (e.g., product data from Shopify) in a single schema
  • Wrap graphql-request calls with Next.js fetch and next.tags for granular on-demand revalidation

Common Pitfalls

  • Exposing the permanent auth token on the client side — use a public Content API with read-only permissions for client-side fetching, or fetch exclusively on the server
  • Querying DRAFT stage content in production — draft content is only visible to authenticated requests and should be gated behind preview mode
  • Not handling Rich Text json output properly — use @graphcms/rich-text-react-renderer to render the AST; falling back to html loses embed and asset rendering
  • Ignoring rate limits on the Content API — Hygraph enforces rate limits per second; use caching and ISR to reduce direct API calls

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add cms-services-skills

Get CLI access →