What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Content Architecture
What any site needs for Web Studio: a framable deployment, preview key handling, the SDK script, and data-agility-* attributes on pages, components and fields.
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.
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.
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.
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 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.
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:
Content-Security-Policy: frame-ancestors 'self' https://app.agilitycms.com;
X-Frame-Options. A DENY or SAMEORIGIN value blocks Web Studio. frame-ancestors is the current mechanism and the one to use.Content-Security-Policy header, add the frame-ancestors directive to it.https://unpkg.com, it adds its stylesheet from https://unpkg.com, and its edit icon loads from https://cdn.aglty.io.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.
When an editor previews, Agility opens your deployment URL with query string parameters added. From a page:
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_{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.SameSite=None; Secure, or the browser won't store it there.ContentID to the page that renders that item, using the sitemap. See 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:
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)
}
Add the SDK script to the pages you serve in preview mode:
<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.
@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.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:
<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:
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.The SDK writes the field's new value into the tagged element. In SDK 1.0.26 that covers:
data-agility-html: the element's HTML is replaced.<a> element): the element's HTML is replaced with the new link.<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.
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.
The Next.js 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:
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:
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Content-Security-Policy",
value: "frame-ancestors 'self' https://app.agilitycms.com;",
},
],
},
]
}
This is a pattern, not starter code. As of 2026-10-03, the .NET 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():
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:
builder.Services.AddAntiforgery(options =>
{
options.SuppressXFrameOptionsHeader = true;
});
If you set your own preview cookie, make it usable inside the frame:
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.
@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:
@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>