Hygraph
Build with Hygraph (formerly GraphCMS) as a GraphQL-native headless CMS.
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 linesHygraph — 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: PUBLISHEDargument in queries for production and omit it (or useDRAFT) 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 theConnectionaggregate) 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-requestcalls with Next.jsfetchandnext.tagsfor 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
DRAFTstage content in production — draft content is only visible to authenticated requests and should be gated behind preview mode - Not handling Rich Text
jsonoutput properly — use@graphcms/rich-text-react-rendererto render the AST; falling back tohtmlloses 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
Related Skills
Builder Io
Builder.io is a Visual Headless CMS and API that empowers developers to integrate
Caisy
caisy is a headless CMS designed for speed and scalability, empowering developers
Contentful
Build with Contentful for headless content management. Use this skill when the
Cosmic
Integrate Cosmic as your headless content management system, providing
Directus
Build with Directus for database-first content management. Use this skill
Ghost
Integrate Ghost as a powerful headless CMS or a full-featured publishing platform.