From 2190d486c27f848fc75848c288a191abcd74dd1d Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 5 Oct 2026 13:03:54 -0400 Subject: [PATCH 1/4] [nextjs][pages] Soft-fail scClient requests when browser context ID is missing --- .../src/client/sitecore-client.test.ts | 61 +++++++++++++++++++ .../content/src/client/sitecore-client.ts | 12 +++- packages/content/src/client/utils.test.ts | 19 +++++- packages/content/src/client/utils.ts | 19 +++++- 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index 39ba2990dd..07482f28cc 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -11,6 +11,7 @@ import { } from '@sitecore-content-sdk/core/tools'; import { ErrorPage, SitecoreClient } from './sitecore-client'; +import { NoOpGraphQLClient } from './utils'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; import { LayoutServicePageState } from '../layout'; @@ -112,6 +113,46 @@ describe('SitecoreClient', () => { (sitecoreClient as any).sitemapXmlService = sitemapXmlServiceStub; }); + describe('constructor', () => { + const originalWindow = global.window; + + afterEach(() => { + global.window = originalWindow; + }); + + it('should set edgeInitialized when instantiated with a context id', () => { + const client = new SitecoreClient(defaultInitOptions); + + expect(client.edgeInitialized).to.be.true; + }); + + it('should set edgeInitialized to false when both server and client context ids are missing', () => { + const client = new SitecoreClient({ + ...defaultInitOptions, + api: { + ...defaultInitOptions.api, + edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl }, + }, + }); + + expect(client.edgeInitialized).to.be.false; + }); + + it('should use a NoOpGraphQLClient in the browser when the browser context id is not provided', () => { + const warnStub = sandbox.stub(console, 'warn'); + // Simulate a browser bundle with no Edge/local configuration + (global as any).window = {}; + + const client = new SitecoreClient({ + ...defaultInitOptions, + api: { edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl } }, + }); + + expect((client as any).graphQLClient).to.be.instanceOf(NoOpGraphQLClient); + expect(warnStub.called).to.be.true; + }); + }); + describe('getData', () => { let requestStub: sinon.SinonStub; @@ -1554,6 +1595,26 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([]); }); + + it('should log and return an empty array when both server and client context ids are missing', () => { + const warnStub = sandbox.stub(console, 'warn'); + const clientWithoutContext = new SitecoreClient({ + ...defaultInitOptions, + api: { + ...defaultInitOptions.api, + edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl }, + }, + }); + + const result = clientWithoutContext.getHeadLinks(layoutData); + + expect(result).to.deep.equal([]); + expect( + warnStub.calledWith( + 'Both client and server context IDs are absent. Cannot retrieve head links.' + ) + ).to.be.true; + }); }); describe('getSiteMap', () => { diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 67d6b9124d..60f6a1dc35 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -297,6 +297,7 @@ export interface BaseServiceOptions { * @public */ export class SitecoreClient implements BaseSitecoreClient { + public edgeInitialized = true; protected layoutService: LayoutService; protected dictionaryService: DictionaryService; protected editingService: EditingService; @@ -315,6 +316,10 @@ export class SitecoreClient implements BaseSitecoreClient { this.graphQLClient = this.clientFactory({ debugger: debug.http, }); + const { contextId: serverContextId, clientContextId } = this.initOptions.api.edge; + if (!serverContextId && !clientContextId) { + this.edgeInitialized = false; + } const baseServiceOptions = this.getBaseServiceOptions(); @@ -420,18 +425,23 @@ export class SitecoreClient implements BaseSitecoreClient { ): HTMLLink[] { const { enableStyles = true, enableThemes = true } = options; const { contextId: serverContextId, clientContextId } = this.initOptions.api.edge; + if (!this.edgeInitialized) { + console.warn('Both client and server context IDs are absent. Cannot retrieve head links.'); + return []; + } + const headLinks: HTMLLink[] = []; // Stylesheets are requested by the browser through elements, so the context id cannot be // moved into the x-sitecore-contextid header. Prefer the client context id to keep the // server-side one out of publicly rendered markup. const contextId = clientContextId || serverContextId; - if (!clientContextId) { debug.common( 'clientContextId is not configured, falling back to the server context id for stylesheet links' ); } + const edgeUrlForStyles = resolveEdgeUrlForStaticFiles(this.initOptions.api.edge.edgeUrl); if (enableStyles) { diff --git a/packages/content/src/client/utils.test.ts b/packages/content/src/client/utils.test.ts index b337276643..f0a34d43b8 100644 --- a/packages/content/src/client/utils.test.ts +++ b/packages/content/src/client/utils.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { GraphQLRequestClient } from '@sitecore-content-sdk/core'; -import { createGraphQLClientFactory } from './utils'; +import { createGraphQLClientFactory, NoOpGraphQLClient } from './utils'; describe('createGraphQLClientFactory', () => { const originalWindow = global.window; @@ -92,6 +92,23 @@ describe('createGraphQLClientFactory', () => { ).to.not.throw(); }); + it('returns a NoOpGraphQLClient factory in the browser when no IDs are configured', () => { + const warnStub = sinon.stub(console, 'warn'); + const createClientFactorySpy = sinon.spy(GraphQLRequestClient, 'createClientFactory'); + + // Browser bundle with no Edge/local configuration + (global as any).window = {}; + + const factory = createGraphQLClientFactory({ api: {} }); + const client = factory(); + + expect(client).to.be.instanceOf(NoOpGraphQLClient); + // Should not fall through to the real client factory + expect(createClientFactorySpy.called).to.be.false; + // Should warn that client-side requests will fail + expect(warnStub.calledOnce).to.be.true; + }); + it('throws error on server when no valid configuration is provided', () => { // Server (no window) delete (global as any).window; diff --git a/packages/content/src/client/utils.ts b/packages/content/src/client/utils.ts index ae6f1e5de2..831e01047f 100644 --- a/packages/content/src/client/utils.ts +++ b/packages/content/src/client/utils.ts @@ -1,10 +1,13 @@ import { FetchOptions, + GraphQLClient, GraphQLRequestClient, + GraphQLRequestClientFactory, GraphQLRequestClientFactoryConfig, } from '@sitecore-content-sdk/core'; import { SitecoreConfigInput } from '../config'; import { getEdgeProxyContentUrl } from './edge-proxy'; +import { debug } from '@sitecore-content-sdk/core'; /** * GraphQL client options @@ -12,6 +15,18 @@ import { getEdgeProxyContentUrl } from './edge-proxy'; */ export type GraphQLClientOptions = Pick & FetchOptions; +/** + * No op client to be used in browser context, when client API settings and env variables are missing + */ +export class NoOpGraphQLClient implements GraphQLClient { + request(): Promise { + debug.common( + 'GraphQL client was initialized without a valid context ID in browser context. Using fallback client that returns empty results.' + ); + return Promise.resolve({} as T); + } +} + /** * Creates a new GraphQLRequestClientFactory instance * @param {GraphQLClientOptions} options content sdk config @@ -47,9 +62,9 @@ export const createGraphQLClientFactory = (options: GraphQLClientOptions) => { // Browser bundle has no IDs – initialise a dummy client and warn /* eslint-disable no-console */ console.warn( - 'GraphQL client initialised in the browser without Edge or local API configuration; client-side requests may fail.' + 'GraphQL client initialized in the browser without Edge or local API configuration; client-side requests will return empty results.' ); - clientConfig = { endpoint: '/api/graphql' }; + return (() => new NoOpGraphQLClient()) as unknown as GraphQLRequestClientFactory; } else { throw new Error( `GraphQL client misconfigured. From 5178d23a39f1b8901a51b8231c567ac7376ec060 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 5 Oct 2026 13:09:01 -0400 Subject: [PATCH 2/4] changeset --- .changeset/jss-10377-scClient-fallback.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/jss-10377-scClient-fallback.md diff --git a/.changeset/jss-10377-scClient-fallback.md b/.changeset/jss-10377-scClient-fallback.md new file mode 100644 index 0000000000..c389624f8e --- /dev/null +++ b/.changeset/jss-10377-scClient-fallback.md @@ -0,0 +1,5 @@ +--- +'@sitecore-content-sdk/content': patch +--- + +Soft land scClient calls when context ID in browser is missing From e47da7d50914cee202d8afc837bbf3edaf21b1cf Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 5 Oct 2026 13:27:14 -0400 Subject: [PATCH 3/4] api surface --- packages/content/api/content-sdk-content.api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index 51a8350f77..47a250c41e 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -1480,6 +1480,8 @@ export class SitecoreClient implements BaseSitecoreClient { // (undocumented) protected dictionaryService: DictionaryService; // (undocumented) + edgeInitialized: boolean; + // (undocumented) protected editingService: EditingService; // (undocumented) protected errorPagesService: ErrorPagesService; From c59f354d806623661a87a1517cf6111a71701a5b Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Tue, 6 Oct 2026 12:50:03 -0400 Subject: [PATCH 4/4] PR comments --- .changeset/jss-10377-scClient-fallback.md | 5 ++++- packages/content/src/client/utils.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/jss-10377-scClient-fallback.md b/.changeset/jss-10377-scClient-fallback.md index c389624f8e..21a2312da2 100644 --- a/.changeset/jss-10377-scClient-fallback.md +++ b/.changeset/jss-10377-scClient-fallback.md @@ -2,4 +2,7 @@ '@sitecore-content-sdk/content': patch --- -Soft land scClient calls when context ID in browser is missing +Avoid throwing errors in scClient calls when context ID in browser is missing. + * Replaces the GraphQL client used by scClient with no-op fallback in browser context, when public content ID is missing, preventing `getPage()`, `getDictionary()` and other GraphQL-bound method from throwing. + * Adds `edgeInitialized` to scClient to indicate the status of Sitecore Edge connectivity. `false` in browser when public Edge context ID is missing. + * Adds a guard into `getHeadLinks()` call to return empty results when both server and client context IDs are missing, instead of throwing an error. diff --git a/packages/content/src/client/utils.ts b/packages/content/src/client/utils.ts index 831e01047f..a81a3afb50 100644 --- a/packages/content/src/client/utils.ts +++ b/packages/content/src/client/utils.ts @@ -4,10 +4,10 @@ import { GraphQLRequestClient, GraphQLRequestClientFactory, GraphQLRequestClientFactoryConfig, + debug, } from '@sitecore-content-sdk/core'; import { SitecoreConfigInput } from '../config'; import { getEdgeProxyContentUrl } from './edge-proxy'; -import { debug } from '@sitecore-content-sdk/core'; /** * GraphQL client options @@ -17,6 +17,7 @@ export type GraphQLClientOptions = Pick & FetchOptio /** * No op client to be used in browser context, when client API settings and env variables are missing + * @internal */ export class NoOpGraphQLClient implements GraphQLClient { request(): Promise {