Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

Troubleshooting SitecoreAI Page Builder Stuck Forever on the Loading Spinner

Here's the awkward part of this failure: SitecoreAI Page Builder opens the page in its editor iframe and the website content is right there. You can see it, but you never get the editing UI. The ng-spd-loading-indicator stays on top indefinitely and blocks interaction with the page.

Nothing points immediately to a dead rendering host. The page isn't blank, the app hasn't crashed completely, and there's no obvious "rendering host is unavailable" message. You may get only one browser console error, and it may look unrelated - duplicate initialization of a third-party script, for example.

For me, Page Builder got as far as displaying the page. The Sitecore loading overlay then stayed there. Getting the fix into DEV, UAT and PROD involved slightly different deployments, but it was the same fix, applied and verified in each environment.

Here is how I got from those first suspicions to the fix.

Something to start with.

First, a reminder of what Page Builder does when rendering in XM Cloud / SitecoreAI.

These are the main references:

The public website isn't all that Page Builder needs. It sends a request to a rendering endpoint, usually:

https://<editing-host-or-rendering-host>/api/editing/render

The endpoint checks the editing secret and takes the Sitecore editing parameters from the request. It fetches editing layout data, renders the page with metadata and returns markup for Page Builder.

That's why rendering with metadata matters: a visible website isn't necessarily editable in Page Builder.

What I First Suspected

There are several reasonable places to start with a Page Builder spinner. Some of them do explain other cases.

1. Broken Rendering Host Configuration

The rendering host item was the first thing I suspected:

/sitecore/system/Settings/Services/Rendering Hosts

When using metadata-based editing, pay attention to these fields:

Server side rendering engine endpoint URL:
https://<host>/api/editing/render

Server side rendering engine application URL:
https://<host>/

Server side rendering engine configuration URL:
https://<host>/api/editing/config

I couldn't put the main failure down to these values, although wrong values here can stop Page Builder early. They are still worth checking.

2. Missing Environment Variables

After that I looked at the environment configuration. The following variables are needed for Preview context and editing on the editing host and external rendering hosts:

SITECORE_EDGE_CONTEXT_ID
NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID
NEXT_PUBLIC_DEFAULT_SITE_NAME
SITECORE_EDITING_SECRET

I did have differences between environments, and corrected the variables, but the spinner still didn't go away. A missing value can give you 401, 404 or 500 from the render endpoint, depending on where it causes the application to fail.

3. Content, Presentation Details, or Personalization

Since I could see content in the iframe, a content-level problem seemed possible too:

  • final/shared presentation that is broken
  • a component that throws only in edit mode
  • a datasource item that is missing in Preview
  • a personalization rule that breaks the layout service output
  • a page using a rendering without metadata support

I wouldn't rule these out as causes of a Page Builder failure. But I had the same application-level behavior across multiple pages and environments; it seemed less likely that one content item was behind it.

4. Experience Edge, Publishing, Indexing, or Links Database

I also considered whether the backend state could explain it:

  • an item that hasn't been published to Edge
  • an Edge cache that is stale
  • an index that hasn't been rebuilt
  • a links database that hasn't been rebuilt
  • page route resolution that is wrong

I'd investigate those for missing content on a public site. But for editing, Page Builder gets Preview context and editing layout data. A problem rendering the public page can look much like an editing render problem in the browser, without being the same problem.

5. Browser Console Errors

The console also reported duplicate initialization of a third-party SDK. Suspicious, yes, but it wasn't blocking editing. The console was useful in the investigation; that prominent warning just wasn't the cause.

The Actual Symptom to Focus On

I wasn't dealing with a simple "the page does not load" failure.

More precisely, this was the failure:

The page content renders, but Page Builder never receives the successful editing-render completion it expects, so the ng-spd-loading-indicator never goes away.

I needed to look at the flow described above.

With a fully broken page, I'd usually look for a rendering exception. When the page renders but Page Builder can't edit it, I'd inspect the editing render flow.

The critical request is:

/api/editing/render

Editable markup with metadata is what Page Builder expects from that request.

How the Render Flow Is Supposed to Work

For Content SDK and Next.js, a Page Builder call to the render API route has parameters along these lines:

/api/editing/render
  ?secret=<editing-secret>
  &sc_site=<site-name>
  &sc_itemid=<item-id>
  &sc_lang=en
  &route=/
  &mode=edit
  &sc_version=latest
  &sc_layoutKind=shared

The SDK render middleware then:

  1. Checks that the editing secret is valid.
  2. Takes the editing parameters from the request.
  3. Turns on preview/editing mode.
  4. Calls the application back to render the requested page.
  5. Sends back HTML containing the metadata needed by Page Builder.

Following the callback back into the application showed me the problem.

This was an App Router application with a multisite route:

/{site}/{locale}/[[...path]]

Public traffic could use that route without trouble. The default render handler still did not know enough about where to render for Page Builder. Its internal editing render request was therefore subject to the usual routing and middleware, instead of being sent to a dedicated editing route.

Which left me in this rather odd situation:

  • the page was visibly rendered
  • the editing request was still affected by middleware and routing
  • the response reaching Page Builder wasn't exactly what it needed
  • the spinner stayed in place

The Fix: Create a Dedicated Editing Render Page

I needed the SDK render handler to send Page Builder editing requests somewhere private. I changed it to use a dedicated route and avoid coming back through the normal public one.

The render API route was creating its handlers with no options:

import { createEditingRenderRouteHandlers } from '@sitecore-content-sdk/nextjs/route-handler';

export const { GET, POST, OPTIONS } = createEditingRenderRouteHandlers({});

I added the options shown here:

import { createEditingRenderRouteHandlers } from '@sitecore-content-sdk/nextjs/route-handler';

export const { GET, POST, OPTIONS } = createEditingRenderRouteHandlers({
  allowedQueryParams: ['secret'],
  resolvePageUrl: () => '/sitecore-editing',
});

These options give the SDK the following instructions:

  • preserve the secret query parameter
  • render editing requests through /sitecore-editing
  • do not guess the public page URL for the internal request

For that private route I added this App Router page:

src/app/sitecore-editing/page.tsx

By the time this page renders the Sitecore page in editing mode, the SDK has already transformed the original sc_* query parameters into the editing request. Public traffic doesn't belong on this route.

The page has three jobs.

First comes request validation:

const CONTENT_SDK_PREVIEW_HEADER = "__content_sdk_preview";

if (
  !editingPreviewData ||
  requestHeaders.get(CONTENT_SDK_PREVIEW_HEADER) !== "1" ||
  secret !== scConfig.editingSecret
) {
  notFound();
}

Then it builds the editing preview data:

return {
  site,
  itemId,
  language,
  mode: mode === "edit"
    ? LayoutServicePageState.Edit
    : LayoutServicePageState.Preview,
  variantIds: getSearchParam(searchParams, "variantIds") || DEFAULT_VARIANT,
  version: getSearchParam(searchParams, "version"),
  layoutKind: getSearchParam(searchParams, "layoutKind") as LayoutKind | undefined,
};

It must then fetch Sitecore preview data, not public page data:

const page = await client.getPreview(editingPreviewData);

I could reuse the layout and providers from the normal public page. The data for this route comes from editing preview data, however, and the route stays out of the public routing behavior.

Do Not Forget Middleware

The middleware also needed a bypass.

If middleware handles multisite routing, localization, redirects, personalization, authentication, language cookies or similar behavior, it may also intercept /sitecore-editing.

That is expected for public traffic, but can interfere with an internal editing render request.

I added a bypass for this route in the proxy/middleware:

export async function proxy(req: NextRequest, _ev: NextFetchEvent) {
  const langCookie = req.cookies.get(LANG_COOKIE_NAME)?.value;
  const pathname = req.nextUrl.pathname;

  if (pathname === '/sitecore-editing') {
    return NextResponse.next();
  }

  // normal middleware logic follows
}

The editing render pipeline needed this bypass in order to use the route.

How I Verified the Fix

You don't need to open Page Builder yet to make the following checks.

1. Check the Render API Route

The render endpoint should respond to OPTIONS:

curl.exe -sS -o NUL -D - -X OPTIONS https://<host>/api/editing/render

A good response is:

HTTP/1.1 204 No Content
X-Matched-Path: /api/editing/render

That response tells you the route is there on the deployed host.

2. Check the Private Editing Route

Direct browser access to /sitecore-editing should be blocked by the page's protection:

curl.exe -sS -o NUL -D - https://<host>/sitecore-editing

Expected result:

HTTP/1.1 404 Not Found
X-Matched-Path: /sitecore-editing

The request has matched the route, so the route exists. But a direct request has neither the internal preview header nor the secret. The security checks should therefore refuse access, which is what this response shows.

3. Check the Actual Page Builder Network Flow

Now open Page Builder with the Network tab in view.

The request to /api/editing/render should complete. A normal error needs its own troubleshooting. When the HTML looks like a public route, there are two checks: resolvePageUrl must point to /sitecore-editing and middleware must not rewrite it.

Deployment Gotchas Across DEV, UAT, and PROD

The environments had different deployment setups.

In DEV the shared render helper was already in the application branch. I could therefore put the fix straight through the development branch.

For UAT I needed to update both the Sitecore editing host and the external Vercel rendering host. Either can be used by Page Builder; the rendering host item configuration determines which.

In PROD the production editing host was tied to main, rather than the development branch. I couldn't push the whole development branch to it. Instead I backported just the fix to main, including only:

src/app/api/editing/render/route.ts
src/app/sitecore-editing/page.tsx
src/proxy.ts

Then the production editing host was deployed from main, and the production Vercel projects were deployed manually because Git deployments were disabled for those projects.

That's why I would check both hosts' deployments when fixing Page Builder. The public rendering host may not be deployed in the same way as the Sitecore editing host.

The Minimal Checklist

For another occurrence, I'd work through these checks:

  1. Establish whether Page Builder is failing during editing render, rather than just public page rendering.
  2. Check /api/editing/render on the rendering host configured in Sitecore.
  3. Use the metadata endpoint in the rendering host item, not an old /jss-render endpoint.
  4. Check the host has SITECORE_EDITING_SECRET and the Edge preview variables.
  5. Check whether App Router, middleware, redirects, localization or multisite proxy logic intercept editing requests.
  6. Give App Router editing render its own /sitecore-editing route.
  7. Have createEditingRenderRouteHandlers use that route via resolvePageUrl.
  8. Make middleware bypass /sitecore-editing.
  9. Get the change deployed to the Sitecore editing host and all external rendering hosts that Page Builder might use.
  10. Check OPTIONS /api/editing/render, and check the protection against direct access on /sitecore-editing.

Final Thoughts

I had a rendered site and visible content, which hardly looked like a crash. I also had console errors that could lead me away from the actual problem. None of that changed the fact that Page Builder couldn't proceed.

The response from the rendering host has to be editing-aware and metadata-enabled for SitecoreAI Page Builder. When using Next.js App Router with custom middleware, I'd take the safest option: give the SDK render handler a dedicated private route and keep it outside the normal public routing pipeline.

After that change the spinner went away. Editing worked normally across the environments again.

So even if it looks like a broken component or an item you can't edit, check what happens between Page Builder and the rendering host as well.