Experience Sitecore ! | All posts tagged 'Content SDK'

Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

Troubleshooting SitecoreAI Page Builder Stuck Forever on the Loading Spinner

Have you experienced that at least once? You open a page in SitecoreAI Page Builder, the page itself visually loads inside the editor iframe, you can clearly see the website content, but the editing UI never becomes available. Instead, the ng-spd-loading-indicator remains on top forever, blocking any interaction with the page.

That is a frustrating one, because at first sight the rendering host seems to work. The page is not blank. The app does not completely crash. There is no obvious "rendering host is unavailable" message. You may even see only one unrelated-looking browser console error, for example, something like a duplicate third-party script initialization.

In my case, the visible symptom was exactly that: Page Builder loaded the page content, but the Sitecore loading overlay never disappeared. The same fix had to be applied and verified on DEV, UAT, and PROD, with slightly different deployment mechanics per environment.

Let's walk through the troubleshooting route from the first false assumptions to the final fix.

Something to start with ...

Before digging into implementation details, it is worth refreshing how Page Builder rendering is supposed to work in XM Cloud / SitecoreAI.

The most important references are:

The key thing to remember is that Page Builder does not simply browse your public website. It talks to a rendering endpoint, typically:

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

That endpoint validates the editing secret, extracts Sitecore editing parameters, fetches editing layout data, renders the page with metadata, and returns markup that Page Builder can understand.

That "with metadata" part is important. A page that visually renders as a website is not automatically a page that Page Builder can edit.

What I First Suspected

When Page Builder hangs with a spinner, there are many tempting directions to investigate first. Some of them are absolutely valid in different cases.

1. Broken Rendering Host Configuration

The first obvious suspect is the rendering host item under:

/sitecore/system/Settings/Services/Rendering Hosts

For metadata-based editing, the important fields are:

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

If these are wrong, Page Builder may fail early. In my case they were not the main reason, but they are still always worth checking.

2. Missing Environment Variables

The next suspect is environment configuration. The editing host and external rendering hosts need the right variables for Preview context and editing:

SITECORE_EDGE_CONTEXT_ID
NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID
NEXT_PUBLIC_DEFAULT_SITE_NAME
SITECORE_EDITING_SECRET

If one of those is missing, the render endpoint may return 401, 404, or 500 depending on where the application fails. I did find environment differences during the overall investigation, but fixing variables alone was not sufficient for the spinner problem.

3. Content, Presentation Details, or Personalization

Because the page content appeared in the iframe, it was natural to suspect some content-level issue:

  • broken final/shared presentation
  • component throwing only in edit mode
  • datasource item missing in Preview
  • personalization rule breaking layout service output
  • page relying on a rendering that does not support metadata

Those issues can absolutely break Page Builder. However, in this case the same application-level behavior appeared across pages and environments, which made a single content item less likely.

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

Another direction is backend state:

  • item not published to Edge
  • stale Edge cache
  • index not rebuilt
  • links database not rebuilt
  • bad page route resolution

These are valid checks for missing content on a public site. But Page Builder editing uses Preview context and editing layout data. A public rendering problem and an editing render problem may look similar in the browser, but they are not the same thing.

5. Browser Console Errors

There was also a console error about duplicate initialization of a third-party SDK. It looked suspicious, but it was not the blocker. This is a good reminder: browser console noise is useful, but do not let the loudest warning own the investigation.

The Actual Symptom to Focus On

The important symptom was not simply "the page does not load".

The more precise symptom was:

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.

This distinction matters a lot.

If a page is fully broken, you usually chase a rendering exception. If the page renders but Page Builder cannot edit it, you need to inspect the editing render flow.

The critical request is:

/api/editing/render

That is the request Page Builder makes to get editable metadata-backed markup.

How the Render Flow Is Supposed to Work

With Content SDK and Next.js, Page Builder calls the render API route with parameters similar to:

/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. Validates the editing secret.
  2. Extracts the editing parameters.
  3. Enables preview/editing mode.
  4. Calls back into the application to render the requested page.
  5. Returns HTML that includes the metadata Page Builder needs.

That callback into the application is where this case became interesting.

The application was using App Router with a multisite route shape:

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

The normal page route worked perfectly for public traffic. But for Page Builder, the default render handler was not explicit enough. The internal editing render request was going through the normal routing and middleware chain instead of a dedicated editing route.

That created a half-broken experience:

  • the page could visually render
  • middleware and routing still interfered with the editing request
  • Page Builder did not receive exactly the response it needed
  • the spinner never disappeared

The Fix: Create a Dedicated Editing Render Page

The fix was to stop sending the SDK render handler back through the normal public route and give Page Builder a dedicated private route for editing render.

The render API route changed from this:

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

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

to this:

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

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

That tells the SDK:

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

Then I added a private App Router page:

src/app/sitecore-editing/page.tsx

Its job is not to serve public traffic. Its job is to render a Sitecore page in editing mode after the SDK has transformed the original sc_* query parameters into the editing request.

The page does three important things.

First, it validates the request:

const CONTENT_SDK_PREVIEW_HEADER = "__content_sdk_preview";

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

Second, it constructs 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,
};

Third, it fetches Sitecore preview data instead of public page data:

const page = await client.getPreview(editingPreviewData);

After that, it renders the same layout and providers as the normal public page. The difference is that the data source is now the editing preview data, and the route is isolated from public routing behavior.

Do Not Forget Middleware

This was the other important part.

If the application has middleware for multisite routing, localization, redirects, personalization, authentication, language cookies, or anything similar, that middleware may intercept /sitecore-editing.

For public traffic that is normal. For the internal editing render request, it is dangerous.

So the proxy/middleware needs to bypass this route:

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
}

That one small bypass is the difference between "the route exists" and "the route is actually usable by the editing render pipeline".

How I Verified the Fix

There are a few useful checks that do not require opening Page Builder first.

1. Check the Render API Route

The render endpoint should answer 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 confirms the route exists on the deployed host.

2. Check the Private Editing Route

Direct browser access to /sitecore-editing should not show the page. It should be protected:

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

Expected result:

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

This is a very useful result. It means the route is deployed and matched, but the security checks are working because the direct request does not include the internal preview header and secret.

3. Check the Actual Page Builder Network Flow

After that, open Page Builder and watch the Network tab.

The request to /api/editing/render should complete. If it returns a normal error, troubleshoot that error. If it returns an HTML page that looks like a public route, check that resolvePageUrl is actually pointing to /sitecore-editing and that middleware is not rewriting it.

Deployment Gotchas Across DEV, UAT, and PROD

The code fix was the same, but deployment was not.

In DEV, the application branch already contained the shared render helper, so the fix was straightforward and went through the development branch.

In UAT, the same fix had to be deployed both to the Sitecore editing host and the external Vercel rendering host, because Page Builder may use either depending on the rendering host item configuration.

In PROD, there was an additional twist: the production editing host was tied to main, not the development branch. Therefore, blindly pushing the full development branch would have been wrong. The fix had to be backported narrowly 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 is another important lesson: when fixing Page Builder, do not assume the public rendering host and the Sitecore editing host are deployed by the same mechanism. Check both.

The Minimal Checklist

If I had to reduce the whole troubleshooting route to a checklist, it would be this:

  1. Confirm Page Builder is failing at the editing render stage, not just public page rendering.
  2. Check /api/editing/render on the host configured in Sitecore.
  3. Confirm the rendering host item points to the metadata endpoint, not an old /jss-render endpoint.
  4. Confirm SITECORE_EDITING_SECRET and Edge preview variables exist on the host.
  5. Check whether App Router, middleware, redirects, localization, or multisite proxy logic intercepts editing requests.
  6. Add a dedicated /sitecore-editing route for App Router editing render.
  7. Point createEditingRenderRouteHandlers to that route with resolvePageUrl.
  8. Bypass middleware for /sitecore-editing.
  9. Deploy both the Sitecore editing host and any external rendering host Page Builder may use.
  10. Verify OPTIONS /api/editing/render and protected direct access to /sitecore-editing.

Final Thoughts

This problem is confusing because it does not look like a classic crash. The site renders. The content is visible. The browser may even show unrelated console errors. But Page Builder is still blocked because visual rendering is only half of the contract.

For SitecoreAI Page Builder, the rendering host must return an editing-aware, metadata-enabled response. With Next.js App Router and custom middleware, the safest approach is to give the SDK render handler a dedicated private route and keep that route out of the normal public routing pipeline.

Once that was done, the spinner disappeared and the editing experience started behaving normally across the environments.

Sometimes the fix is not in the component that appears broken, nor in the content item being edited. It is in the invisible bridge between Page Builder and the rendering host.