Hygraph
Docs

#Click to Edit - Vanilla JavaScript

This page walks through implementing Click to Edit in a server-rendered vanilla JavaScript 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. Make the SDK available in the browser.
  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

#Make the SDK available in the browser

Without a bundler, the browser cannot resolve import '@hygraph/preview-sdk'. Copy the package's prebuilt UMD bundle into a static directory your server exposes, and load it with a plain <script> tag.

#Step 1: Copy the UMD bundle

Add a setup script that copies the bundle from node_modules into a folder your server serves statically:

// setup.js
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
async function setup() {
const jsDir = join(__dirname, 'js');
await fs.mkdir(jsDir, { recursive: true });
const srcPath = join(__dirname, 'node_modules', '@hygraph', 'preview-sdk', 'dist', 'index.umd.js');
const destPath = join(jsDir, 'preview-sdk.js');
await fs.copyFile(srcPath, destPath);
console.log('Preview SDK UMD bundle copied to js/preview-sdk.js');
}
setup();

Run it after every npm install, for example with a postinstall or predev script in package.json:

{
"scripts": {
"setup": "node setup.js",
"dev": "npm run setup && node server.js"
}
}

#Step 2: Serve the bundle and initialize Preview

Serve the js/ directory as static files, then include the bundle and initialize Preview in the page you render:

// server.js
import express from 'express';
import { join } from 'path';
const app = express();
app.use('/js', express.static(join(process.cwd(), 'js')));
<!-- rendered page -->
<script src="/js/preview-sdk.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.HygraphPreviewSDK && window.HygraphPreviewSDK.Preview) {
const preview = new window.HygraphPreviewSDK.Preview({
endpoint: '__HYGRAPH_ENDPOINT__',
studioUrl: '__HYGRAPH_STUDIO_URL__',
debug: true, // Optional: Enable console logging
mode: 'iframe', // Optional: 'iframe' | 'standalone' | 'auto'
overlayEnabled: true, // Optional: Set to false to disable overlays entirely
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
},
});
// Refresh on save. The core class has no `onSave` constructor
// option; subscribe to the save event instead.
preview.subscribe('save', {
callback: () => {
console.log('Content saved, refreshing...');
window.location.reload();
},
});
window.addEventListener('beforeunload', () => {
preview.destroy();
});
console.log('Live Preview initialized', preview.getVersion());
} else {
console.error('Live Preview not loaded');
}
});
</script>

Replace __HYGRAPH_ENDPOINT__ and __HYGRAPH_STUDIO_URL__ with values interpolated from your server-side configuration, as shown in Set environment variables below. If the script tag or the Preview initialization is missing from a page, the SDK cannot register that page's content and Click to Edit will not work on it.

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.
overlayEnabledOptionalSet to false to disable hover overlays and Edit buttons entirely, while keeping the SDK's Studio connection active. Defaults to true.
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.

The core Preview class has no onSave constructor option. Use preview.subscribe('save', { callback }) to react to saves.

#Set environment variables

The server reads configuration from Node's process.env when it renders each page. Set the following on your host, or in a .env file loaded by your process manager:

# .env
HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
HYGRAPH_STUDIO_URL=https://your-region.hygraph.com
HYGRAPH_TOKEN=your-permanent-auth-token # Optional: Required if your project uses authentication
// server.js
const CONFIG = {
HYGRAPH_ENDPOINT: process.env.HYGRAPH_ENDPOINT,
HYGRAPH_STUDIO_URL: process.env.HYGRAPH_STUDIO_URL,
HYGRAPH_TOKEN: process.env.HYGRAPH_TOKEN || '',
};

Interpolate CONFIG.HYGRAPH_ENDPOINT and CONFIG.HYGRAPH_STUDIO_URL into the inline <script> that initializes Preview, as shown in Step 2 above.

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.

A server without a templating engine has no JSX or component-chain helper calls available at render time. Build the data-hygraph-* attributes as plain strings inside your HTML template literals, and encode data-hygraph-component-chain as a JSON string with JSON.stringify.

#Simple fields

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

// server.js
const html = `
<main>
<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"
>
${recipe.description.html}
</div>
<div data-hygraph-field-api-id="prepTime" data-hygraph-entry-id="${recipe.id}">
${recipe.prepTime}min
</div>
<div data-hygraph-field-api-id="cookTime" data-hygraph-entry-id="${recipe.id}">
${recipe.cookTime}min
</div>
<div data-hygraph-field-api-id="servings" data-hygraph-entry-id="${recipe.id}">
${recipe.servings}
</div>
<div data-hygraph-field-api-id="difficulty" data-hygraph-entry-id="${recipe.id}">
${recipe.difficulty}
</div>
<div data-hygraph-field-api-id="heroImage" data-hygraph-entry-id="${recipe.id}">
${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. Without helper functions, build the chain as a plain array and encode it with JSON.stringify:

const componentChain = JSON.stringify([
{ fieldApiId: 'ingredients', instanceId: ingredient.id },
]);

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. Branch on __typename 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). data-hygraph-field-api-id must still use the schema API IDs (title, content).

#Full example

// server.js
app.get('/recipes/:id', async (req, res) => {
const { id } = req.params;
const data = await graphqlRequest(GET_RECIPE_BY_ID_QUERY, { id });
const recipe = data.recipe;
if (!recipe) {
return res.status(404).send('Recipe not found');
}
// Basic fields
const titleHtml = `<h1 data-hygraph-field-api-id="title" data-hygraph-entry-id="${recipe.id}">${recipe.title}</h1>`;
// Basic components: Ingredients
const ingredientsHtml = recipe.ingredients.map((ingredient) => {
const chain = JSON.stringify([{ fieldApiId: 'ingredients', instanceId: ingredient.id }]);
return `
<div>
<span>${ingredient.ingredient?.name ?? ''}</span>
<span data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="quantity" data-hygraph-component-chain='${chain}'>${ingredient.quantity}</span>
<span data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="unit" data-hygraph-component-chain='${chain}'>${ingredient.unit}</span>
</div>
`;
}).join('');
// Basic components + nested components: Recipe Steps with Equipment
const stepsHtml = recipe.recipeSteps.map((step) => {
const stepChain = JSON.stringify([{ fieldApiId: 'recipeSteps', instanceId: step.id }]);
const equipmentHtml = (step.equipment || []).map((equip) => {
const equipChain = JSON.stringify([
{ fieldApiId: 'recipeSteps', instanceId: step.id },
{ fieldApiId: 'equipment', instanceId: equip.id },
]);
return `<span data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="name" data-hygraph-component-chain='${equipChain}'>${equip.name}</span>`;
}).join('');
return `
<div>
<span data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="stepNumber" data-hygraph-component-chain='${stepChain}'>${step.stepNumber}</span>
<div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="instruction" data-hygraph-component-chain='${stepChain}' data-hygraph-rich-text-format="html">${step.instruction.html}</div>
${equipmentHtml}
</div>
`;
}).join('');
// Modular component: Featured Content
const featuredContentHtml = renderFeaturedContent(recipe);
const html = `
<!DOCTYPE html>
<html lang="en">
<head><title>${recipe.title}</title></head>
<body>
<main>
${titleHtml}
${ingredientsHtml}
${stepsHtml}
${featuredContentHtml}
</main>
<script src="/js/preview-sdk.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.HygraphPreviewSDK && window.HygraphPreviewSDK.Preview) {
const preview = new window.HygraphPreviewSDK.Preview({
endpoint: '${CONFIG.HYGRAPH_ENDPOINT}',
studioUrl: '${CONFIG.HYGRAPH_STUDIO_URL}',
debug: true,
});
preview.subscribe('save', { callback: () => window.location.reload() });
window.addEventListener('beforeunload', () => preview.destroy());
}
});
</script>
</body>
</html>
`;
res.send(html);
});

#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, set debug: true on the Preview instance and check the browser console for missing attribute warnings. Also confirm js/preview-sdk.js loaded successfully in the Network tab. 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.