Sitecore Pages Edit Mode without an official SDK: The Handshake, in Astro
Sitecore Pages edit mode is a handshake any server-rendered app can implement: two endpoints, a shared secret, CORS and CSP headers, and one preview data call. We build it by hand in Astro so you can see every piece, and point to the shortcuts if you just want it working.
Sohrab is a senior front-end developer specializing in React, Next.js, and Sitecore Headless—building lightning-fast, scalable, and seamless digital experiences.
Edit mode is a contract, not a framework feature
Making edit mode in Sitecore Pages actually work is one of the hardest parts of a headless Sitecore project, and this post is for the case where your framework has no official Sitecore package. Sitecore ships first-party Content SDK packages for Next.js and, since September 2026, Angular (Content SDK for Angular 1.0); those hide the edit-mode wiring behind a route handler or two. Everything else gets the raw contract, and almost none of it is framework-specific: the handshake between Sitecore Pages and your app is the same on Next.js, Astro, or anything else that serves HTML per request. Only the syntax changes. We walk through what Pages sends, what your app has to answer, and what the editing secret is for, with Astro for the working code because Astro is what we built.
If you just want edit mode working in Astro, you have two shortcuts and don't need to read further. Our open-source starter contains all of the code below. And EXDST maintains a community Astro adapter for the Content SDK (@exdst-sitecore-content-sdk/astro), built on the same core package, with Pages metadata editing wired in. It isn't a Sitecore product, so weigh its maintenance the way you would any third-party dependency. This post is for understanding what either of those is doing on your behalf.
What "edit mode" actually means
When a content author clicks Edit in Sitecore Pages, the editor doesn't hit your production site directly. The flow, as Sitecore's editing architecture page describes it, comes down to four moves (Sitecore: editing architecture):
Sitecore Pages calls your /api/editing/config endpoint to confirm your app supports metadata editing and to learn which components it exposes.
Sitecore Pages calls your /api/editing/render endpoint with the route and item it wants to edit.
Your endpoint renders the page in edit mode and returns the full HTML document.
Sitecore Pages loads that HTML in an iframe and overlays its editing chrome on top.
Everything else, sc_mode=edit, sc_itemid, the editing secret, is plumbing that supports those four steps.
Notice that none of the four steps mention a framework. Any app that can expose two HTTP endpoints and render a page server-side can do this. That's the whole contract.
There are two things that make it harder than it looks:
Edit mode is per-request. Static output won't work. You need server rendering.
Sitecore Pages runs on a different origin. You're rendering inside its iframe, so CORS, CSP, and a shared secret all matter.
Any server-rendering framework can meet both requirements. In Astro that means output: 'server' with an adapter, so pages render on demand (Astro on-demand rendering). Here's how the rest looks.
The editing secret
Every editing request from Sitecore Pages carries a secret query parameter. Your endpoint compares it to a server-side environment variable, conventionally named SITECORE_EDITING_SECRET (Sitecore: editing architecture):
SITECORE_EDITING_SECRET=<random-token>
The same value goes into your SitecoreAI environment, under Deploy, your project, the authoring environment, Developer settings, Environment variables (Sitecore: enable visual editing). This is the only real authentication on these endpoints. An Origin header can be set by any HTTP client, so the CORS check alone proves nothing about who is calling /api/editing/render. Treat it like a webhook secret: generate it once, store it in env vars on both sides, never commit it.
One more setting matters here. The server-side Edge context ID your app uses must be the Preview context ID, not the Live one, because visual editing always reads draft content (Sitecore: enable visual editing). If your editor shows only published content no matter what you do, check this before anything else.
Endpoint 1: /api/editing/config
Sitecore Pages calls this first, with GET and OPTIONS, to find out what your app supports: the edit mode, the list of components, and the package versions it's running (Sitecore: editing architecture). This is the GET handler from our starter; the OPTIONS handler runs the same origin check and returns 204:
Astro maps exported functions named after HTTP methods to requests, so GET and OPTIONS are just two exports in one file (Astro endpoints).
Two guardrails matter here.
CORS allowlist. Only Sitecore's editing hosts should be able to call this endpoint cross-origin. We use the same list the Content SDK ships as EDITING_ALLOWED_ORIGINS: pages.sitecorecloud.io, xmapps.sitecorecloud.io, and designlibrary.sitecorecloud.io (Sitecore/content-sdk on GitHub). Sitecore's docs give pages.sitecorecloud.io and app.sitecorecloud.io as examples (Sitecore: editing architecture), so if your editor reports an invalid origin, compare the host in the error with your list before anything else.
Secret check. A request without a secret query parameter, or with the wrong one, gets a 401.
We allowlist the origins explicitly rather than echoing whatever the client sent. That's the difference between safe CORS and a wide-open endpoint.
Endpoint 2: /api/editing/render
This is the workhorse. Sitecore Pages sends route, sc_site, sc_itemid, sc_lang, mode, and optionally sc_version, sc_variant, and sc_layoutKind (Sitecore: editing architecture). Your endpoint renders the page in edit mode and returns HTML.
The simplest correct implementation: proxy back to your own catch-all route with the right query parameters. This is the starter's handler with its error responses trimmed:
The Content-Security-Policy: frame-ancestors header. It lists the parents allowed to embed your page in an iframe (MDN: frame-ancestors). Browsers allow framing when no such header is present, so this line is not what makes the iframe work.
Why it still matters. Most production setups send X-Frame-Options or a restrictive frame-ancestors, and the moment one of those reaches the editing response, Sitecore Pages shows an empty frame with only a "refused to display" line in the console. Setting frame-ancestors on the editing response to exactly your CORS allowlist, which is Sitecore's guidance (Sitecore: editing architecture), lets Pages in and keeps everyone else out. The Content SDK's Next.js render handler sets the same header.
Cookie forwarding. The iframe request arrives from the author's browser, so the internal call to your own catch-all should carry the same cookies. Otherwise anything that depends on them, such as a protected preview deployment or a site-resolution cookie, behaves differently inside the editor than outside it. The Content SDK's Next.js render handler propagates the incoming request headers to its internal render call for the same reason.
The catch-all route, with edit-mode awareness
Back in [...path].astro we already had the published-page flow. Adding edit mode is one branch. Trimmed from the starter, which also accepts the SDK's alternate parameter names and logs the fallback:
client.getPreview() is doing real work: it asks Sitecore Edge for the editing layout of that exact item, language, and version, with the requested personalization variants applied, and returns it in the shape Pages needs to overlay its chrome. client.getPage() resolves by route and returns the plain layout, which is right for visitors and wrong for the editor.
The fallback is intentional. If getPreview fails, because of an expired token, a wrong item ID, or a transient error, the editor still gets something useful, the last published version, instead of a 500.
Injecting the Pages chrome with EditingScripts
Sitecore Pages adds an editing overlay on top of your rendered HTML: selectable fields, the components panel, drag-and-drop. For that to attach, your page must include the script URLs and the JSON client data that came back with the editing layout, plus a marker that tells Pages your app supports metadata editing (Sitecore: editing architecture). The Content SDK's getContentSdkPagesClientData() supplies that marker: an empty JSON script tag with the id jss-hrz-editing, which Pages looks for before activating.
is:inline. It stops Astro from bundling or hoisting the script tag, so the tag passes through to the browser as written (Astro template directives).
Escaping <. Replacing < in the JSON script tags prevents field content from breaking out of the tag and injecting HTML.
Don't cache edit-mode responses
The published-page branch sets a CDN-friendly cache header. Edit mode skips it:
if (!isEditMode) {
Astro.response.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
}
If you forget this, content authors will publish a change, hit refresh in Pages, and see the previous draft for the next minute. Subtle, infuriating bug.
A mental model for the whole flow
Author clicks Editin Sitecore Pages
│
▼
Sitecore Pages → GET /api/editing/config (with secret)
│ (confirms metadata edit mode, discovers your components)
▼
Sitecore Pages → GET /api/editing/render?route=/...&sc_itemid=...&secret=...
│
▼
Your /api/editing/render handler
│ - checks secret + CORS
│ - rebuilds target URL with sc_mode=edit + sc_itemid
│ - fetches your own [...path].astro
▼
[...path].astro
│ - sees sc_mode=edit
│ - calls client.getPreview() (not getPage)
│ - renders placeholders +
▼
HTML response → /api/editing/render → Sitecore Pages iframe
│
▼
Pages overlays editing chrome on the rendered HTML
Once you have this picture, every piece has a job: the secret authenticates the request, CORS scopes it to Sitecore origins, the CSP allows iframing, getPreview gets unpublished content, EditingScripts lets Pages overlay its chrome.
Common things that go wrong
Symptom
Likely cause
Editor shows blank iframe
An X-Frame-Options or restrictive frame-ancestors header is reaching the editing response; set frame-ancestors to Sitecore's hosts
Editor shows "Invalid origin"
CORS allowlist is missing the Sitecore Pages host
Editor shows published content, not draft
The server-side context ID is the Live one instead of Preview
Page renders but nothing is editable
client.getPage is being called instead of client.getPreview, so the layout has no editing metadata
Editor overlay doesn't activate
EditingScripts not rendered in edit mode (or is:inline missing)
Random 401s
SITECORE_EDITING_SECRET differs between your app and the Sitecore environment
Wrap-up
Sitecore Pages edit mode looks complicated because it has so many moving parts: secret, CORS, CSP, two endpoints, two SDK methods. But each piece is small, and none of it is tied to a particular framework. Two API routes and one edit-mode branch in your page rendering is the entire surface, whatever stack you're on. That is why a framework without an official Sitecore package is not locked out of visual editing; it just has to write the handshake down instead of importing it.
Sitecore now documents this path officially. The framework-agnostic development docs cover project setup, content rendering, and visual editing, with Astro among the example stacks, so what this post describes is a supported route rather than a workaround.
Source code. Everything above is implemented in our open-source Astro starter at github.com/rikaweb/astro-sitecore-content-sdk, on Astro 6 and Content SDK 1.4; this post explains what that code does. If you'd rather not own the endpoints yourself, the community EXDST Astro adapter wraps the same handshake. This is Part 2 of a series: Part 1 covers the Astro setup, and Part 3 compares the same site built on Astro and on Next.js.