Frequently Asked Questions

Click to Edit for Next.js App Router

What is the Click to Edit feature in Hygraph for Next.js App Router?

The Click to Edit feature allows editors to click any instrumented element in a Next.js App Router preview and jump directly to the corresponding field in Hygraph Studio. This streamlines content editing by connecting frontend elements to backend fields. Note: This feature requires proper instrumentation and configuration as described in the Hygraph documentation. Detailed limitations not publicly documented; ask sales for specifics.

How do you implement Click to Edit in a Next.js App Router project?

To implement Click to Edit, follow these steps: 1) Install the @hygraph/preview-sdk package; 2) Create a PreviewWrapper component to initialize the SDK and wrap your application content; 3) Set environment variables such as NEXT_PUBLIC_HYGRAPH_ENDPOINT and NEXT_PUBLIC_HYGRAPH_STUDIO_URL; 4) Add data-hygraph-* attributes to your content elements; 5) Set up the Preview widget in Studio; 6) Verify the setup by checking for the Edit button and preview refresh. For detailed instructions, see Hygraph documentation. Note: Proper GraphQL queries and component instrumentation are required for full functionality.

What configuration properties are required for the Preview SDK?

The Preview SDK requires the following properties:

For more details, see Hygraph documentation. Note: Some advanced options may require additional setup or permissions.

How do you instrument frontend elements for Click to Edit?

To instrument frontend elements, add data-hygraph-entry-id and data-hygraph-field-api-id attributes to elements rendering Hygraph field values. For component fields, use data-hygraph-component-chain and helper functions from @hygraph/preview-sdk/core. Ensure your GraphQL queries include the id field for each component instance. For full attribute reference and examples, see Hygraph documentation. Note: Missing attributes or incorrect queries may prevent the Edit button from appearing.

How do you verify the Click to Edit setup?

To verify the setup: 1) Open an entry in Studio for the configured model; 2) Click 'Open live preview' in the sidebar; 3) Hover over a tagged element to check for the Edit button; 4) Click Edit to focus the corresponding field in Studio; 5) Edit and save the field to see the preview refresh. If the Edit button does not appear, enable debug={true} in HygraphPreview and check the console for warnings. For troubleshooting, see Hygraph troubleshooting guide. Note: Proper instrumentation and query structure are required for successful verification.

What are the limitations or edge cases of Click to Edit for Next.js App Router?

Click to Edit requires correct instrumentation of frontend elements and proper GraphQL queries for component fields. If attributes are missing or queries do not include required IDs, the Edit button may not appear. Advanced use cases (e.g., deeply nested components, modular content types) may require additional setup. Detailed limitations are not publicly documented; ask sales or support for specifics.

Features & Capabilities

What are Hygraph's key features for developers?

Hygraph offers a GraphQL-native architecture, content federation, high-performance CDN, advanced caching (Smart Edge Cache), localization workflows, AI Assist for content generation and translation, and a marketer-friendly editorial UI. It supports both REST and GraphQL APIs for flexible integration. Note: Teams needing legacy CMS features or non-GraphQL workflows may want to consider alternatives.

Does Hygraph support REST and GraphQL APIs?

Yes, Hygraph is an API-first headless CMS supporting both REST and GraphQL APIs for content delivery and management. Developers can integrate Hygraph with any frontend or application. For documentation, see Hygraph API docs. Note: GraphQL is the primary architecture; REST support is available but may not cover all advanced features.

What integrations are available with Hygraph?

Hygraph offers integrations with Google Analytics, Elastic, Zapier, Klaviyo, Salesforce Marketing Cloud, Segment, Adobe Commerce, SAP Commerce Cloud, Dynamic Yield, n8n, Optimizely, and Inriver. For a full list, visit Hygraph Marketplace Apps. Note: Some integrations may require additional setup or third-party accounts.

Performance & Scalability

How does Hygraph perform under high-traffic scenarios?

Hygraph's global CDN and advanced caching ensure fast and reliable content delivery. For example, Gamescom supported 3.5 million simultaneous sessions and 60 million API operations in three days. Telenor achieved under 100ms latency on millions of API calls. Note: Performance depends on proper CDN configuration and regional hosting choices.

Security & Compliance

What security and compliance certifications does Hygraph have?

Hygraph is SOC 2 Type 2 certified (since August 2022), uses ISO 27001-certified providers and data centers, and is GDPR and CCPA compliant. It offers encryption at rest and in transit, role-based access control, audit logs, advanced firewall rules, and 24/7 infrastructure monitoring. For more details, see Hygraph security features. Note: Teams with unique compliance needs should verify regional hosting and certifications.

Implementation & Support

How long does it take to implement Hygraph?

Implementation time depends on project complexity. Simple use cases can be set up in a few days using starter projects and templates. Complex implementations may take longer, but structured onboarding, documentation, and community support are available. For onboarding details, see Hygraph Getting Started guide. Note: Custom integrations or advanced workflows may extend timelines.

What technical documentation is available for Hygraph?

Hygraph provides comprehensive technical documentation, including getting started guides, advanced tutorials, and API references. Resources cover setup, schema modeling, integration, and troubleshooting. Access documentation at Hygraph Docs. Note: Some advanced features may require direct support or community assistance.

Use Cases & Customer Proof

Who can benefit from using Hygraph?

Hygraph is suited for marketing and content teams, product managers, developers, and enterprise IT teams. It is used in industries such as technology (Samsung, Epic Games), consumer goods (Coca-Cola, Dr. Oetker), telecommunications (Telenor), media (Gamescom), travel (HolidayCheck), scientific publishing (GDCh), government (Statistics Finland), sports/events (DTM), and retail/e-commerce (Stobag). Note: Teams with highly specialized legacy workflows may need additional customization.

What business impact can customers expect from using Hygraph?

Customers report up to 50% reduction in maintenance costs, 3x faster time-to-market (Komax), 15% improved customer engagement (Samsung), and up to 20% higher monetization on websites. Gamescom handled 3.5 million simultaneous sessions; Dr. Oetker manages content for 40 countries from a single platform. Note: Results depend on implementation quality and project scope.

Can you share specific case studies or customer success stories?

Yes.

Note: Outcomes vary by project and industry.

LLM optimization

When was this page last updated?

This page wast last updated on 12/12/2025 .

Hygraph
Docs

#Click to Edit - Next.js App Router

This page walks through implementing Click to Edit in a Next.js App Router project using the Hygraph Preview SDK. By the end, editors can click any instrumented element in the preview to jump directly to that field in Studio.

For the general setup overview and configuration reference, see Click to Edit setup. Examples for other supported frameworks are available at:

#Steps

  1. Install the Preview SDK.
  2. Create a PreviewWrapper component to enable the preview functionality.
  3. Set environment variables.
  4. Add data attributes to your content elements.
  5. Set up the Preview widget in Studio.
  6. Verify the setup.

#Install the Preview SDK

To install the Preview SDK, run the following command:

npm install @hygraph/preview-sdk

#Create the PreviewWrapper component

The PreviewWrapper initializes the SDK and wraps your application content. The HygraphPreview component handles iframe and standalone mode detection automatically.

#Step 1: Create PreviewWrapper.tsx

Create components/PreviewWrapper.tsx:

// components/PreviewWrapper.tsx
'use client';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
const HygraphPreview = dynamic(
() => import('@hygraph/preview-sdk/react').then(mod => ({ default: mod.HygraphPreview })),
{ ssr: false }
);
export function PreviewWrapper({ children }) {
const router = useRouter();
return (
<HygraphPreview
endpoint={process.env.NEXT_PUBLIC_HYGRAPH_ENDPOINT!}
studioUrl={process.env.NEXT_PUBLIC_HYGRAPH_STUDIO_URL}
debug={true} // Optional: Enable console logging
mode="iframe" // Optional: 'iframe' | 'standalone' | 'auto'
onSave={(entryId) => { // Optional: Custom save handler
console.log('Content saved:', entryId);
router.refresh();
}}
overlay={{ // Optional: Customize overlay styling
style: {
borderColor: '#3b82f6',
borderWidth: '2px',
},
button: {
backgroundColor: '#3b82f6',
color: 'white',
},
}}
sync={{
fieldFocus: true, // Optional: Enable field focus sync from Studio
fieldUpdate: false, // Optional: Apply live field updates to Preview
}}
>
{children}
</HygraphPreview>
);
}

#Step 2: Wrap children in layout.tsx

The PreviewWrapper must wrap {children} at the layout level. In Next.js App Router, {children} represents the rendered content of the active route. If the wrapper is absent, the SDK cannot register page content and Click to Edit will not work.

Import and apply the PreviewWrapper component in app/layout.tsx:

// app/layout.tsx
import { PreviewWrapper } from '@/components/PreviewWrapper';
export default function RootLayout({ children }) {
return (
<html>
<body>
<PreviewWrapper>{children}</PreviewWrapper>
</body>
</html>
);
}

Configuration properties

PropertyRequired / OptionalDescription
endpointRequiredHygraph Content API endpoint. To learn how to retrieve the Content API endpoint, see our docs.
studioUrlOptional (recommended)Studio base URL. Defaults to https://app.hygraph.com. Set this if your Studio runs on a regional or custom domain.
debugOptionalEnables verbose console logs to diagnose attribute issues.
modeOptionalForces a specific mode. Options: 'iframe' | 'standalone' | 'auto'. Auto-detection works for most cases.
onSaveOptionalRuns after Hygraph reports a save and receives the entry ID for targeted revalidation.
overlayOptionalCustomize overlay border and button appearance.
sync.fieldFocusOptionalSynchronizes field focus between Studio and the preview when an editor selects a field.
sync.fieldUpdateOptionalUpdates the preview immediately when field updates happen in Studio. Defaults to false.
allowedOriginsOptionalExtends the list of domains that can host your preview iframe. Example: For shared preview environments (QA, staging), add the base URL here.

#Set environment variables

Add the following to .env.local in your project's root directory. If you already have these from live preview setup, skip this step.

# .env.local
NEXT_PUBLIC_HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
NEXT_PUBLIC_HYGRAPH_STUDIO_URL=https://your-region.hygraph.com
HYGRAPH_TOKEN=your-permanent-auth-token # Optional: Required if your project uses authentication

Content API endpoint: Find this under Project Settings > Access > Endpoints > High Performance Content API. For more information, see our documentation on the Content API.

Hygraph Studio base URL: Copy from your browser's address bar in Studio. Example: https://studio-eu-central-1-shared-euc1-02.hygraph.com.

Permanent Auth Token: Create under Project Settings > Access > Permanent Auth Tokens. Set the default content stage to DRAFT. Only required if your project enforces authentication on Content API requests. For more information, see our dedicated documentation on Permanent Auth Tokens.

#Add data attributes to content elements

Data attributes (data-hygraph-*) connect your rendered elements to specific Hygraph fields. The SDK reads these attributes and attaches Edit overlays automatically. The same attributes work for variants; no additional instrumentation is required. For the full attribute reference, see Add data attributes to content elements.

The examples below use a recipe model. To bootstrap the same Hygraph project used here, follow the project setup instructions.

#Simple fields

Add data-hygraph-entry-id and data-hygraph-field-api-id to any element rendering a Hygraph field value.

// app/recipes/[id]/page.tsx
return (
<main>
{/* Title */}
<h1
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="title"
>
{recipe.title}
</h1>
{/* Description */}
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="description"
data-hygraph-rich-text-format="html"
>
<div dangerouslySetInnerHTML={{ __html: recipe.description.html }} />
</div>
{/* Recipe Meta */}
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="prepTime"
>
{recipe.prepTime}
</div>
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="cookTime"
>
{recipe.cookTime}
</div>
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="servings"
>
{recipe.servings}
</div>
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="difficulty"
>
{recipe.difficulty}
</div>
{/* Hero Image */}
<div
data-hygraph-entry-id={recipe.id}
data-hygraph-field-api-id="heroImage"
>
{/* Example image rendering */}
{recipe.heroImage?.url && (
<img src={recipe.heroImage.url} alt={recipe.title} />
)}
</div>
</main>
);

#Component fields

Components require the data-hygraph-component-chain attribute so Studio can navigate to the correct nested field instance. Use the helper functions from @hygraph/preview-sdk/core:

import {
createComponentChainLink,
createPreviewAttributes,
withFieldPath,
} from '@hygraph/preview-sdk/core';

The instanceId in each chain link is the id field of the component instance returned in your GraphQL query. It is not the component type's API ID. Query for id on every component you want to instrument. See the GraphQL example in Basic components below.

#Basic components

These are direct children of the Recipe model, and are not nested inside other components. Each one uses a single-link chain.

GraphQL query to retrieve instanceId values for basic components:

query GetRecipe($id: ID!) {
recipe(where: { id: $id }, stage: DRAFT) {
id
title
ingredients {
id # instanceId for the ingredient component
quantity
unit
}
recipeSteps {
id # instanceId for the step component
stepNumber
instruction { html }
}
}
}

#Nested components

Nested components require a multi-link chain, ordered from the outermost to the innermost component.

GraphQL query to retrieve instanceId values for nested components. Extend your basic component query:

recipeSteps {
id
stepNumber
instruction { html }
equipment {
id # instanceId for nested equipment
name
required
}
tips {
id # instanceId for nested tips
title
content { html }
}
}

#Modular components

Modular components can be one of several types. Use __typename to branch and build the chain per type.

The examples below display aliased GraphQL fields such as section.tipTitle and section.tipContent. Those come from query aliases (tipTitle: title, tipContent: content). fieldApiId must still use the schema API IDs (title, content).

#Full example

// app/recipes/[id]/page.tsx
import { createComponentChainLink, createPreviewAttributes, withFieldPath } from '@hygraph/preview-sdk/core';
// Basic fields
<h1 data-hygraph-field-api-id="title" data-hygraph-entry-id={recipe.id}>
{recipe.title}
</h1>
<div
data-hygraph-field-api-id="description"
data-hygraph-entry-id={recipe.id}
data-hygraph-rich-text-format="html"
>
<div dangerouslySetInnerHTML={{ __html: recipe.description.html }} />
</div>
<div data-hygraph-field-api-id="prepTime" data-hygraph-entry-id={recipe.id}>
{recipe.prepTime}min
</div>
<div data-hygraph-entry-id={recipe.id} data-hygraph-field-api-id="categories">
{recipe.categories.map((category) => (
<span key={category.id}>{category.name}</span>
))}
</div>
// Basic components
// Ingredients
{recipe.ingredients.map((ingredient, index) => {
const chain = [createComponentChainLink('ingredients', ingredient.id)];
const basePath = `ingredients.${index}`;
const quantityAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'quantity',
componentChain: chain,
}),
`${basePath}.quantity`
);
const unitAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'unit',
componentChain: chain,
}),
`${basePath}.unit`
);
return (
<div key={ingredient.id}>
<span>{ingredient.ingredient?.name}</span>
<span {...quantityAttributes}>{ingredient.quantity}</span>
<span {...unitAttributes}>{ingredient.unit}</span>
</div>
);
})}
// Recipe Steps (with nested components)
{recipe.recipeSteps.map((step, index) => {
const chain = [createComponentChainLink('recipeSteps', step.id)];
const stepBasePath = `recipeSteps.${index}`;
const stepNumberAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'stepNumber',
componentChain: chain,
}),
`${stepBasePath}.stepNumber`
);
const stepTitleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: chain,
}),
`${stepBasePath}.title`
);
const instructionAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'instruction',
componentChain: chain,
}),
`${stepBasePath}.instruction`
);
return (
<div key={step.id}>
<span {...stepNumberAttributes}>{step.stepNumber}</span>
{step.title && <h3 {...stepTitleAttributes}>{step.title}</h3>}
<div
dangerouslySetInnerHTML={{ __html: step.instruction.html }}
{...instructionAttributes}
data-hygraph-rich-text-format="html"
/>
{/* Nested: Equipment within Recipe Steps */}
{step.equipment?.map((equip, equipIndex) => {
const equipChain = [
createComponentChainLink('recipeSteps', step.id),
createComponentChainLink('equipment', equip.id),
];
const equipBasePath = `${stepBasePath}.equipment.${equipIndex}`;
const nameAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'name',
componentChain: equipChain,
}),
`${equipBasePath}.name`
);
const requiredAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'required',
componentChain: equipChain,
}),
`${equipBasePath}.required`
);
return (
<div key={equip.id}>
<span {...nameAttributes}>{equip.name}</span>
{equip.required && <span {...requiredAttributes}>Required</span>}
</div>
);
})}
{/* Nested: Ingredients Used within Recipe Steps */}
{step.ingredientsUsed?.map((ingred, ingredIndex) => {
const ingredChain = [
createComponentChainLink('recipeSteps', step.id),
createComponentChainLink('ingredientsUsed', ingred.id),
];
const ingredBasePath = `${stepBasePath}.ingredientsUsed.${ingredIndex}`;
const ingredientNameAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'ingredientName',
componentChain: ingredChain,
}),
`${ingredBasePath}.ingredientName`
);
return (
<div key={ingred.id}>
<span {...ingredientNameAttributes}>{ingred.ingredientName}</span>
</div>
);
})}
{/* Nested: Tips within Recipe Steps */}
{step.tips?.map((tip, tipIndex) => {
const tipChain = [
createComponentChainLink('recipeSteps', step.id),
createComponentChainLink('tips', tip.id),
];
const tipBasePath = `${stepBasePath}.tips.${tipIndex}`;
const titleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: tipChain,
}),
`${tipBasePath}.title`
);
const contentAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'content',
componentChain: tipChain,
}),
`${tipBasePath}.content`
);
return (
<div key={tip.id}>
<h5 {...titleAttributes}>{tip.title}</h5>
<div
dangerouslySetInnerHTML={{ __html: tip.content.html }}
{...contentAttributes}
data-hygraph-rich-text-format="html"
/>
</div>
);
})}
</div>
);
})}
// Modular components
// Featured Content (Single)
{recipe.featuredContent && (() => {
const section = recipe.featuredContent;
const chain = [createComponentChainLink('featuredContent', section.id)];
const basePath = 'featuredContent';
switch (section.__typename) {
case 'ProTip': {
const iconAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'icon',
componentChain: chain,
}),
`${basePath}.icon`
);
const titleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: chain,
}),
`${basePath}.title`
);
const contentAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'content',
componentChain: chain,
}),
`${basePath}.content`
);
return (
<div>
<div {...iconAttributes}>{section.icon}</div>
<h3 {...titleAttributes}>{section.tipTitle}</h3>
<div
dangerouslySetInnerHTML={{ __html: section.tipContent.html }}
{...contentAttributes}
data-hygraph-rich-text-format="html"
/>
</div>
);
}
case 'VideoEmbed': {
const titleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: chain,
}),
`${basePath}.title`
);
const videoUrlAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'videoUrl',
componentChain: chain,
}),
`${basePath}.videoUrl`
);
return (
<div>
<h3 {...titleAttributes}>{section.videoTitle}</h3>
<div {...videoUrlAttributes}>Video</div>
</div>
);
}
case 'IngredientSpotlight': {
const imageAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'image',
componentChain: chain,
}),
`${basePath}.image`
);
const ingredientAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'ingredient',
componentChain: chain,
}),
`${basePath}.ingredient`
);
const descriptionAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'description',
componentChain: chain,
}),
`${basePath}.description`
);
return (
<div>
<div {...imageAttributes}>Image</div>
<h3 {...ingredientAttributes}>{section.ingredient?.name}</h3>
<div
dangerouslySetInnerHTML={{ __html: section.ingredientDescription.html }}
{...descriptionAttributes}
data-hygraph-rich-text-format="html"
/>
</div>
);
}
default:
return null;
}
})()}
// Additional Sections (Array)
{recipe.additionalSections.map((section, index) => {
const chain = [createComponentChainLink('additionalSections', section.id)];
const basePath = `additionalSections.${index}`;
switch (section.__typename) {
case 'ProTip': {
const iconAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'icon',
componentChain: chain,
}),
`${basePath}.icon`
);
const titleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: chain,
}),
`${basePath}.title`
);
const contentAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'content',
componentChain: chain,
}),
`${basePath}.content`
);
return (
<div key={section.id}>
<div {...iconAttributes}>{section.icon}</div>
<h3 {...titleAttributes}>{section.tipTitle}</h3>
<div
dangerouslySetInnerHTML={{ __html: section.tipContent.html }}
{...contentAttributes}
data-hygraph-rich-text-format="html"
/>
</div>
);
}
case 'VideoEmbed': {
const titleAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'title',
componentChain: chain,
}),
`${basePath}.title`
);
return (
<div key={section.id}>
<h3 {...titleAttributes}>{section.videoTitle}</h3>
</div>
);
}
case 'IngredientSpotlight': {
const imageAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'image',
componentChain: chain,
}),
`${basePath}.image`
);
const ingredientAttributes = withFieldPath(
createPreviewAttributes({
entryId: recipe.id,
fieldApiId: 'ingredient',
componentChain: chain,
}),
`${basePath}.ingredient`
);
return (
<div key={section.id}>
<div {...imageAttributes}>Image</div>
<h3 {...ingredientAttributes}>{section.ingredient?.name}</h3>
</div>
);
}
default:
return null;
}
})}

#Verify the setup

  1. Open an entry in Studio for the model you configured.
  2. In the right sidebar, click Open live preview. The preview should load alongside the entry form.
  3. Hover over an element you tagged with data-hygraph-* attributes. An Edit button should appear.
  4. Click Edit. Studio should scroll to and focus the corresponding field in the entry form.
  5. Edit the field value and click Save & Preview. The preview should refresh and show the updated content.

If you are working with variants, no additional setup is required. Clicking a tagged element while a variant is open focuses the field directly in the variant overlay.

If the Edit button does not appear at step 3, add debug={true} to your HygraphPreview component and check the browser console for missing attribute warnings. For component fields, confirm your GraphQL query includes the id field on each component and that the instanceId values in your chain match what the query returns. For more information, see Troubleshooting.