#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:
If you are instrumenting a frontend you did not build from scratch, start by initializing Preview on a page with a single simple field. Confirm the Edit button appears and the save refresh works before adding component attributes. Components require additional data from your GraphQL queries.
#Steps
- Install the Preview SDK.
- Make the SDK available in the browser.
- Set environment variables.
- Add data attributes to your content elements.
- Set up the Preview widget in Studio.
- 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.
There is no framework wrapper component for vanilla JavaScript. The SDK ships a UMD build you serve as a static file and initialize directly with a <script> tag. You can also load the same UMD build from a CDN such as unpkg.
#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.jsimport { 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.jsimport 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 loggingmode: 'iframe', // Optional: 'iframe' | 'standalone' | 'auto'overlayEnabled: true, // Optional: Set to false to disable overlays entirelyoverlay: { // Optional: Customize overlay stylingstyle: {borderColor: '#3b82f6',borderWidth: '2px',},button: {backgroundColor: '#3b82f6',color: 'white',},},sync: {fieldFocus: true, // Optional: Enable field focus sync from StudiofieldUpdate: 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
| Property | Required / Optional | Description |
|---|---|---|
endpoint | Required | Hygraph Content API endpoint. To learn how to retrieve the Content API endpoint, see our docs. |
studioUrl | Optional (recommended) | Studio base URL. Defaults to https://app.hygraph.com. Set this if your Studio runs on a regional or custom domain. |
debug | Optional | Enables verbose console logs to diagnose attribute issues. |
mode | Optional | Forces a specific mode. Options: 'iframe' | 'standalone' | 'auto'. Auto-detection works for most cases. |
overlayEnabled | Optional | Set to false to disable hover overlays and Edit buttons entirely, while keeping the SDK's Studio connection active. Defaults to true. |
overlay | Optional | Customize overlay border and button appearance. |
sync.fieldFocus | Optional | Synchronizes field focus between Studio and the preview when an editor selects a field. |
sync.fieldUpdate | Optional | Updates the preview immediately when field updates happen in Studio. Defaults to false. |
allowedOrigins | Optional | Extends 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:
# .envHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_STUDIO_URL=https://your-region.hygraph.comHYGRAPH_TOKEN=your-permanent-auth-token # Optional: Required if your project uses authentication
// server.jsconst 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.jsconst html = `<main><h1 data-hygraph-field-api-id="title" data-hygraph-entry-id="${recipe.id}">${recipe.title}</h1><divdata-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.
If your server uses a template engine that supports importing functions (for example, EJS or a Node script that imports ES modules), you can use createComponentChainLink, createPreviewAttributes, and withFieldPath from @hygraph/preview-sdk/core instead of building attribute strings by hand. Both approaches produce identical HTML.
#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) {idtitleingredients {id # instanceId for the ingredient componentquantityunit}recipeSteps {id # instanceId for the step componentstepNumberinstruction { 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 {idstepNumberinstruction { html }equipment {id # instanceId for nested equipmentnamerequired}tips {id # instanceId for nested tipstitlecontent { 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.jsapp.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 fieldsconst titleHtml = `<h1 data-hygraph-field-api-id="title" data-hygraph-entry-id="${recipe.id}">${recipe.title}</h1>`;// Basic components: Ingredientsconst 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 Equipmentconst 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 Contentconst 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
- Open an entry in Studio for the model you configured.
- In the right sidebar, click Open live preview. The preview should load alongside the entry form.
- Hover over an element you tagged with
data-hygraph-*attributes. An Edit button should appear. - Click Edit. Studio should scroll to and focus the corresponding field in the entry form.
- 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.
#Related docs
- Click to Edit setup: General setup steps, attribute reference, Studio widget configuration, and troubleshooting.
- Click to Edit - Advanced API: React hooks, Preview methods, DOM events, and helpers for dynamic content.
- Live preview setup: Configure the Studio preview iframe before deploying Click to Edit to editors.