# Click to Edit - Vanilla JavaScript

Add interactive edit buttons to your content previews with the Hygraph Preview SDK. Click any content element to jump directly to it in the Hygraph editor.

This page walks through implementing Click to Edit in a server-rendered vanilla JavaScript project using the [Hygraph Preview SDK](https://github.com/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](https://hygraph.com/docs/developer-guides/schema/click-to-edit). Examples for other supported frameworks are available at:
- [Next.js App Router](https://hygraph.com/docs/developer-guides/schema/click-to-edit-next-js-app-router)
- [Next.js Pages Router](https://hygraph.com/docs/developer-guides/schema/click-to-edit-next-js-pages-router)
- [Remix](https://hygraph.com/docs/developer-guides/schema/click-to-edit-remix)
- [Vue / Nuxt](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vue-nuxt)
- [Advanced API](https://hygraph.com/docs/developer-guides/schema/click-to-edit-advanced-api)

**Note:**
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

1. [Install the Preview SDK](#install-the-preview-sdk).
2. [Make the SDK available in the browser](#make-the-sdk-available-in-the-browser).
3. [Set environment variables](#set-environment-variables).
4. [Add data attributes to your content elements](#add-data-attributes-to-content-elements).
5. [Set up the Preview widget in Studio](https://hygraph.com/docs/developer-guides/schema/click-to-edit#set-up-the-preview-widget-in-studio).
6. [Verify the setup](#verify-the-setup).

## Install the Preview SDK

To install the Preview SDK, run the following command:

```bash
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.

**Note:**
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](https://unpkg.com/@hygraph/preview-sdk/dist/index.umd.js).

### Step 1: Copy the UMD bundle

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

```js
// 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`:

```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:

```js
// server.js
import express from 'express';
import { join } from 'path';

const app = express();
app.use('/js', express.static(join(process.cwd(), 'js')));
```

```html
<!-- 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](#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](https://hygraph.com/docs/developer-guides/project/api-access#endpoints). |
| `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:

```bash
# .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
```

```js
// 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](#step-2-serve-the-bundle-and-initialize-preview) 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](https://hygraph.com/docs/developer-guides/project/api-access#endpoints).

**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](https://hygraph.com/docs/getting-started/access-and-permissions/api-access#authenticated-requests-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](https://hygraph.com/docs/developer-guides/schema/click-to-edit#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](https://github.com/hygraph/preview-sdk/blob/main/examples/project-setup/README.md).

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.

```js

// 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`:

```js
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](#basic-components) below.

**Note:**
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.

  **Ingredients**

```js

// Type: Basic component
// Structure: Recipe → ingredients[] → ingredient fields
// Used for: quantity, unit fields
// Path depth: 1 level (Recipe → Ingredient)
// Chain link: 1

// server.js

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('');
```

  
  **Recipe steps**

```js

// Type: Basic component
// Structure: Recipe → recipeSteps[] → step fields
// Used for: stepNumber, title, instruction, estimatedTime
// Path depth: 1 level (Recipe → Recipe Step)
// Chain link: 1

// server.js

const stepsHtml = recipe.recipeSteps.map((step) => {
  const chain = JSON.stringify([{ fieldApiId: 'recipeSteps', instanceId: step.id }]);

  return `
    <div>
      <span
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="stepNumber"
        data-hygraph-component-chain='${chain}'
      >${step.stepNumber}</span>
      ${step.title ? `<h3
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="title"
        data-hygraph-component-chain='${chain}'
      >${step.title}</h3>` : ''}
      <div
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="instruction"
        data-hygraph-component-chain='${chain}'
        data-hygraph-rich-text-format="html"
      >${step.instruction.html}</div>
    </div>
  `;
}).join('');
```


GraphQL query to retrieve `instanceId` values for basic components:

```graphql
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.

  **Equipment within recipe steps**

```js

// Type: Nested component
// Structure: Recipe → recipeSteps[] → equipment[] → equipment fields
// Used for: name, required, alternatives, notes fields
// Path depth: 2 levels (Recipe → Recipe Step → Equipment)
// Chain links: 2

// server.js

const equipmentHtml = (step.equipment || []).map((equip) => {
  const chain = JSON.stringify([
    { fieldApiId: 'recipeSteps', instanceId: step.id },
    { fieldApiId: 'equipment', instanceId: equip.id },
  ]);

  return `
    <div>
      <span
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="name"
        data-hygraph-component-chain='${chain}'
      >${equip.name}</span>
      ${equip.required ? `<span
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="required"
        data-hygraph-component-chain='${chain}'
      >Required</span>` : ''}
    </div>
  `;
}).join('');
```

  
  **Ingredients used within recipe steps**

```js

// Type: Nested component
// Structure: Recipe → recipeSteps[] → ingredientsUsed[] → ingredient fields
// Used for: ingredientName, preparation, timing, notes fields
// Path depth: 2 levels (Recipe → Recipe Step → Ingredient Reference)
// Chain links: 2

// server.js

const ingredientsUsedHtml = (step.ingredientsUsed || []).map((ingred) => {
  const chain = JSON.stringify([
    { fieldApiId: 'recipeSteps', instanceId: step.id },
    { fieldApiId: 'ingredientsUsed', instanceId: ingred.id },
  ]);

  return `
    <div>
      <span
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="ingredientName"
        data-hygraph-component-chain='${chain}'
      >${ingred.ingredientName}</span>
    </div>
  `;
}).join('');
```

  
  **Tips within recipe steps**

```js

// Type: Nested component
// Structure: Recipe → recipeSteps[] → tips[] → tip fields
// Used for: icon, title, content fields
// Path depth: 2 levels (Recipe → Recipe Step → Tip)
// Chain links: 2

// server.js

const tipsHtml = (step.tips || []).map((tip) => {
  const chain = JSON.stringify([
    { fieldApiId: 'recipeSteps', instanceId: step.id },
    { fieldApiId: 'tips', instanceId: tip.id },
  ]);

  return `
    <div>
      <h5
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="title"
        data-hygraph-component-chain='${chain}'
      >${tip.title}</h5>
      <div
        data-hygraph-entry-id="${recipe.id}"
        data-hygraph-field-api-id="content"
        data-hygraph-component-chain='${chain}'
        data-hygraph-rich-text-format="html"
      >${tip.content.html}</div>
    </div>
  `;
}).join('');
```


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

```graphql
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`).

  **Featured Content**

```js

// Type: Modular component
// Structure: Recipe → featuredContent (ProTip | VideoEmbed | IngredientSpotlight)
// Used for: Single featured content section
// Path depth: 1 level
// Chain link: 1

// server.js

function renderFeaturedContent(recipe) {
  const section = recipe.featuredContent;
  if (!section) return '';

  const chain = JSON.stringify([{ fieldApiId: 'featuredContent', instanceId: section.id }]);

  switch (section.__typename) {
    case 'ProTip':
      return `
        <div>
          <div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="icon" data-hygraph-component-chain='${chain}'>
            ${section.icon || '⭐'}
          </div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="title" data-hygraph-component-chain='${chain}'>
            ${section.tipTitle}
          </h3>
          <div
            data-hygraph-entry-id="${recipe.id}"
            data-hygraph-field-api-id="content"
            data-hygraph-component-chain='${chain}'
            data-hygraph-rich-text-format="html"
          >${section.tipContent.html}</div>
        </div>
      `;

    case 'VideoEmbed':
      return `
        <div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="title" data-hygraph-component-chain='${chain}'>
            ${section.videoTitle}
          </h3>
          <div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="videoUrl" data-hygraph-component-chain='${chain}'>
            <!-- Video embed logic -->
          </div>
        </div>
      `;

    case 'IngredientSpotlight':
      return `
        <div>
          <div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="image" data-hygraph-component-chain='${chain}'>
            ${section.ingredientImage ? `<img src="${section.ingredientImage.url}" alt="${section.ingredient?.name || 'Ingredient'}" width="120" height="120" />` : ''}
          </div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="ingredient" data-hygraph-component-chain='${chain}'>
            Star Ingredient: ${section.ingredient?.name || 'Unknown'}
          </h3>
        </div>
      `;

    default:
      return '';
  }
}
```

  
  **Additional Sections**

```js

// Type: Modular component
// Structure: Recipe → additionalSections[] (ProTip | VideoEmbed | IngredientSpotlight)[]
// Used for: Multiple additional content sections
// Path depth: 1 level
// Chain link: 1

// server.js

const additionalSectionsHtml = (recipe.additionalSections || []).map((section) => {
  const chain = JSON.stringify([{ fieldApiId: 'additionalSections', instanceId: section.id }]);

  switch (section.__typename) {
    case 'ProTip':
      return `
        <div>
          <div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="icon" data-hygraph-component-chain='${chain}'>
            ${section.icon || '💡'}
          </div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="title" data-hygraph-component-chain='${chain}'>
            ${section.tipTitle}
          </h3>
          <div
            data-hygraph-entry-id="${recipe.id}"
            data-hygraph-field-api-id="content"
            data-hygraph-component-chain='${chain}'
            data-hygraph-rich-text-format="html"
          >${section.tipContent.html}</div>
        </div>
      `;

    case 'VideoEmbed':
      return `
        <div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="title" data-hygraph-component-chain='${chain}'>
            ${section.videoTitle}
          </h3>
        </div>
      `;

    case 'IngredientSpotlight':
      return `
        <div>
          <div data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="image" data-hygraph-component-chain='${chain}'>
            ${section.ingredientImage ? `<img src="${section.ingredientImage.url}" alt="${section.ingredient?.name || 'Ingredient'}" width="80" height="80" />` : ''}
          </div>
          <h3 data-hygraph-entry-id="${recipe.id}" data-hygraph-field-api-id="ingredient" data-hygraph-component-chain='${chain}'>
            Featured: ${section.ingredient?.name || 'Unknown'}
          </h3>
        </div>
      `;

    default:
      return '';
  }
}).join('');
```


### Full example

```js

// 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](https://hygraph.com/docs/developer-guides/schema/click-to-edit#troubleshooting).

## Related docs

- [Click to Edit setup](https://hygraph.com/docs/developer-guides/schema/click-to-edit): General setup steps, attribute reference, Studio widget configuration, and troubleshooting.
- [Click to Edit - Advanced API](https://hygraph.com/docs/developer-guides/schema/click-to-edit-advanced-api): Advanced Preview SDK APIs, including live preview updates for scalar lists, component arrays, custom save handling, and DOM events.
- [Live preview setup](https://hygraph.com/docs/developer-guides/schema/live-preview): Configure the Studio preview iframe before deploying Click to Edit to editors.
