# Click to Edit - Vue / Nuxt

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 Vue or Nuxt 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)
- [Vanilla JavaScript](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vanilla-js)
- [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 with the `PreviewWrapper` component and 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. Set up the PreviewWrapper for your framework:
   - [Vue](#vue)
   - [Nuxt](#nuxt)
3. [Add data attributes to your content elements](#add-data-attributes-to-content-elements).
4. [Set up the Preview widget in Studio](https://hygraph.com/docs/developer-guides/schema/click-to-edit#set-up-the-preview-widget-in-studio).
5. [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
```

## Vue

For a client-side Vue 3 app with Vue Router. The PreviewWrapper instantiates the `Preview` class from `@hygraph/preview-sdk/core` inside `onMounted`, and tears it down in `onUnmounted`.

**Note:**
There is no framework-specific React-style component for Vue or Nuxt. The wrapper uses the `Preview` class from `@hygraph/preview-sdk/core` directly, through Vue's Composition API.

Running Nuxt instead of plain Vue? Use the [Nuxt](#nuxt) section below.

### Step 1: Create PreviewWrapper.vue

Create `src/components/PreviewWrapper.vue`:

```vue
<template>
  <div>
    <slot />
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import type { Preview as PreviewInstance } from '@hygraph/preview-sdk/core'

const router = useRouter()
let preview: PreviewInstance | null = null

onMounted(async () => {
  if (!import.meta.env.VITE_HYGRAPH_ENDPOINT) return

  try {
    const { Preview } = await import('@hygraph/preview-sdk/core')

    preview = new Preview({
      endpoint: import.meta.env.VITE_HYGRAPH_ENDPOINT,
      studioUrl: import.meta.env.VITE_HYGRAPH_STUDIO_URL,
      debug: true,  // Optional: Enable console logging
      mode: 'iframe',  // Optional: 'iframe' | 'standalone' | 'auto'
      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 the page on save. The core class has no `onSave` config
    // option; subscribe to the save event instead.
    preview.subscribe('save', {
      callback: () => {
        console.log('Content saved, refreshing...')
        router.go(0)
      },
    })
  } catch (error) {
    console.warn('Could not load Hygraph Preview:', error)
  }
})

onUnmounted(() => {
  if (preview) {
    preview.destroy()
    preview = null
  }
})
</script>
```

### Step 2: Register PreviewWrapper in App.vue

Wrap `<router-view />` with `PreviewWrapper` at the root of your app. If the wrapper is absent, the SDK cannot register page content and Click to Edit will not work.

```vue
<!-- src/App.vue -->
<template>
  <PreviewWrapper>
    <router-view />
  </PreviewWrapper>
</template>

<script setup lang="ts">
import PreviewWrapper from './components/PreviewWrapper.vue'
</script>
```

### Step 3: 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. Vite exposes client-side variables through the `VITE_` prefix.

```bash
# .env.local
VITE_HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
VITE_HYGRAPH_STUDIO_URL=https://your-region.hygraph.com
HYGRAPH_TOKEN=your-permanent-auth-token  # Optional: Required if your project enforces 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](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).

**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. |
| `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, and `preview.destroy()` to clean up the instance when the wrapper unmounts.

For a complete Vue example, see the [Vue Preview SDK example](https://github.com/hygraph/preview-sdk/tree/main/examples/vue-example).

When the wrapper and environment variables are in place, continue with [Add data attributes to content elements](#add-data-attributes-to-content-elements).

## Nuxt

Nuxt uses the same `Preview` class from `@hygraph/preview-sdk/core`. The differences from plain Vue are how you expose environment variables (`NUXT_PUBLIC_*` / `useRuntimeConfig()`), where you register the wrapper (`app.vue`), and how you refresh data after a save (`refreshNuxtData()`).

The `data-hygraph-*` attributes and component-chain helpers are the same as for Vue.

### Step 1: Create PreviewWrapper.vue

Create `components/PreviewWrapper.vue`. Initialize the SDK only on the client inside `onMounted`, and read public runtime config instead of `import.meta.env.VITE_*`.

```vue
<template>
  <div>
    <slot />
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import type { Preview as PreviewInstance } from '@hygraph/preview-sdk/core'

const config = useRuntimeConfig()
let preview: PreviewInstance | null = null

onMounted(async () => {
  const endpoint = config.public.hygraphEndpoint
  if (!endpoint) return

  try {
    const { Preview } = await import('@hygraph/preview-sdk/core')

    preview = new Preview({
      endpoint,
      studioUrl: config.public.hygraphStudioUrl,
      debug: true,  // Optional: Enable console logging
      mode: 'iframe',  // Optional: 'iframe' | 'standalone' | 'auto'
      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
      },
    })

    // Re-fetch Nuxt data on save instead of a full page reload
    preview.subscribe('save', {
      callback: () => {
        console.log('Content saved, refreshing...')
        refreshNuxtData()
      },
    })
  } catch (error) {
    console.warn('Could not load Hygraph Preview:', error)
  }
})

onUnmounted(() => {
  if (preview) {
    preview.destroy()
    preview = null
  }
})
</script>
```

### Step 2: Register PreviewWrapper in app.vue

Wrap `<NuxtPage />` with `PreviewWrapper` in `app.vue`. If the wrapper is absent, the SDK cannot register page content and Click to Edit will not work.

```vue
<!-- app.vue -->
<template>
  <PreviewWrapper>
    <NuxtPage />
  </PreviewWrapper>
</template>
```

Nuxt auto-imports components from `components/`, so you usually do not need a manual import.

### Step 3: Configure runtime config and environment variables

Expose only the values the browser needs through `runtimeConfig.public`. Keep Permanent Auth Tokens server-side if you use them for GraphQL queries.

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-only (optional): used by your GraphQL loaders, not by the Preview SDK
    hygraphToken: process.env.HYGRAPH_TOKEN,
    public: {
      hygraphEndpoint: process.env.NUXT_PUBLIC_HYGRAPH_ENDPOINT,
      hygraphStudioUrl: process.env.NUXT_PUBLIC_HYGRAPH_STUDIO_URL,
    },
  },
})
```

Add the following to `.env` in your project's root directory. If you already have these from live preview setup, map the public endpoint and Studio URL into `NUXT_PUBLIC_*` as shown.

```bash
# .env
NUXT_PUBLIC_HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
NUXT_PUBLIC_HYGRAPH_STUDIO_URL=https://your-region.hygraph.com
HYGRAPH_TOKEN=your-permanent-auth-token  # Optional: server-only; required if your project enforces 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](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. Keep this value out of `runtimeConfig.public`. 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).

When the wrapper and environment variables are in place, continue with [Add data attributes to content elements](#add-data-attributes-to-content-elements).

## 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).

In Vue, read the route id with `useRoute()` from `vue-router` and fetch the entry inside `onMounted`, re-fetching with a `watch` on the route param. In Nuxt, use the same `useRoute()` composable with file-based routes (for example `pages/recipes/[id].vue`) and prefer `useAsyncData` or `useFetch` for the query. Static attribute values can be written directly; dynamic values need Vue's `:` binding shorthand.

```vue
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const recipe = ref(null)

const fetchRecipe = async (id: string) => {
  recipe.value = await getRecipe(id)
}

onMounted(() => {
  const id = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
  if (id) fetchRecipe(id)
})

watch(
  () => route.params.id,
  (newId) => {
    const id = Array.isArray(newId) ? newId[0] : newId
    if (id) fetchRecipe(id)
  }
)
</script>
```

### Simple fields

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

```vue

<!-- src/views/Recipe.vue -->

<template>
  <main v-if="recipe">
    <!-- Title -->
    <h1
      data-hygraph-field-api-id="title"
      :data-hygraph-entry-id="recipe.id"
    >
      {{ recipe.title }}
    </h1>

    <!-- Description -->
    <div
      data-hygraph-field-api-id="description"
      :data-hygraph-entry-id="recipe.id"
      data-hygraph-rich-text-format="html"
      v-html="recipe.description.html"
    ></div>

    <!-- Recipe Meta -->
    <div data-hygraph-field-api-id="prepTime" :data-hygraph-entry-id="recipe.id">
      {{ recipe.prepTime }}
    </div>

    <div data-hygraph-field-api-id="cookTime" :data-hygraph-entry-id="recipe.id">
      {{ recipe.cookTime }}
    </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>

    <!-- Hero Image -->
    <div
      data-hygraph-field-api-id="heroImage"
      :data-hygraph-entry-id="recipe.id"
    >
      <img v-if="recipe.heroImage?.url" :src="recipe.heroImage.url" :alt="recipe.title" />
    </div>
  </main>
</template>
```

### 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`, imported in `<script setup>` and called directly in the template with `v-bind`:

```ts
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](#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.

  **Ingredients**

```vue

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

<!-- src/views/Recipe.vue -->

<template>
  <div v-for="(ingredient, index) in recipe.ingredients" :key="ingredient.id">
    <span>{{ ingredient.ingredient?.name }}</span>
    <span
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'quantity',
          componentChain: [createComponentChainLink('ingredients', ingredient.id)],
        }),
        `ingredients.${index}.quantity`
      )"
    >
      {{ ingredient.quantity }}
    </span>
    <span
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'unit',
          componentChain: [createComponentChainLink('ingredients', ingredient.id)],
        }),
        `ingredients.${index}.unit`
      )"
    >
      {{ ingredient.unit }}
    </span>
  </div>
</template>
```

  
  **Recipe steps**

```vue

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

<!-- src/views/Recipe.vue -->

<template>
  <div v-for="(step, index) in recipe.recipeSteps" :key="step.id">
    <span
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'stepNumber',
          componentChain: [createComponentChainLink('recipeSteps', step.id)],
        }),
        `recipeSteps.${index}.stepNumber`
      )"
    >
      {{ step.stepNumber }}
    </span>
    <h3
      v-if="step.title"
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'title',
          componentChain: [createComponentChainLink('recipeSteps', step.id)],
        }),
        `recipeSteps.${index}.title`
      )"
    >
      {{ step.title }}
    </h3>
    <div
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'instruction',
          componentChain: [createComponentChainLink('recipeSteps', step.id)],
        }),
        `recipeSteps.${index}.instruction`
      )"
      data-hygraph-rich-text-format="html"
      v-html="step.instruction.html"
    ></div>
  </div>
</template>
```


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

```vue

<!-- 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 -->

<!-- src/views/Recipe.vue -->

<template>
  <div
    v-for="(equip, equipIndex) in step.equipment"
    :key="equip.id"
  >
    <span
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'name',
          componentChain: [
            createComponentChainLink('recipeSteps', step.id),
            createComponentChainLink('equipment', equip.id),
          ],
        }),
        `recipeSteps.${index}.equipment.${equipIndex}.name`
      )"
    >
      {{ equip.name }}
    </span>
    <span
      v-if="equip.required"
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'required',
          componentChain: [
            createComponentChainLink('recipeSteps', step.id),
            createComponentChainLink('equipment', equip.id),
          ],
        }),
        `recipeSteps.${index}.equipment.${equipIndex}.required`
      )"
    >
      Required
    </span>
  </div>
</template>
```

  
  **Ingredients used within recipe steps**

```vue

<!-- 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 -->

<!-- src/views/Recipe.vue -->

<template>
  <div
    v-for="(ingred, ingredIndex) in step.ingredientsUsed"
    :key="ingred.id"
  >
    <span
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'ingredientName',
          componentChain: [
            createComponentChainLink('recipeSteps', step.id),
            createComponentChainLink('ingredientsUsed', ingred.id),
          ],
        }),
        `recipeSteps.${index}.ingredientsUsed.${ingredIndex}.ingredientName`
      )"
    >
      {{ ingred.ingredientName }}
    </span>
  </div>
</template>
```

  
  **Tips within recipe steps**

```vue

<!-- 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 -->

<!-- src/views/Recipe.vue -->

<template>
  <div v-for="(tip, tipIndex) in step.tips" :key="tip.id">
    <h5
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'title',
          componentChain: [
            createComponentChainLink('recipeSteps', step.id),
            createComponentChainLink('tips', tip.id),
          ],
        }),
        `recipeSteps.${index}.tips.${tipIndex}.title`
      )"
    >
      {{ tip.title }}
    </h5>
    <div
      v-bind="withFieldPath(
        createPreviewAttributes({
          entryId: recipe.id,
          fieldApiId: 'content',
          componentChain: [
            createComponentChainLink('recipeSteps', step.id),
            createComponentChainLink('tips', tip.id),
          ],
        }),
        `recipeSteps.${index}.tips.${tipIndex}.content`
      )"
      data-hygraph-rich-text-format="html"
      v-html="tip.content.html"
    ></div>
  </div>
</template>
```


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. Use `__typename` to branch and build the chain per type.

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

  **Featured Content**

```vue

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

<!-- src/views/Recipe.vue -->

<template>
  <div v-if="recipe.featuredContent">
    <template v-if="recipe.featuredContent.__typename === 'ProTip'">
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'icon',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.icon'
        )"
      >
        {{ recipe.featuredContent.icon || '⭐' }}
      </div>
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.title'
        )"
      >
        {{ recipe.featuredContent.tipTitle }}
      </h3>
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'content',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.content'
        )"
        data-hygraph-rich-text-format="html"
        v-html="recipe.featuredContent.tipContent.html"
      ></div>
    </template>

    <template v-else-if="recipe.featuredContent.__typename === 'VideoEmbed'">
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.title'
        )"
      >
        {{ recipe.featuredContent.videoTitle }}
      </h3>
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'videoUrl',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.videoUrl'
        )"
      >
        <!-- Video embed logic -->
      </div>
    </template>

    <template v-else-if="recipe.featuredContent.__typename === 'IngredientSpotlight'">
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'image',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.image'
        )"
      >
        <img
          v-if="recipe.featuredContent.ingredientImage"
          :src="recipe.featuredContent.ingredientImage.url"
          :alt="recipe.featuredContent.ingredient?.name || 'Ingredient'"
          width="120"
          height="120"
        />
      </div>
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'ingredient',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.ingredient'
        )"
      >
        Star Ingredient: {{ recipe.featuredContent.ingredient?.name || 'Unknown' }}
      </h3>
    </template>
  </div>
</template>
```

  
  **Additional Sections**

```vue

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

<!-- src/views/Recipe.vue -->

<template>
  <div v-for="(section, index) in recipe.additionalSections" :key="section.id">
    <template v-if="section.__typename === 'ProTip'">
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'icon',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.icon`
        )"
      >
        {{ section.icon || '💡' }}
      </div>
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.title`
        )"
      >
        {{ section.tipTitle }}
      </h3>
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'content',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.content`
        )"
        data-hygraph-rich-text-format="html"
        v-html="section.tipContent.html"
      ></div>
    </template>

    <template v-else-if="section.__typename === 'VideoEmbed'">
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.title`
        )"
      >
        {{ section.videoTitle }}
      </h3>
    </template>

    <template v-else-if="section.__typename === 'IngredientSpotlight'">
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'image',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.image`
        )"
      >
        <img
          v-if="section.ingredientImage"
          :src="section.ingredientImage.url"
          :alt="section.ingredient?.name || 'Ingredient'"
          width="80"
          height="80"
        />
      </div>
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'ingredient',
            componentChain: [createComponentChainLink('additionalSections', section.id)],
          }),
          `additionalSections.${index}.ingredient`
        )"
      >
        Featured: {{ section.ingredient?.name || 'Unknown' }}
      </h3>
    </template>
  </div>
</template>
```


### Full example

```vue

<!-- src/views/Recipe.vue -->

<template>
  <main v-if="recipe">
    <!-- 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"
      v-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="categories" :data-hygraph-entry-id="recipe.id">
      <span v-for="category in recipe.categories" :key="category.id">
        {{ category.name }}
      </span>
    </div>

    <!-- Basic components: Ingredients -->
    <div v-for="(ingredient, index) in recipe.ingredients" :key="ingredient.id">
      <span>{{ ingredient.ingredient?.name }}</span>
      <span
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'quantity',
            componentChain: [createComponentChainLink('ingredients', ingredient.id)],
          }),
          `ingredients.${index}.quantity`
        )"
      >
        {{ ingredient.quantity }}
      </span>
      <span
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'unit',
            componentChain: [createComponentChainLink('ingredients', ingredient.id)],
          }),
          `ingredients.${index}.unit`
        )"
      >
        {{ ingredient.unit }}
      </span>
    </div>

    <!-- Basic components: Recipe Steps -->
    <div v-for="(step, index) in recipe.recipeSteps" :key="step.id">
      <span
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'stepNumber',
            componentChain: [createComponentChainLink('recipeSteps', step.id)],
          }),
          `recipeSteps.${index}.stepNumber`
        )"
      >
        {{ step.stepNumber }}
      </span>
      <h3
        v-if="step.title"
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('recipeSteps', step.id)],
          }),
          `recipeSteps.${index}.title`
        )"
      >
        {{ step.title }}
      </h3>
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'instruction',
            componentChain: [createComponentChainLink('recipeSteps', step.id)],
          }),
          `recipeSteps.${index}.instruction`
        )"
        data-hygraph-rich-text-format="html"
        v-html="step.instruction.html"
      ></div>

      <!-- Nested components: Equipment within this step -->
      <div v-for="(equip, equipIndex) in step.equipment" :key="equip.id">
        <span
          v-bind="withFieldPath(
            createPreviewAttributes({
              entryId: recipe.id,
              fieldApiId: 'name',
              componentChain: [
                createComponentChainLink('recipeSteps', step.id),
                createComponentChainLink('equipment', equip.id),
              ],
            }),
            `recipeSteps.${index}.equipment.${equipIndex}.name`
          )"
        >
          {{ equip.name }}
        </span>
        <span v-if="equip.required">Required</span>
      </div>
    </div>

    <!-- Modular components: Featured Content -->
    <div v-if="recipe.featuredContent && recipe.featuredContent.__typename === 'ProTip'">
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'icon',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.icon'
        )"
      >
        {{ recipe.featuredContent.icon }}
      </div>
      <h3
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'title',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.title'
        )"
      >
        {{ recipe.featuredContent.tipTitle }}
      </h3>
      <div
        v-bind="withFieldPath(
          createPreviewAttributes({
            entryId: recipe.id,
            fieldApiId: 'content',
            componentChain: [createComponentChainLink('featuredContent', recipe.featuredContent.id)],
          }),
          'featuredContent.content'
        )"
        data-hygraph-rich-text-format="html"
        v-html="recipe.featuredContent.tipContent.html"
      ></div>
    </div>
  </main>
</template>

<script setup lang="ts">
import { createComponentChainLink, createPreviewAttributes, withFieldPath } from '@hygraph/preview-sdk/core'
// recipe is fetched via useRoute() + onMounted/watch, see "Add data attributes to content elements" above
</script>
```

## 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. 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.
- [Live preview - Frontend implementation](https://hygraph.com/docs/developer-guides/schema/live-preview-frontend): Serve DRAFT content for Vue, Nuxt, and other frameworks.
