# Set Up Web Studio with Any Framework

> Source: https://agilitycms.com/docs/developers/web-studio-setup-any-framework

Web Studio loads your site inside Agility so editors can preview, comment and edit in context. It works with any front end that can serve HTML: Agility frames your deployment, and a small script on your site (the Web Studio SDK) reports what is on the page. This guide lists everything a site needs, in framework-neutral terms, with notes for Next.js and an ASP.NET Core Razor pattern at the end.

> [!NOTE]
> Checked on 2026-10-03 against `@agility/web-studio-sdk` 1.0.26 (the source ships in the npm package), the official Next.js starter, and the Agility docs site's own integration. Attribute behavior below describes that SDK version.

## What each step unlocks

Web Studio works in layers. Each one adds to the one before it.

| You add | Editors get |
| --- | --- |
| A registered deployment, and headers that let Agility frame your site | Your site rendered inside Agility, screen-size previews, and commenting |
| The Web Studio SDK script | Web Studio follows navigation inside the frame and tracks scroll position for comments, and comments can be dragged |
| `data-agility-*` attributes on pages, components and fields | Outlines on hover, click-to-edit for components and fields, and live updates as they type |

Without the SDK, Agility shows a notice that the Web Studio SDK is not installed.

## Step 1: Register your deployment

Web Studio opens the URL of a deployment registered against your sitemap. In Agility, go to **Settings > Sitemaps**, click **Setup Deployment**, choose **Custom Deployment**, and enter your site's URL. See [Setting Up Preview](/docs/developers/setting-up-preview) for the full walkthrough, including preview pages for content lists and items.

Web Studio can show both preview and production deployments. For local development, add a preview deployment that points at your local server, for example `http://localhost:3000`.

## Step 2: Let Agility frame your site

Browsers refuse to show a page in a frame when the page's headers forbid it. Editors then see a "refused to connect" or "refused to display" message instead of your site. Send this header:

```http
Content-Security-Policy: frame-ancestors 'self' https://app.agilitycms.com;
```

- **Replace `X-Frame-Options`.** A `DENY` or `SAMEORIGIN` value blocks Web Studio. `frame-ancestors` is the current mechanism and the one to use.
- **Merge, don't duplicate.** If you already send a `Content-Security-Policy` header, add the `frame-ancestors` directive to it.
- **Cover every deployment editors open in Web Studio**, production included, not only your preview deployment.
- **Allow the SDK's own files** if your policy restricts scripts, styles or images on preview pages. The SDK script is served from `https://unpkg.com`, it adds its stylesheet from `https://unpkg.com`, and its edit icon loads from `https://cdn.aglty.io`.
- **Serve HTTPS.** Agility runs on `https://app.agilitycms.com`, and browsers block an `http://` page inside an HTTPS page as mixed content. `http://localhost` is the exception, because browsers treat localhost as a trusted origin.

Web Studio checks your Content Security Policy when it loads your site and explains what to change if the policy blocks it.

## Step 3: Handle the preview key

When an editor previews, Agility opens your deployment URL with query string parameters added. From a page:

```text
https://your-site.com/about?agilitypreviewkey={key}&lang=en-us&agilityts={timestamp}
```

From a content item, Agility uses the item's preview page and adds the item ID (the parameter is `ContentID` unless you named it differently in the list's Developer Settings).

Your site needs to do four things with that request:

1. **Validate the key.** The key is the Base64-encoded SHA-512 hash of the string `-1_{securityKey}_Preview`, encoded as UTF-16LE, where `{securityKey}` is your instance's security key. The `+` characters in the key can arrive as spaces after URL decoding, so turn spaces back into `+` before you compare.
2. **Switch to preview content.** Read content with your preview API key so editors see saved, unpublished changes.
3. **Remember preview mode** while the editor clicks around inside the frame. Most sites set a cookie. Inside Web Studio your site runs in a cross-site frame, so the cookie must be `SameSite=None; Secure`, or the browser won't store it there.
4. **Resolve `ContentID`** to the page that renders that item, using the sitemap. See [Setting Up Preview](/docs/developers/setting-up-preview).

The Agility SDKs do the key check for you: `validatePreview` in `@agility/nextjs`, and `PreviewHelpers.GenerateAgilityPreviewKey(securityKey)` in the `Agility.NET.FetchAPI` package. In any other Node.js stack, this is the same check:

```js
import { createHash, timingSafeEqual } from "node:crypto"

export function isValidPreviewKey(incomingKey, securityKey) {
  if (!incomingKey || !securityKey) return false
  const expected = createHash("sha512")
    .update(Buffer.from(`-1_${securityKey}_Preview`, "utf16le"))
    .digest("base64")
  const received = incomingKey.split(" ").join("+")
  const a = Buffer.from(received)
  const b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}
```

> [!WARNING]
> Make sure a preview request reaches your code. If a CDN or edge cache answers the request with cached, published HTML first, the key is never checked and Web Studio quietly shows published content. The Next.js hosting guides for [Vercel](/docs/nextjs/deploying-next-js-to-vercel) and [Netlify](/docs/nextjs/deploying-next-js-to-netlify) show the fix for those platforms.

## Step 4: Load the SDK in preview

Add the SDK script to the pages you serve in preview mode:

```html
<script src="https://unpkg.com/@agility/web-studio-sdk@latest/dist/index.js"></script>
```

It is also published on npm as `@agility/web-studio-sdk` if you would rather bundle it.

- **The SDK only acts inside a frame.** In a normal browser tab it does nothing.
- **Load it only in preview** if you can. That keeps a third-party script off your public pages. The trade-off: when an editor views a production deployment in Web Studio without the SDK, Web Studio can't follow navigation there. Both official starters currently load the script on every page, which also works.
- **`@latest` updates itself.** unpkg serves the newest published release, so you get fixes without redeploying. The SDK loads its stylesheet from `@latest` on unpkg whichever way you load the script.

## Step 5: Tag your markup

The SDK finds pages, components and fields through `data-agility-*` attributes on the HTML you render.

| Attribute | Put it on | Value | What it does |
| --- | --- | --- | --- |
| `data-agility-guid` | `<body>` | Your instance GUID | Identifies your instance. The SDK ignores messages from Agility for any other GUID, so without it nothing is editable. |
| `data-agility-page` | The element that wraps the page content | The page ID (a number) | Tells Web Studio which page is showing, so it can follow navigation. |
| `data-agility-dynamic-content` | The same page wrapper, on dynamic pages | The content ID of the item the page renders | Tells Web Studio which item a dynamic page is showing. |
| `data-agility-component` | The outermost element of each component | The component's content ID | Outlines the component and adds its edit button. Fields inside it belong to it. |
| `data-agility-field` | The element that renders one field | The field name from the model | Adds the field's edit button and is where live updates are applied. |
| `data-agility-html` | A field element that renders HTML | `true` | Live updates are inserted as HTML instead of plain text. |
| `data-agility-nested-listitem` | The outermost element of each item rendered from a linked content list | That item's content ID | Makes each list item editable on its own, with its own fields. |
| `data-agility-previewbar` | Your own preview bar, if you have one | `true` | Hides your preview bar inside Web Studio. The value must be `"true"`. |

Here is a page with one component, a rich text field and a list of linked items:

```html
<body data-agility-guid="YOUR_INSTANCE_GUID">
  <div data-agility-previewbar="true"><!-- your preview bar --></div>

  <main data-agility-page="12" data-agility-dynamic-content="345">
    <section data-agility-component="678">
      <h1 data-agility-field="title">Our services</h1>
      <div data-agility-field="textblob" data-agility-html="true">
        <p>Rich text from the CMS</p>
      </div>

      <ul>
        <li data-agility-nested-listitem="901">
          <h3 data-agility-field="heading">Consulting</h3>
        </li>
      </ul>
    </section>
  </main>

  <script src="https://unpkg.com/@agility/web-studio-sdk@latest/dist/index.js"></script>
</body>
```

A few rules keep the tagging accurate:

- **Use the field name from the content or component model.** Live updates match it without regard to case, but keep it identical to the model so it stays readable.
- **Tag a component's own fields only.** Each field belongs to the nearest `data-agility-component` or `data-agility-nested-listitem` around it. Items from a linked list are separate content items, so tag them with `data-agility-nested-listitem` rather than letting their fields fall into the parent component.
- **Client-side routing is fine.** The SDK watches the page for changes to these attributes and reports the new page when they change.
- **Next.js sites can tag automatically.** [Agility Decorate](/docs/developers/agility-decorate) adds these attributes to an existing Next.js codebase.

### What updates live as you type

The SDK writes the field's new value into the tagged element. In SDK 1.0.26 that covers:

- **Text fields:** the element's text is replaced.
- **HTML fields** with `data-agility-html`: the element's HTML is replaced.
- **Values that arrive as a complete link** (an `<a>` element): the element's HTML is replaced with the new link.
- **Image fields:** the first `<img>` inside the field element (and any `<source>` in a `<picture>`) gets the new image, keeping your existing query string such as width or format.

Other values are not applied live. Web Studio marks the fields that cannot update live, and those changes show after the editor saves.

Because the raw value is written straight in, tag the element whose content is exactly the field's value. If your code formats a value first (a formatted date, or Markdown converted to HTML), the live preview shows the unformatted value until the editor saves.

## Step 6: Test it

1. Open a page in Agility and click **Preview** to open it in Web Studio.
2. Check that your site loads in the frame and the "SDK not installed" notice is gone.
3. Hover over a component: it should be outlined, with edit buttons on its fields.
4. Click a field, type a change, and watch the preview update.

If something doesn't work, open your browser's developer tools on the framed page. The SDK writes its messages to the console starting with `Web Studio SDK`. Then see [Troubleshooting Web Studio](/docs/developers/troubleshooting-web-studio).

## Next.js

The [Next.js starter](/docs/nextjs/using-the-next-js-blog-starter) comes with most of this done: the GUID on `<body>`, the SDK script, the page wrapper attributes, and tagged components and fields. These guides cover the Next.js side:

- [How the Next.js Starter Works](/docs/nextjs/how-the-next-js-starter-works): the page wrapper and component structure.
- [Preview URL Lifecycle](/docs/nextjs/preview-url-lifecycle): how a preview link becomes draft mode, and why caching can hide it.
- [Agility Decorate](/docs/developers/agility-decorate): tag an existing component library automatically.

The starter sends no Content Security Policy, so nothing stops Agility framing it. If you add a policy, include `frame-ancestors` in `next.config.js`:

```js
async headers() {
  return [
    {
      source: "/:path*",
      headers: [
        {
          key: "Content-Security-Policy",
          value: "frame-ancestors 'self' https://app.agilitycms.com;",
        },
      ],
    },
  ]
}
```

## ASP.NET Core (Razor)

> [!IMPORTANT]
> **This is a pattern, not starter code.** As of 2026-10-03, the [.NET starter](https://github.com/agility/agilitycms-dotnet-starter) handles preview mode (the MVC and Blazor versions both validate the preview key and keep a preview cookie), but neither ships complete Web Studio wiring. The MVC version loads the SDK script and has a placeholder for the GUID on `<body>`, but its page ID attribute is unfinished and its components and fields are not tagged. The Blazor version has no Web Studio wiring. The examples below use only the attributes documented above. Adapt the model and property names to your project.

**Allow framing.** In `Program.cs`, before `app.Run()`:

```csharp
app.Use(async (context, next) =>
{
    context.Response.Headers["Content-Security-Policy"] =
        "frame-ancestors 'self' https://app.agilitycms.com;";
    await next();
});
```

ASP.NET Core antiforgery adds `X-Frame-Options: SAMEORIGIN` to responses that render antiforgery tokens, such as pages with forms. Turn that off so it can't block Web Studio:

```csharp
builder.Services.AddAntiforgery(options =>
{
    options.SuppressXFrameOptionsHeader = true;
});
```

If you set your own preview cookie, make it usable inside the frame:

```csharp
context.Response.Cookies.Append("agility-preview", "true", new CookieOptions
{
    Path = "/",
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.None
});
```

**Layout and page wrapper.** In Razor, write `@@` to output a literal `@` in the unpkg URL. An attribute whose value is `null` is left out of the HTML, which suits `data-agility-dynamic-content` on pages that aren't dynamic.

```cshtml
@inject Microsoft.Extensions.Options.IOptions<AppSettings> Settings
@{
    // Your own preview check, for example the MVC starter's PreviewHelpers.IsPreviewMode(Context)
    var isPreview = (bool)(Context.Items["IsPreview"] ?? false);
    var dynamicContentId = Model.SitemapPage?.ContentID > 0 ? Model.SitemapPage.ContentID : (int?)null;
}
<body data-agility-guid="@Settings.Value.InstanceGUID">
    <main data-agility-page="@Model.SitemapPage?.PageID"
          data-agility-dynamic-content="@dynamicContentId">
        @await RenderSectionAsync("MainContentZone")
    </main>

    @if (isPreview)
    {
        <script src="https://unpkg.com/@@agility/web-studio-sdk@latest/dist/index.js"></script>
    }
</body>
```

**A component view.** Put the component's content ID on its outer element and the field name on each field:

```cshtml
@model ContentItemResponse<Agility.Models.RichTextArea>

<section data-agility-component="@Model?.ContentID">
    <div data-agility-field="textblob" data-agility-html="true">
        @Html.Raw(Model?.Fields?.TextBlob)
    </div>
</section>
```

## Related

- [Getting Started With Web Studio](/docs/overview/web-studio)
- [Previewing in Web Studio](/docs/editors/previewing-in-web-studio) (for editors)
- [Troubleshooting Web Studio](/docs/developers/troubleshooting-web-studio)
- [Setting Up Preview](/docs/developers/setting-up-preview)
- [Why We Recommend Next.js and .NET](/docs/developers/why-we-recommend-nextjs-and-dotnet)
- [Web Studio SDK on GitHub](https://github.com/agility/web-studio-sdk)
