diff --git a/.changeset/jss-10377-scClient-fallback.md b/.changeset/jss-10377-scClient-fallback.md
new file mode 100644
index 0000000000..21a2312da2
--- /dev/null
+++ b/.changeset/jss-10377-scClient-fallback.md
@@ -0,0 +1,8 @@
+---
+'@sitecore-content-sdk/content': patch
+---
+
+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/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;
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..a81a3afb50 100644
--- a/packages/content/src/client/utils.ts
+++ b/packages/content/src/client/utils.ts
@@ -1,7 +1,10 @@
import {
FetchOptions,
+ GraphQLClient,
GraphQLRequestClient,
+ GraphQLRequestClientFactory,
GraphQLRequestClientFactoryConfig,
+ debug,
} from '@sitecore-content-sdk/core';
import { SitecoreConfigInput } from '../config';
import { getEdgeProxyContentUrl } from './edge-proxy';
@@ -12,6 +15,19 @@ 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
+ * @internal
+ */
+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 +63,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.