From 4e866f8d81bf95c2867af246e2ea98d1e63e4e2f Mon Sep 17 00:00:00 2001 From: MenKNas Date: Fri, 30 Jan 2026 19:21:37 +0200 Subject: [PATCH 01/25] Route Edge requests and returned URLs through SITECORE_EDGE_HOSTNAME --- packages/content/src/client/edge-proxy.ts | 21 +- .../src/client/sitecore-client.test.ts | 23 +- .../content/src/client/sitecore-client.ts | 6 +- packages/content/src/config/define-config.ts | 7 +- .../src/editing/component-layout-service.ts | 14 +- .../content/src/editing/design-library.ts | 12 +- .../content/src/editing/editing-service.ts | 17 +- packages/content/src/layout/content-styles.ts | 16 +- packages/content/src/layout/index.ts | 2 + packages/content/src/layout/layout-service.ts | 9 +- .../src/layout/rewrite-edge-host.test.ts | 215 ++++++++++++++++++ .../content/src/layout/rewrite-edge-host.ts | 121 ++++++++++ packages/content/src/layout/themes.ts | 16 +- .../content/src/site/error-pages-service.ts | 20 +- packages/core/src/tools/index.ts | 9 + .../core/src/tools/resolve-edge-url.test.ts | 159 +++++++++++++ packages/core/src/tools/resolve-edge-url.ts | 163 +++++++++++++ .../nextjs-app-router/.env.remote.example | 9 + .../src/templates/nextjs/.env.remote.example | 9 + packages/nextjs/src/config/define-config.ts | 3 +- .../react/src/components/SitecoreProvider.tsx | 6 +- packages/search/src/search-service.ts | 5 +- 22 files changed, 788 insertions(+), 74 deletions(-) create mode 100644 packages/content/src/layout/rewrite-edge-host.test.ts create mode 100644 packages/content/src/layout/rewrite-edge-host.ts create mode 100644 packages/core/src/tools/resolve-edge-url.test.ts create mode 100644 packages/core/src/tools/resolve-edge-url.ts diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index 69ac2f0b3d..94057071ed 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -1,30 +1,27 @@ -import { constants } from '@sitecore-content-sdk/core'; -import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; - -const { SITECORE_EDGE_URL_DEFAULT } = constants; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; /** * Generates a URL for accessing Sitecore Edge Platform Content using the provided endpoint and context ID. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. Default is https://edge-platform.sitecorecloud.io + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. If not provided, + * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {string} The complete URL for accessing content through the Edge Platform. * @public */ -export const getEdgeProxyContentUrl = (sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT) => - `${normalizeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; +export const getEdgeProxyContentUrl = (sitecoreEdgeUrl?: string) => + `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; /** * Generates a URL for accessing Sitecore Edge Platform Forms using the provided form ID and context ID. * @param {string} sitecoreEdgeContextId - The unique context id. * @param {string} formId - The unique form id. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. Default is https://edge-platform.sitecorecloud.io + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. If not provided, + * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {string} The complete URL for accessing forms through the Edge Platform. * @internal */ export const getEdgeProxyFormsUrl = ( sitecoreEdgeContextId: string, formId: string, - sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl?: string ) => - `${normalizeUrl( - sitecoreEdgeUrl - )}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; + `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index cebf157612..c407f665e6 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -1,4 +1,4 @@ -/* eslint-disable dot-notation */ +/* eslint-disable dot-notation */ /* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ import chai, { expect } from 'chai'; import sinonChai from 'sinon-chai'; @@ -1454,6 +1454,27 @@ describe('SitecoreClient', () => { expect(result).to.equal(xmlContent); }); + it('should rewrite Edge hostnames in sitemap path and XML when custom hostname is configured', async () => { + const originalEnv = process.env.SITECORE_EDGE_HOSTNAME; + process.env.SITECORE_EDGE_HOSTNAME = 'https://custom.example.com'; + + const edgeSitemapPath = 'https://edge-platform.sitecorecloud.io/sitemap.xml'; + const xmlContent = + 'https://edge-platform.sitecorecloud.io/a'; + + sitemapXmlServiceStub.getSitemap.resolves(edgeSitemapPath); + const dataFetcherStub = sandbox + .stub(NativeDataFetcher.prototype, 'fetch') + .resolves({ data: xmlContent, status: 200, statusText: 'OK' }); + + const result = await sitecoreClient.getSiteMap({ ...defaultReqConfig }); + + expect(dataFetcherStub.calledWith('https://custom.example.com/sitemap.xml')).to.be.true; + expect(result).to.include('https://custom.example.com/a'); + + process.env.SITECORE_EDGE_HOSTNAME = originalEnv; + }); + it('should fetch specific sitemap when ID is provided', async () => { const sitemapId = '1'; const absoluteSitemapPath = 'https://cdn.example.com/sitemap-1.xml'; diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 1c80043fde..0b05d22086 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -15,6 +15,7 @@ import { LayoutServiceData, RouteOptions, LayoutServicePageState, + rewriteEdgeHostInResponse, } from '../layout'; import { HTMLLink, StaticPath } from '../models'; import { getGroomedVariantIds, PersonalizedRewriteData } from '../personalize/utils'; @@ -623,12 +624,13 @@ export class SitecoreClient implements BaseSitecoreClient { // regular sitemap if (sitemapPath) { try { + const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath); const fetcher = new NativeDataFetcher(); - const xmlResponse = await fetcher.fetch(sitemapPath); + const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); if (!xmlResponse.data) { throw new Error('REDIRECT_404'); } - return xmlResponse.data; + return rewriteEdgeHostInResponse(xmlResponse.data); // eslint-disable-next-line no-unused-vars } catch (error) { throw new Error('REDIRECT_404'); diff --git a/packages/content/src/config/define-config.ts b/packages/content/src/config/define-config.ts index c9a2aea45b..d775522028 100644 --- a/packages/content/src/config/define-config.ts +++ b/packages/content/src/config/define-config.ts @@ -1,9 +1,8 @@ -import { constants, DefaultRetryStrategy } from '@sitecore-content-sdk/core'; +import { DefaultRetryStrategy } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { DeepPartial, SitecoreConfig, SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; - /** * Provides default initial values for SitecoreConfig * @returns default config @@ -13,7 +12,7 @@ export const getFallbackConfig = (): SitecoreConfig => ({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: '', - edgeUrl: process.env.SITECORE_EDGE_URL || SITECORE_EDGE_URL_DEFAULT, + edgeUrl: resolveEdgeUrl(), }, local: { apiKey: process.env.SITECORE_API_KEY || process.env.NEXT_PUBLIC_SITECORE_API_KEY || '', diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index c55dd69d72..dedf572b9a 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -1,11 +1,9 @@ -import { NativeDataFetcher, constants, FetchOptions } from '@sitecore-content-sdk/core'; -import { resolveUrl } from '@sitecore-content-sdk/core/tools'; -import { LayoutServiceData } from '../layout/models'; +import { NativeDataFetcher, FetchOptions } from '@sitecore-content-sdk/core'; +import { resolveUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { LayoutServiceData, rewriteEdgeHostInResponse } from '../layout'; import debug from '../debug'; import { DesignLibraryMode, DesignLibraryVariantGeneration } from './models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; - /** * Params for requesting component data in Design Library mode * @public @@ -113,10 +111,10 @@ export class ComponentLayoutService { sc_editMode: `${params.mode === DesignLibraryMode.Metadata}`, }, }) - .then((response) => response.data) + .then((response) => rewriteEdgeHostInResponse(response.data)) .catch((error) => { if (error.response?.status === 404) { - return error.response.data; + return rewriteEdgeHostInResponse(error.response.data); } throw error; }); @@ -144,7 +142,7 @@ export class ComponentLayoutService { */ private getFetchUrl(params: ComponentLayoutRequestParams) { return resolveUrl( - `${this.config.edgeUrl || SITECORE_EDGE_URL_DEFAULT}/layout/component`, + `${resolveEdgeUrl(this.config.edgeUrl)}/layout/component`, this.getComponentFetchParams(params) ); } diff --git a/packages/content/src/editing/design-library.ts b/packages/content/src/editing/design-library.ts index e35069077f..d7b2e926a5 100644 --- a/packages/content/src/editing/design-library.ts +++ b/packages/content/src/editing/design-library.ts @@ -1,5 +1,4 @@ -import { constants } from '@sitecore-content-sdk/core'; -import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentFields, ComponentParams, @@ -9,8 +8,6 @@ import { } from '../layout/models'; import { DesignLibraryMode } from './models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; - /** * Event to be sent when report status to design library */ @@ -223,12 +220,13 @@ export function getDesignLibraryStatusEvent( /** * Generates the URL for the design library script link. - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. Default is https://edge-platform.sitecorecloud.io + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, + * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns The full URL to the design library script. * @internal */ -export function getDesignLibraryScriptLink(sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT): string { - return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; +export function getDesignLibraryScriptLink(sitecoreEdgeUrl?: string): string { + return `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; } /** diff --git a/packages/content/src/editing/editing-service.ts b/packages/content/src/editing/editing-service.ts index 2695e71c45..704b9370de 100644 --- a/packages/content/src/editing/editing-service.ts +++ b/packages/content/src/editing/editing-service.ts @@ -1,6 +1,6 @@ import { GraphQLClient, GraphQLRequestClientFactory, FetchOptions } from '@sitecore-content-sdk/core'; import debug from '../debug'; -import { LayoutServiceData, LayoutServicePageState } from '../layout'; +import { LayoutServiceData, LayoutServicePageState, rewriteEdgeHostInResponse } from '../layout'; import { LayoutKind } from './models'; /** @@ -100,14 +100,17 @@ export class EditingService { } ); - return { - layoutData: editingData?.item?.rendered || { - sitecore: { - context: { pageEditing: true, language }, - route: null, - }, + const layoutData = editingData?.item?.rendered || { + sitecore: { + context: { pageEditing: true, language }, + route: null, }, }; + + // Rewrite Edge hostnames in response if custom hostname is configured + return { + layoutData: rewriteEdgeHostInResponse(layoutData), + }; } /** diff --git a/packages/content/src/layout/content-styles.ts b/packages/content/src/layout/content-styles.ts index 6ad6c7e754..9b02bc821d 100644 --- a/packages/content/src/layout/content-styles.ts +++ b/packages/content/src/layout/content-styles.ts @@ -1,10 +1,7 @@ -import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; -import { constants } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, Field, Item, LayoutServiceData, RouteData } from './index'; import { HTMLLink } from '../models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; - /** * Regular expression to check if the content styles are used in the field value */ @@ -16,14 +13,15 @@ type Config = { loadStyles: boolean }; * Get the content styles link to be loaded from the Sitecore Edge Platform * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. Default is https://edge-platform.sitecorecloud.io + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, + * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {HTMLLink | null} content styles link, null if no styles are used in layout * @public */ export const getContentStylesheetLink = ( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl?: string ): HTMLLink | null => { if (!layoutData.sitecore.route) return null; @@ -41,11 +39,9 @@ export const getContentStylesheetLink = ( export const getContentStylesheetUrl = ( sitecoreEdgeContextId: string, - sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl?: string ): string => - `${normalizeUrl( - sitecoreEdgeUrl - )}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; + `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; export const traversePlaceholder = (components: Array, config: Config) => { if (config.loadStyles) return; diff --git a/packages/content/src/layout/index.ts b/packages/content/src/layout/index.ts index 53c1710c67..e2702f92f8 100644 --- a/packages/content/src/layout/index.ts +++ b/packages/content/src/layout/index.ts @@ -35,3 +35,5 @@ export { getContentStylesheetLink } from './content-styles'; export { LayoutService, LayoutServiceConfig, GRAPHQL_LAYOUT_QUERY_NAME } from './layout-service'; export { getDesignLibraryStylesheetLinks } from './themes'; + +export { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; diff --git a/packages/content/src/layout/layout-service.ts b/packages/content/src/layout/layout-service.ts index 0478f94da3..77e491a34e 100644 --- a/packages/content/src/layout/layout-service.ts +++ b/packages/content/src/layout/layout-service.ts @@ -1,6 +1,7 @@ import { FetchOptions } from '@sitecore-content-sdk/core'; import { GraphQLServiceConfig, SitecoreServiceBase } from '../sitecore-service-base'; import { LayoutServiceData, RouteOptions } from './models'; +import { rewriteEdgeHostInResponse } from './rewrite-edge-host'; import debug from '../debug'; import { SitecoreConfigInput } from '../config'; @@ -51,11 +52,13 @@ export class LayoutService extends SitecoreServiceBase { }>(query, {}, fetchOptions); // If `rendered` is empty -> not found - return ( + const layoutData = data?.layout?.item?.rendered || { sitecore: { context: { pageEditing: false, language: routeOptions?.locale }, route: null }, - } - ); + }; + + // Rewrite Edge hostnames in response if custom hostname is configured + return rewriteEdgeHostInResponse(layoutData); } /** diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts new file mode 100644 index 0000000000..22e59a2990 --- /dev/null +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -0,0 +1,215 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; +import { + SITECORE_EDGE_HOSTNAME_ENV, + SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, +} from '@sitecore-content-sdk/core/tools'; + +describe('rewriteEdgeHostInResponse', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all relevant env vars before each test + delete process.env[SITECORE_EDGE_HOSTNAME_ENV]; + delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + }); + + afterEach(() => { + // Restore original env + process.env = { ...originalEnv }; + }); + + describe('rewriteEdgeHostInResponse()', () => { + it('should return response unchanged when no custom hostname is configured', () => { + const response = { + url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result).to.deep.equal(response); + }); + + it('should rewrite edge-platform.sitecorecloud.io in string values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); + }); + + it('should rewrite edge.sitecorecloud.io in string values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'https://edge.sitecorecloud.io/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); + }); + + it('should rewrite multiple occurrences in a string', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + html: '', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.html).to.equal( + '' + ); + }); + + it('should rewrite nested objects', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + sitecore: { + context: {}, + route: { + fields: { + image: { + value: { + src: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + }, + }, + }, + }, + }, + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.sitecore.route.fields.image.value.src).to.equal( + 'https://custom.example.com/media/image.jpg' + ); + }); + + it('should rewrite arrays', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + urls: [ + 'https://edge-platform.sitecorecloud.io/a.jpg', + 'https://edge-platform.sitecorecloud.io/b.jpg', + ], + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.urls).to.deep.equal([ + 'https://custom.example.com/a.jpg', + 'https://custom.example.com/b.jpg', + ]); + }); + + it('should handle null values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + value: null, + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.value).to.be.null; + }); + + it('should handle undefined values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + value: undefined, + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.value).to.be.undefined; + }); + + it('should preserve non-string primitives', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + number: 42, + boolean: true, + string: 'no edge url here', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.number).to.equal(42); + expect(result.boolean).to.be.true; + expect(result.string).to.equal('no edge url here'); + }); + + it('should handle http protocol in edge URLs', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'http://edge-platform.sitecorecloud.io/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); + }); + + it('should handle mixed case (case insensitive)', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'https://EDGE-PLATFORM.SITECORECLOUD.IO/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); + }); + + it('should handle complex layout service data structure', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const layoutData = { + sitecore: { + context: { + pageEditing: false, + language: 'en', + }, + route: { + name: 'Home', + placeholders: { + main: [ + { + componentName: 'Image', + fields: { + image: { + value: { + src: 'https://edge-platform.sitecorecloud.io/-/media/image.jpg', + alt: 'Test image', + }, + }, + }, + }, + { + componentName: 'RichText', + fields: { + content: { + value: + '

Image:

', + }, + }, + }, + ], + }, + }, + }, + }; + + const result = rewriteEdgeHostInResponse(layoutData); + + expect(result.sitecore.route.placeholders.main[0].fields.image.value.src).to.equal( + 'https://custom.example.com/-/media/image.jpg' + ); + expect(result.sitecore.route.placeholders.main[1].fields.content.value).to.equal( + '

Image:

' + ); + }); + }); + + describe('containsDefaultEdgeHost()', () => { + it('should return true for edge-platform.sitecorecloud.io', () => { + expect( + containsDefaultEdgeHost('https://edge-platform.sitecorecloud.io/media/image.jpg') + ).to.be.true; + }); + + it('should return true for edge.sitecorecloud.io', () => { + expect(containsDefaultEdgeHost('https://edge.sitecorecloud.io/media/image.jpg')).to.be.true; + }); + + it('should return false for custom hostname', () => { + expect(containsDefaultEdgeHost('https://custom.example.com/media/image.jpg')).to.be.false; + }); + + it('should return false for empty string', () => { + expect(containsDefaultEdgeHost('')).to.be.false; + }); + }); +}); diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts new file mode 100644 index 0000000000..17d52587cd --- /dev/null +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -0,0 +1,121 @@ +import { hasCustomEdgeHostname, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; + +/** + * The default Edge Platform hostnames that may appear in responses. + * These will be replaced with the custom hostname when configured. + * @internal + */ +const DEFAULT_EDGE_HOSTNAMES = [ + 'edge-platform.sitecorecloud.io', + 'edge.sitecorecloud.io', // Legacy hostname, included for defensive replacement +]; + +/** + * Regular expression patterns for matching Edge hostnames in URLs. + * Matches both http:// and https:// protocols. + * @internal + */ +const EDGE_HOST_PATTERNS = DEFAULT_EDGE_HOSTNAMES.map( + (hostname) => new RegExp(`https?://${hostname.replace(/\./g, '\\.')}`, 'gi') +); + +/** + * Rewrites Edge Platform hostnames in a response object to use the custom hostname. + * This function performs a deep traversal of the object and replaces any string values + * containing the default Edge hostnames with the custom hostname. + * + * Only performs rewriting when a custom Edge hostname is configured via environment variables. + * + * @param {T} response - The response object to process (typically LayoutServiceData) + * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) + * @public + * + * @example + * // With SITECORE_EDGE_HOSTNAME=my-tenant.edge.example.com + * const layout = await layoutService.fetchLayoutData(path, options); + * const rewritten = rewriteEdgeHostInResponse(layout); + * // All URLs like 'https://edge-platform.sitecorecloud.io/...' are now 'https://my-tenant.edge.example.com/...' + */ +export function rewriteEdgeHostInResponse(response: T): T { + // Skip if no custom hostname is configured + if (!hasCustomEdgeHostname()) { + return response; + } + + const customEdgeUrl = resolveEdgeUrl(); + + return deepRewriteEdgeHost(response, customEdgeUrl); +} + +/** + * Recursively traverses an object/array and rewrites Edge hostnames in string values. + * + * @param {T} value - The value to process + * @param {string} customEdgeUrl - The custom Edge URL to replace with + * @returns {T} The processed value with Edge hostnames replaced + * @internal + */ +function deepRewriteEdgeHost(value: T, customEdgeUrl: string): T { + // Handle null/undefined + if (value === null || value === undefined) { + return value; + } + + // Handle strings - perform the actual replacement + if (typeof value === 'string') { + return rewriteEdgeHostInString(value, customEdgeUrl) as T; + } + + // Handle arrays + if (Array.isArray(value)) { + return value.map((item) => deepRewriteEdgeHost(item, customEdgeUrl)) as T; + } + + // Handle plain objects + if (typeof value === 'object') { + // Skip non-plain objects (Date, RegExp, etc.) + if (Object.getPrototypeOf(value) !== Object.prototype) { + return value; + } + + const result: Record = {}; + for (const key of Object.keys(value as Record)) { + result[key] = deepRewriteEdgeHost((value as Record)[key], customEdgeUrl); + } + return result as T; + } + + // Return primitives (numbers, booleans) unchanged + return value; +} + +/** + * Replaces Edge Platform hostnames in a string with the custom hostname. + * + * @param {string} str - The string to process + * @param {string} customEdgeUrl - The custom Edge URL to replace with + * @returns {string} The string with Edge hostnames replaced + * @internal + */ +function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { + let result = str; + + for (const pattern of EDGE_HOST_PATTERNS) { + // Reset lastIndex for global regex + pattern.lastIndex = 0; + result = result.replace(pattern, customEdgeUrl); + } + + return result; +} + +/** + * Checks if a string contains any default Edge Platform hostnames. + * + * @param {string} str - The string to check + * @returns {boolean} True if the string contains a default Edge hostname + * @public + */ +export function containsDefaultEdgeHost(str: string): boolean { + return DEFAULT_EDGE_HOSTNAMES.some((hostname) => str.includes(hostname)); +} diff --git a/packages/content/src/layout/themes.ts b/packages/content/src/layout/themes.ts index 643d4b8ef7..598fb84dbf 100644 --- a/packages/content/src/layout/themes.ts +++ b/packages/content/src/layout/themes.ts @@ -1,10 +1,7 @@ -import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; -import { constants } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, LayoutServiceData, RouteData, getFieldValue } from '.'; import { HTMLLink } from '../models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; - /** * Pattern for library ids * @example -library--foo @@ -15,14 +12,15 @@ const STYLES_LIBRARY_ID_REGEX = /-library--([^\s]+)/; * Walks through rendering tree and returns list of links of all FEAAS, BYOC or SXA Design Library Stylesheets that are used * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. Default is https://edge-platform.sitecorecloud.io + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, + * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {HTMLLink[]} library stylesheet links * @public */ export function getDesignLibraryStylesheetLinks( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl?: string ): HTMLLink[] { const ids = new Set(); @@ -39,11 +37,9 @@ export function getDesignLibraryStylesheetLinks( export const getStylesheetUrl = ( id: string, sitecoreEdgeContextId: string, - sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl?: string ) => { - return `${normalizeUrl( - sitecoreEdgeUrl - )}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; + return `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; }; /** diff --git a/packages/content/src/site/error-pages-service.ts b/packages/content/src/site/error-pages-service.ts index 0be95b5cdf..1192e0ee91 100644 --- a/packages/content/src/site/error-pages-service.ts +++ b/packages/content/src/site/error-pages-service.ts @@ -1,7 +1,7 @@ import { GraphQLRequestClientFactory } from '@sitecore-content-sdk/core'; import { FetchOptions, GraphQLClient } from '../client'; import debug from '../debug'; -import { LayoutServiceData } from '../layout'; +import { LayoutServiceData, rewriteEdgeHostInResponse } from '../layout'; import { GraphQLServiceConfig } from '../sitecore-service-base'; import { siteNameError } from '../constants'; @@ -105,9 +105,21 @@ export class ErrorPagesService { }, fetchOptions )) - .then((result: ErrorPagesQueryResult) => - result.site.siteInfo ? result.site.siteInfo.errorHandling : null - ) + .then((result: ErrorPagesQueryResult) => { + if (!result.site.siteInfo) return null; + + // Rewrite Edge hostnames in error page layouts if custom hostname is configured + const errorHandling = result.site.siteInfo.errorHandling; + return { + ...errorHandling, + notFoundPage: { + rendered: rewriteEdgeHostInResponse(errorHandling.notFoundPage.rendered), + }, + serverErrorPage: { + rendered: rewriteEdgeHostInResponse(errorHandling.serverErrorPage.rendered), + }, + }; + }) .catch((e) => Promise.reject(e)); } diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 7a4268d8ac..87d8cc62a5 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -6,6 +6,15 @@ export * from './metadata'; export { default as isServer } from './is-server'; export { ensurePathExists } from './ensurePath'; export { normalizeUrl } from './normalize-url'; +export { + resolveEdgeUrl, + hasCustomEdgeHostname, + getCustomEdgeUrl, + SITECORE_EDGE_HOSTNAME_ENV, + SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, + SITECORE_EDGE_URL_ENV, + SITECORE_EDGE_URL_PUBLIC_ENV, +} from './resolve-edge-url'; export { resolveUrl, isTimeoutError, diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts new file mode 100644 index 0000000000..a76fb11ca6 --- /dev/null +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -0,0 +1,159 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { + resolveEdgeUrl, + hasCustomEdgeHostname, + getCustomEdgeUrl, + SITECORE_EDGE_HOSTNAME_ENV, + SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, + SITECORE_EDGE_URL_ENV, + SITECORE_EDGE_URL_PUBLIC_ENV, +} from './resolve-edge-url'; +import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; + +describe('resolveEdgeUrl', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all relevant env vars before each test + delete process.env[SITECORE_EDGE_HOSTNAME_ENV]; + delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + delete process.env[SITECORE_EDGE_URL_ENV]; + delete process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + }); + + afterEach(() => { + // Restore original env + process.env = { ...originalEnv }; + }); + + describe('resolveEdgeUrl()', () => { + it('should return explicit edgeUrl parameter when provided', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const result = resolveEdgeUrl('https://explicit.example.com'); + expect(result).to.equal('https://explicit.example.com'); + }); + + it('should normalize trailing slash from explicit edgeUrl', () => { + const result = resolveEdgeUrl('https://explicit.example.com/'); + expect(result).to.equal('https://explicit.example.com'); + }); + + it('should use SITECORE_EDGE_HOSTNAME when set (hostname only)', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'my-tenant.edge.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://my-tenant.edge.example.com'); + }); + + it('should use SITECORE_EDGE_HOSTNAME when set (full URL)', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://my-tenant.edge.example.com'); + }); + + it('should normalize trailing slash from SITECORE_EDGE_HOSTNAME', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com/'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://my-tenant.edge.example.com'); + }); + + it('should trim whitespace from SITECORE_EDGE_HOSTNAME', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = ' my-tenant.edge.example.com '; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://my-tenant.edge.example.com'); + }); + + it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME as fallback', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'public-tenant.edge.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://public-tenant.edge.example.com'); + }); + + it('should prefer SITECORE_EDGE_HOSTNAME over NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'server-tenant.edge.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'public-tenant.edge.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://server-tenant.edge.example.com'); + }); + + it('should use SITECORE_EDGE_URL when no hostname is set', () => { + process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-url.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://edge-url.example.com'); + }); + + it('should use NEXT_PUBLIC_SITECORE_EDGE_URL as fallback', () => { + process.env[SITECORE_EDGE_URL_PUBLIC_ENV] = 'https://public-edge-url.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://public-edge-url.example.com'); + }); + + it('should prefer hostname env var over URL env var', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'hostname.example.com'; + process.env[SITECORE_EDGE_URL_ENV] = 'https://url.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('https://hostname.example.com'); + }); + + it('should return default when no env vars are set', () => { + const result = resolveEdgeUrl(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + + it('should treat the string "undefined" as an unset hostname env var', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'undefined'; + const result = resolveEdgeUrl(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + + it('should treat the string "null" as an unset hostname env var', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'null'; + const result = resolveEdgeUrl(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + + it('should treat whitespace-only hostname env var as unset', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = ' '; + const result = resolveEdgeUrl(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + + it('should handle http protocol in hostname', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'http://insecure.example.com'; + const result = resolveEdgeUrl(); + expect(result).to.equal('http://insecure.example.com'); + }); + }); + + describe('hasCustomEdgeHostname()', () => { + it('should return false when no hostname env vars are set', () => { + expect(hasCustomEdgeHostname()).to.be.false; + }); + + it('should return true when SITECORE_EDGE_HOSTNAME is set', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + expect(hasCustomEdgeHostname()).to.be.true; + }); + + it('should return true when NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME is set', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + expect(hasCustomEdgeHostname()).to.be.true; + }); + + it('should return false when only SITECORE_EDGE_URL is set', () => { + process.env[SITECORE_EDGE_URL_ENV] = 'https://url.example.com'; + expect(hasCustomEdgeHostname()).to.be.false; + }); + }); + + describe('getCustomEdgeUrl()', () => { + it('should return undefined when no custom hostname is configured', () => { + expect(getCustomEdgeUrl()).to.be.undefined; + }); + + it('should return resolved URL when custom hostname is configured', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + expect(getCustomEdgeUrl()).to.equal('https://custom.example.com'); + }); + }); +}); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts new file mode 100644 index 0000000000..585624479f --- /dev/null +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -0,0 +1,163 @@ +import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; +import { normalizeUrl } from './normalize-url'; + +/** + * Environment variable name for the custom Edge hostname (server-side). + * When set, this hostname replaces the default Edge Platform hostname. + * @public + */ +export const SITECORE_EDGE_HOSTNAME_ENV = 'SITECORE_EDGE_HOSTNAME'; + +/** + * Environment variable name for the custom Edge hostname (client-side / browser). + * Required for Next.js client bundles where server-only env vars are not available. + * @public + */ +export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME'; + +/** + * Environment variable name for the Edge URL override. + * @public + */ +export const SITECORE_EDGE_URL_ENV = 'SITECORE_EDGE_URL'; + +/** + * Environment variable name for the Edge URL override (client-side / browser). + * @public + */ +export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; + +/** + * Resolves the Sitecore Edge URL based on environment variables and configuration. + * + * Priority order: + * 1. Explicit `edgeUrl` parameter (if provided and not empty) + * 2. `SITECORE_EDGE_HOSTNAME` / `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` environment variable + * 3. `SITECORE_EDGE_URL` / `NEXT_PUBLIC_SITECORE_EDGE_URL` environment variable + * 4. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) + * + * The hostname env var can be provided as: + * - Full URL: `https://my-custom-edge.example.com` + * - Hostname only: `my-custom-edge.example.com` (will be prefixed with `https://`) + * + * @param {string} [edgeUrl] - Optional explicit Edge URL to use (takes precedence if provided) + * @returns {string} The resolved Edge Platform base URL (normalized, no trailing slash) + * @public + * + * @example + * // With SITECORE_EDGE_HOSTNAME=my-tenant.edge.example.com + * resolveEdgeUrl() // => 'https://my-tenant.edge.example.com' + * + * @example + * // With explicit edgeUrl parameter + * resolveEdgeUrl('https://custom.edge.com') // => 'https://custom.edge.com' + * + * @example + * // With no env vars set (fallback to default) + * resolveEdgeUrl() // => 'https://edge-platform.sitecorecloud.io' + */ +export function resolveEdgeUrl(edgeUrl?: string): string { + // 1. Use explicit edgeUrl if provided and not empty + const explicit = normalizeMaybeEnvValue(edgeUrl); + if (explicit) { + return normalizeUrl(explicit); + } + + // Determine if we're in browser context + const isBrowser = typeof window !== 'undefined'; + + // 2. Check for custom hostname env var (prioritize custom hostname over URL) + const hostnameEnvVarRaw = isBrowser + ? process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] + : process.env[SITECORE_EDGE_HOSTNAME_ENV] || process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + + const hostnameEnvVar = normalizeMaybeEnvValue(hostnameEnvVarRaw); + if (hostnameEnvVar) { + return normalizeHostnameToUrl(hostnameEnvVar); + } + + // 3. Check for Edge URL env var + const urlEnvVarRaw = isBrowser + ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] + : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + + const urlEnvVar = normalizeMaybeEnvValue(urlEnvVarRaw); + if (urlEnvVar) { + return normalizeUrl(urlEnvVar); + } + + // 4. Fall back to default + return SITECORE_EDGE_URL_DEFAULT; +} + +/** + * Normalizes a hostname or URL to a full HTTPS URL without trailing slash. + * + * @param {string} hostnameOrUrl - A hostname (e.g., 'my.domain.com') or full URL (e.g., 'https://my.domain.com') + * @returns {string} A normalized HTTPS URL + * @internal + */ +function normalizeHostnameToUrl(hostnameOrUrl: string): string { + const trimmed = hostnameOrUrl.trim(); + + // If it already has a protocol, normalize and return + if (trimmed.startsWith('https://') || trimmed.startsWith('http://')) { + return normalizeUrl(trimmed); + } + + // Otherwise, treat as hostname and add https:// + return normalizeUrl(`https://${trimmed}`); +} + +/** + * Normalizes values that may come from environment variables. + * In Node, setting `process.env.FOO = undefined` results in the string 'undefined', + * which should be treated as if the variable is not set. + * + * @param {string | undefined} value - Possibly undefined env-like value + * @returns {string | undefined} A usable string value, or undefined if not meaningful + * @internal + */ +function normalizeMaybeEnvValue(value: string | undefined): string | undefined { + if (!value) return undefined; + + const trimmed = value.trim(); + if (!trimmed) return undefined; + + const lowered = trimmed.toLowerCase(); + if (lowered === 'undefined' || lowered === 'null') return undefined; + + return trimmed; +} + +/** + * Checks if a custom Edge hostname is configured via environment variables. + * + * @returns {boolean} True if a custom hostname is configured + * @public + */ +export function hasCustomEdgeHostname(): boolean { + const isBrowser = typeof window !== 'undefined'; + + if (isBrowser) { + return !!normalizeMaybeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); + } + + return !!normalizeMaybeEnvValue( + process.env[SITECORE_EDGE_HOSTNAME_ENV] || process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] + ); +} + +/** + * Gets the custom Edge hostname if configured, otherwise returns undefined. + * + * @returns {string | undefined} The custom Edge URL if configured, undefined otherwise + * @public + */ +export function getCustomEdgeUrl(): string | undefined { + if (!hasCustomEdgeHostname()) { + return undefined; + } + + return resolveEdgeUrl(); +} diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index 9a6ac1643a..92d1410e54 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -19,6 +19,15 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= +# Custom Edge hostname configured in Sitecore Cloud Portal (optional, server-side). +# When set, overrides the default Edge Platform hostname (edge-platform.sitecorecloud.io). +# Can be a hostname (my-tenant.edge.example.com) or full URL (https://my-tenant.edge.example.com). +SITECORE_EDGE_HOSTNAME= + +# Custom Edge hostname for client-side use (optional). +# Required for browser-side features (CloudSDK, events) when using a custom hostname. +NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= + # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. # This should match the PAGES_PERSONALIZE_SCOPE environment variable for your connected XM Cloud Environment. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index 9a6ac1643a..92d1410e54 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -19,6 +19,15 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= +# Custom Edge hostname configured in Sitecore Cloud Portal (optional, server-side). +# When set, overrides the default Edge Platform hostname (edge-platform.sitecorecloud.io). +# Can be a hostname (my-tenant.edge.example.com) or full URL (https://my-tenant.edge.example.com). +SITECORE_EDGE_HOSTNAME= + +# Custom Edge hostname for client-side use (optional). +# Required for browser-side features (CloudSDK, events) when using a custom hostname. +NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= + # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. # This should match the PAGES_PERSONALIZE_SCOPE environment variable for your connected XM Cloud Environment. diff --git a/packages/nextjs/src/config/define-config.ts b/packages/nextjs/src/config/define-config.ts index 62f4b967e1..2dd3dd3a36 100644 --- a/packages/nextjs/src/config/define-config.ts +++ b/packages/nextjs/src/config/define-config.ts @@ -3,6 +3,7 @@ import { defineConfig as defineConfigCore, SitecoreConfigInput as SitecoreConfigInputCore, } from '@sitecore-content-sdk/content/config'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; /** * Provides default NextJs initial values from env variables for SitecoreConfig @@ -19,7 +20,7 @@ export const getNextFallbackConfig = (config?: SitecoreConfigInput): SitecoreCon contextId: config?.api?.edge?.contextId || '', clientContextId: config?.api?.edge?.clientContextId || process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: config?.api?.edge?.edgeUrl || process.env.NEXT_PUBLIC_SITECORE_EDGE_URL, + edgeUrl: resolveEdgeUrl(config?.api?.edge?.edgeUrl), }, local: { ...config?.api?.local, diff --git a/packages/react/src/components/SitecoreProvider.tsx b/packages/react/src/components/SitecoreProvider.tsx index 6c764f9829..cc2cc28eed 100644 --- a/packages/react/src/components/SitecoreProvider.tsx +++ b/packages/react/src/components/SitecoreProvider.tsx @@ -3,7 +3,7 @@ import React from 'react'; import fastDeepEqual from 'fast-deep-equal/es6/react'; import { Page } from '@sitecore-content-sdk/content/client'; import { SitecoreConfig } from '@sitecore-content-sdk/content/config'; -import { constants } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentMap } from './sharedTypes'; import { ImportMapImport } from './DesignLibrary/models'; @@ -77,7 +77,7 @@ export class SitecoreProvider extends React.Component< constructor(props: SitecoreProviderProps) { super(props); - // If any Edge ID is present but no edgeUrl, apply the default + // If any Edge ID is present but no edgeUrl, resolve using custom hostname or default let api = props.api; if ( (props.api?.edge?.contextId || props.api?.edge?.clientContextId) && @@ -87,7 +87,7 @@ export class SitecoreProvider extends React.Component< ...props.api, edge: { ...props.api.edge, - edgeUrl: constants.SITECORE_EDGE_URL_DEFAULT, + edgeUrl: resolveEdgeUrl(), }, }; } diff --git a/packages/search/src/search-service.ts b/packages/search/src/search-service.ts index f0425e50cd..e5691890e4 100644 --- a/packages/search/src/search-service.ts +++ b/packages/search/src/search-service.ts @@ -1,4 +1,5 @@ -import { NativeDataFetcher, constants } from '@sitecore-content-sdk/core'; +import { NativeDataFetcher } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { SearchDocument, PathsToStringProps } from './models'; import { debug } from './debug'; @@ -100,7 +101,7 @@ export class SearchService { private fetcher: NativeDataFetcher; constructor(private config: SearchServiceConfig) { - this.config.edgeUrl = this.config.edgeUrl || constants.SITECORE_EDGE_URL_DEFAULT; + this.config.edgeUrl = resolveEdgeUrl(this.config.edgeUrl); this.fetcher = new NativeDataFetcher({ debugger: debug, From a580791aa752e3d4b63f1819dca44424bf3e49b9 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 14:54:01 +0200 Subject: [PATCH 02/25] Fix failing test --- packages/nextjs/src/config/define-config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/define-config.test.ts b/packages/nextjs/src/config/define-config.test.ts index 0f052f12e0..c3bf3cf611 100644 --- a/packages/nextjs/src/config/define-config.test.ts +++ b/packages/nextjs/src/config/define-config.test.ts @@ -140,10 +140,10 @@ describe('defineConfig', () => { describe('config.api.edge.edgeUrl', () => { describe('environment variable is not set', () => { - it('should default to undefined', () => { + it('should default to Edge Platform URL', () => { defineConfigModule.defineConfig(defaultConfig()); const resultConfig = defineConfigCoreStub.getCalls()[0].args[0]; - expect(resultConfig.api?.edge?.edgeUrl).to.be.undefined; + expect(resultConfig.api?.edge?.edgeUrl).to.equal('https://edge-platform.sitecorecloud.io'); }); it('should use the value from the config', () => { From a72243caef285318cbd83f63c8fb9d70a47958b3 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 17:00:16 +0200 Subject: [PATCH 03/25] Fix error pages null handling + document custom Edge hostname env vars --- .../content/src/site/error-pages-service.ts | 27 +++++++++++++------ .../nextjs-app-router/.env.container.example | 6 +++++ .../nextjs-app-router/.env.remote.example | 7 ++--- .../templates/nextjs/.env.container.example | 6 +++++ .../src/templates/nextjs/.env.remote.example | 7 ++--- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/content/src/site/error-pages-service.ts b/packages/content/src/site/error-pages-service.ts index 1192e0ee91..00a4a90140 100644 --- a/packages/content/src/site/error-pages-service.ts +++ b/packages/content/src/site/error-pages-service.ts @@ -46,9 +46,17 @@ export interface ErrorPagesServiceConfig extends GraphQLServiceConfig { * @public */ export type ErrorPages = { - notFoundPage: { rendered: LayoutServiceData }; + /** + * Rendered 404 page layout. + * Can be null if the site has no error handling configured for the requested language. + */ + notFoundPage: { rendered: LayoutServiceData } | null; notFoundPagePath: string; - serverErrorPage: { rendered: LayoutServiceData }; + /** + * Rendered 500 page layout. + * Can be null if the site has no error handling configured for the requested language. + */ + serverErrorPage: { rendered: LayoutServiceData } | null; serverErrorPagePath: string; }; @@ -110,14 +118,17 @@ export class ErrorPagesService { // Rewrite Edge hostnames in error page layouts if custom hostname is configured const errorHandling = result.site.siteInfo.errorHandling; + const notFoundPage = errorHandling.notFoundPage?.rendered + ? { rendered: rewriteEdgeHostInResponse(errorHandling.notFoundPage.rendered) } + : null; + const serverErrorPage = errorHandling.serverErrorPage?.rendered + ? { rendered: rewriteEdgeHostInResponse(errorHandling.serverErrorPage.rendered) } + : null; + return { ...errorHandling, - notFoundPage: { - rendered: rewriteEdgeHostInResponse(errorHandling.notFoundPage.rendered), - }, - serverErrorPage: { - rendered: rewriteEdgeHostInResponse(errorHandling.serverErrorPage.rendered), - }, + notFoundPage, + serverErrorPage, }; }) .catch((e) => Promise.reject(e)); diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example index 07d7f83866..2a30f59747 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example @@ -17,6 +17,12 @@ NEXT_PUBLIC_SITECORE_API_KEY= # Your Sitecore API hostname is needed to build the app. NEXT_PUBLIC_SITECORE_API_HOST= +# Optional: custom Experience Edge hostname override (XM Cloud/Edge). +SITECORE_EDGE_HOSTNAME= + +# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). +NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= + # Sitecore Content SDK npm packages utilize the debug module for debug logging. # https://www.npmjs.com/package/debug # Set the DEBUG environment variable to 'content-sdk:*' to see all logs: diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index 92d1410e54..18bd18a59e 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -19,13 +19,10 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= -# Custom Edge hostname configured in Sitecore Cloud Portal (optional, server-side). -# When set, overrides the default Edge Platform hostname (edge-platform.sitecorecloud.io). -# Can be a hostname (my-tenant.edge.example.com) or full URL (https://my-tenant.edge.example.com). +# Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). SITECORE_EDGE_HOSTNAME= -# Custom Edge hostname for client-side use (optional). -# Required for browser-side features (CloudSDK, events) when using a custom hostname. +# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= # An optional Sitecore Personalize scope identifier. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example index 07d7f83866..2a30f59747 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example @@ -17,6 +17,12 @@ NEXT_PUBLIC_SITECORE_API_KEY= # Your Sitecore API hostname is needed to build the app. NEXT_PUBLIC_SITECORE_API_HOST= +# Optional: custom Experience Edge hostname override (XM Cloud/Edge). +SITECORE_EDGE_HOSTNAME= + +# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). +NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= + # Sitecore Content SDK npm packages utilize the debug module for debug logging. # https://www.npmjs.com/package/debug # Set the DEBUG environment variable to 'content-sdk:*' to see all logs: diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index 92d1410e54..18bd18a59e 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -19,13 +19,10 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= -# Custom Edge hostname configured in Sitecore Cloud Portal (optional, server-side). -# When set, overrides the default Edge Platform hostname (edge-platform.sitecorecloud.io). -# Can be a hostname (my-tenant.edge.example.com) or full URL (https://my-tenant.edge.example.com). +# Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). SITECORE_EDGE_HOSTNAME= -# Custom Edge hostname for client-side use (optional). -# Required for browser-side features (CloudSDK, events) when using a custom hostname. +# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= # An optional Sitecore Personalize scope identifier. From 52709e25bf3728c495b05948965dc60aec67c4fa Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 19:36:58 +0200 Subject: [PATCH 04/25] Fix failing api verification issue --- packages/core/api/content-sdk-core.api.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index fff87f5569..7e975c5038 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -141,6 +141,9 @@ export function getCache(key: string): T | undefined; // @internal export function getCacheAndClean(key: string): T | undefined; +// @public +export function getCustomEdgeUrl(): string | undefined; + // @public export const getEnforcedCorsHeaders: ({ requestMethod, headers, presetCorsHeader, allowedOrigins, }: { requestMethod: string | undefined; @@ -195,6 +198,9 @@ export type GraphQLRequestClientFactoryConfig = { // @internal export function hasCache(key: string): boolean; +// @public +export function hasCustomEdgeHostname(): boolean; + // @public export const isRegexOrUrl: (input: string) => "regex" | "url"; @@ -262,6 +268,9 @@ export interface NativeDataFetcherResponse { // @public export const normalizeUrl: (url: string) => string; +// @public +export function resolveEdgeUrl(edgeUrl?: string): string; + // @public export function resolveUrl(urlBase: string, params?: ParsedUrlQueryInput): string; @@ -274,9 +283,21 @@ export interface RetryStrategy { // @internal export function setCache(key: string, data: unknown): void; +// @public +export const SITECORE_EDGE_HOSTNAME_ENV = "SITECORE_EDGE_HOSTNAME"; + +// @public +export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = "NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME"; + // @internal const SITECORE_EDGE_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; +// @public +export const SITECORE_EDGE_URL_ENV = "SITECORE_EDGE_URL"; + +// @public +export const SITECORE_EDGE_URL_PUBLIC_ENV = "NEXT_PUBLIC_SITECORE_EDGE_URL"; + // @public export interface TenantArgs { audience?: string; @@ -290,7 +311,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:32:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) From 90871de134625f1540b472c4e237b77ea965f71e Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 20:16:59 +0200 Subject: [PATCH 05/25] Minor comment adjustments --- packages/content/src/layout/rewrite-edge-host.ts | 4 +--- packages/core/src/tools/resolve-edge-url.ts | 11 ++++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index 17d52587cd..1211f20514 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -7,7 +7,7 @@ import { hasCustomEdgeHostname, resolveEdgeUrl } from '@sitecore-content-sdk/cor */ const DEFAULT_EDGE_HOSTNAMES = [ 'edge-platform.sitecorecloud.io', - 'edge.sitecorecloud.io', // Legacy hostname, included for defensive replacement + 'edge.sitecorecloud.io', // Legacy hostname included for safety replacement ]; /** @@ -31,10 +31,8 @@ const EDGE_HOST_PATTERNS = DEFAULT_EDGE_HOSTNAMES.map( * @public * * @example - * // With SITECORE_EDGE_HOSTNAME=my-tenant.edge.example.com * const layout = await layoutService.fetchLayoutData(path, options); * const rewritten = rewriteEdgeHostInResponse(layout); - * // All URLs like 'https://edge-platform.sitecorecloud.io/...' are now 'https://my-tenant.edge.example.com/...' */ export function rewriteEdgeHostInResponse(response: T): T { // Skip if no custom hostname is configured diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index 585624479f..f028176c73 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -45,19 +45,16 @@ export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; * @public * * @example - * // With SITECORE_EDGE_HOSTNAME=my-tenant.edge.example.com * resolveEdgeUrl() // => 'https://my-tenant.edge.example.com' * * @example - * // With explicit edgeUrl parameter * resolveEdgeUrl('https://custom.edge.com') // => 'https://custom.edge.com' * * @example - * // With no env vars set (fallback to default) * resolveEdgeUrl() // => 'https://edge-platform.sitecorecloud.io' */ export function resolveEdgeUrl(edgeUrl?: string): string { - // 1. Use explicit edgeUrl if provided and not empty + // Use explicit edgeUrl if provided and not empty const explicit = normalizeMaybeEnvValue(edgeUrl); if (explicit) { return normalizeUrl(explicit); @@ -66,7 +63,7 @@ export function resolveEdgeUrl(edgeUrl?: string): string { // Determine if we're in browser context const isBrowser = typeof window !== 'undefined'; - // 2. Check for custom hostname env var (prioritize custom hostname over URL) + // Check for custom hostname env var (prioritize custom hostname over URL) const hostnameEnvVarRaw = isBrowser ? process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] : process.env[SITECORE_EDGE_HOSTNAME_ENV] || process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; @@ -76,7 +73,7 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeHostnameToUrl(hostnameEnvVar); } - // 3. Check for Edge URL env var + // Check for Edge URL env var const urlEnvVarRaw = isBrowser ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; @@ -86,7 +83,7 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeUrl(urlEnvVar); } - // 4. Fall back to default + // Fall back to default return SITECORE_EDGE_URL_DEFAULT; } From 6fb96ae61d7b6533f78abd1f0ab476640e5e9cb5 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 20:26:56 +0200 Subject: [PATCH 06/25] Adjust CHANGELOG.md --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c6b07289..a7a5dcec97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,8 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements * `[nextjs]` `[create-content-sdk-app]` Enable Next.js 16 Cache Components and Turbopack File System Caching ([#334](https://github.com/Sitecore/content-sdk/pull/334)) - - Enabled `cacheComponents: true` for explicit caching with "use cache" directive - - Enabled `experimental.turbopackFileSystemCacheForDev: true` for faster dev startup (beta) - - Available in both Pages Router and App Router templates + +* `[core]` `[content]` `[nextjs]` Support custom Edge hostnames via `SITECORE_EDGE_HOSTNAME` / `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` ([#359](https://github.com/Sitecore/content-sdk/pull/359)) * Search integration ([#295](https://github.com/Sitecore/content-sdk/pull/295)) * `[search]` New `@sitecore-content-sdk/search` package providing search functionality From 6224c9cb5f6d97d29a866505819689f7f603538c Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 20:33:42 +0200 Subject: [PATCH 07/25] Fix regex analysis issue --- .../content/src/layout/rewrite-edge-host.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index 1211f20514..d934ce3a98 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -16,20 +16,27 @@ const DEFAULT_EDGE_HOSTNAMES = [ * @internal */ const EDGE_HOST_PATTERNS = DEFAULT_EDGE_HOSTNAMES.map( - (hostname) => new RegExp(`https?://${hostname.replace(/\./g, '\\.')}`, 'gi') + (hostname) => new RegExp(`https?://${escapeRegExp(hostname)}`, 'gi') ); +/** + * Escapes a string so it can be safely embedded in a RegExp as a literal. + * @param {string} input - The string to escape + * @returns {string} The escaped string safe for RegExp construction + * @internal + */ +function escapeRegExp(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** * Rewrites Edge Platform hostnames in a response object to use the custom hostname. * This function performs a deep traversal of the object and replaces any string values * containing the default Edge hostnames with the custom hostname. - * * Only performs rewriting when a custom Edge hostname is configured via environment variables. - * * @param {T} response - The response object to process (typically LayoutServiceData) * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) * @public - * * @example * const layout = await layoutService.fetchLayoutData(path, options); * const rewritten = rewriteEdgeHostInResponse(layout); @@ -47,7 +54,6 @@ export function rewriteEdgeHostInResponse(response: T): T { /** * Recursively traverses an object/array and rewrites Edge hostnames in string values. - * * @param {T} value - The value to process * @param {string} customEdgeUrl - The custom Edge URL to replace with * @returns {T} The processed value with Edge hostnames replaced @@ -89,7 +95,6 @@ function deepRewriteEdgeHost(value: T, customEdgeUrl: string): T { /** * Replaces Edge Platform hostnames in a string with the custom hostname. - * * @param {string} str - The string to process * @param {string} customEdgeUrl - The custom Edge URL to replace with * @returns {string} The string with Edge hostnames replaced @@ -109,7 +114,6 @@ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { /** * Checks if a string contains any default Edge Platform hostnames. - * * @param {string} str - The string to check * @returns {boolean} True if the string contains a default Edge hostname * @public From 45d93445b4b3619ab4124bf5905352706e312c73 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 2 Feb 2026 20:38:16 +0200 Subject: [PATCH 08/25] Fix verify api extractor issue --- packages/search/api/content-sdk-search.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/api/content-sdk-search.api.md b/packages/search/api/content-sdk-search.api.md index cbe51a9814..b1e696f2eb 100644 --- a/packages/search/api/content-sdk-search.api.md +++ b/packages/search/api/content-sdk-search.api.md @@ -54,7 +54,7 @@ export type SortSetting = { // Warnings were encountered during analysis: // // src/models.ts:8:3 - (ae-forgotten-export) The symbol "PrimitiveType" needs to be exported by the entry point index.d.ts -// src/search-service.ts:10:3 - (ae-forgotten-export) The symbol "PathsToStringProps" needs to be exported by the entry point index.d.ts +// src/search-service.ts:11:3 - (ae-forgotten-export) The symbol "PathsToStringProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From 64062b0be0716b0ad3208caddc8d3930092880b9 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 3 Feb 2026 14:36:16 +0200 Subject: [PATCH 09/25] Fix api verification issue --- packages/content/api/content-sdk-content.api.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index b77411b9d8..da1268aaa4 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -183,6 +183,9 @@ export interface ComponentUpdateEventArgs { name: string; } +// @public +export function containsDefaultEdgeHost(str: string): boolean; + // @internal export const createComponentInstance: (importMap: ImportEntry[], previewEventArgs: ComponentPreviewEventArgs) => unknown; @@ -416,11 +419,11 @@ export enum ErrorPage { export type ErrorPages = { notFoundPage: { rendered: LayoutServiceData; - }; + } | null; notFoundPagePath: string; serverErrorPage: { rendered: LayoutServiceData; - }; + } | null; serverErrorPagePath: string; }; @@ -976,6 +979,9 @@ export const resetEditorChromes: () => void; export { RetryStrategy } +// @public +export function rewriteEdgeHostInResponse(response: T): T; + // @public export type RobotsQueryResult = { site: { @@ -1338,7 +1344,7 @@ export type WriteImportMapArgsInternal = WriteImportMapArgs & { // Warnings were encountered during analysis: // -// src/client/sitecore-client.ts:58:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts +// src/client/sitecore-client.ts:59:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts // src/editing/codegen/preview.ts:108:5 - (ae-forgotten-export) The symbol "ComponentImport_2" needs to be exported by the entry point api-surface.d.ts // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "ComponentMapTemplate" which is marked as @internal // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "EnhancedComponentMapTemplate" which is marked as @internal From 49ba70ed24032d2f30a5632a3bdc62e31c3554af Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 10 Feb 2026 18:34:32 +0200 Subject: [PATCH 10/25] Add opt-in layout URL rewrite for media content --- .../content/api/content-sdk-content.api.md | 14 +- .../src/client/sitecore-client.test.ts | 41 +++++ .../content/src/client/sitecore-client.ts | 76 +++++--- packages/content/src/config/define-config.ts | 3 + packages/content/src/config/models.ts | 13 ++ .../src/editing/component-layout-service.ts | 6 +- .../content/src/editing/editing-service.ts | 5 +- .../src/layout/content-rewrite.test.ts | 162 ++++++++++++++++++ .../content/src/layout/content-rewrite.ts | 121 +++++++++++++ packages/content/src/layout/index.ts | 5 + packages/content/src/layout/layout-service.ts | 4 +- .../src/layout/rewrite-edge-host.test.ts | 24 +++ .../content/src/layout/rewrite-edge-host.ts | 3 + .../content/src/site/error-pages-service.ts | 7 +- packages/core/api/content-sdk-core.api.md | 5 +- packages/core/src/tools/index.ts | 1 + .../core/src/tools/resolve-edge-url.test.ts | 27 +++ packages/core/src/tools/resolve-edge-url.ts | 27 ++- .../nextjs-app-router/sitecore.config.ts | 5 +- .../src/templates/nextjs/sitecore.config.ts | 5 +- 20 files changed, 503 insertions(+), 51 deletions(-) create mode 100644 packages/content/src/layout/content-rewrite.test.ts create mode 100644 packages/content/src/layout/content-rewrite.ts diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index da1268aaa4..efff5d34d3 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -186,6 +186,11 @@ export interface ComponentUpdateEventArgs { // @public export function containsDefaultEdgeHost(str: string): boolean; +// @public +export interface ContentRewriteOptions { + type?: 'prefix' | 'normal'; +} + // @internal export const createComponentInstance: (importMap: ImportEntry[], previewEventArgs: ComponentPreviewEventArgs) => unknown; @@ -979,6 +984,9 @@ export const resetEditorChromes: () => void; export { RetryStrategy } +// @public +export function rewriteContentInLayout(layout: LayoutServiceData, source: string | RegExp, target: string, options?: ContentRewriteOptions): LayoutServiceData; + // @public export function rewriteEdgeHostInResponse(response: T): T; @@ -1092,6 +1100,8 @@ export type SitecoreCliConfigInput = { // @public export class SitecoreClient implements BaseSitecoreClient { constructor(initOptions: SitecoreClientInit); + // @internal + protected applyContentRewrite(layout: LayoutServiceData): LayoutServiceData; // (undocumented) protected clientFactory: GraphQLRequestClientFactory; // (undocumented) @@ -1195,6 +1205,8 @@ export type SitecoreConfigInput = { enabled?: boolean; locales?: string[]; }; + rewriteContentUrls?: boolean; + contentRewrite?: (layout: unknown) => unknown; disableCodeGeneration?: boolean; }; @@ -1344,7 +1356,7 @@ export type WriteImportMapArgsInternal = WriteImportMapArgs & { // Warnings were encountered during analysis: // -// src/client/sitecore-client.ts:59:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts +// src/client/sitecore-client.ts:60:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts // src/editing/codegen/preview.ts:108:5 - (ae-forgotten-export) The symbol "ComponentImport_2" needs to be exported by the entry point api-surface.d.ts // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "ComponentMapTemplate" which is marked as @internal // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "EnhancedComponentMapTemplate" which is marked as @internal diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index c407f665e6..a66f22aba3 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -484,6 +484,38 @@ describe('SitecoreClient', () => { }); }); + it('should apply content rewrite when rewriteContentUrls is true', async () => { + const path = '/test/path'; + const locale = 'en-US'; + const siteInfo = { name: 'default-site', hostName: 'example.com', language: 'en' }; + const rawLayout = { + sitecore: { + route: { name: 'home', placeholders: {} }, + context: { site: siteInfo, pageState: LayoutServicePageState.Normal }, + }, + }; + layoutServiceStub.fetchLayoutData.returns(rawLayout); + const customRewriter = (layout: LayoutServiceData): LayoutServiceData => ({ + ...layout, + sitecore: { + ...layout.sitecore, + route: layout.sitecore.route + ? { ...layout.sitecore.route, displayName: 'rewritten' } + : null, + }, + }); + const clientWithRewrite = new SitecoreClient({ + ...defaultInitOptions, + rewriteContentUrls: true, + contentRewrite: customRewriter as any, + } as any); + (clientWithRewrite as any).layoutService = layoutServiceStub; + + const result = await clientWithRewrite.getPage(path, { locale }); + + expect(result?.layout.sitecore.route?.displayName).to.equal('rewritten'); + }); + it('should pass fetchOptions to layoutService when calling getPage', async () => { const path = '/test/path'; const locale = 'en-US'; @@ -1309,6 +1341,15 @@ describe('SitecoreClient', () => { }); describe('getHeadLinks', function () { + const SITECORE_EDGE_URL_ENV = 'SITECORE_EDGE_URL'; + + beforeEach(() => { + process.env[SITECORE_EDGE_URL_ENV] = 'https://edge.example.com'; + }); + afterEach(() => { + delete process.env[SITECORE_EDGE_URL_ENV]; + }); + const truthyValue = { value: '

bar

', }; diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 0b05d22086..9e2f491cde 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -7,6 +7,7 @@ import { NativeDataFetcher, debug, } from '@sitecore-content-sdk/core'; +import { resolveEdgeUrlForStaticFiles } from '@sitecore-content-sdk/core/tools'; import { DictionaryPhrases, DictionaryService } from '../i18n'; import { getDesignLibraryStylesheetLinks, @@ -348,7 +349,7 @@ export class SitecoreClient implements BaseSitecoreClient { const locale = pageOptions?.locale ?? this.initOptions.defaultLanguage; const site = pageOptions?.site ?? this.initOptions.defaultSite; // Fetch layout data, passing on req/res for SSR - const layout = await this.layoutService.fetchLayoutData( + let layout = await this.layoutService.fetchLayoutData( computedPath, { locale, @@ -358,25 +359,25 @@ export class SitecoreClient implements BaseSitecoreClient { ); if (!layout.sitecore.route) { return null; - } else { - // Initialize links to be inserted on the page - if (pageOptions?.personalize?.variantId) { - // Modify layoutData to use specific variant(s) instead of default - // This will also set the variantId on the Sitecore context so that it is accessible here - personalizeLayout( - layout, - pageOptions.personalize.variantId, - pageOptions.personalize.componentVariantIds - ); - } - - return { + } + layout = this.applyContentRewrite(layout); + // Initialize links to be inserted on the page + if (pageOptions?.personalize?.variantId) { + // Modify layoutData to use specific variant(s) instead of default + // This will also set the variantId on the Sitecore context so that it is accessible here + personalizeLayout( layout, - siteName: layout.sitecore.context.site?.name || site, - locale, - mode: this.getPageMode(LayoutServicePageState.Normal), - }; + pageOptions.personalize.variantId, + pageOptions.personalize.componentVariantIds + ); } + + return { + layout, + siteName: layout.sitecore.context.site?.name || site, + locale, + mode: this.getPageMode(LayoutServicePageState.Normal), + }; } /** @@ -392,18 +393,20 @@ export class SitecoreClient implements BaseSitecoreClient { options: { enableStyles?: boolean; enableThemes?: boolean } = {} ): HTMLLink[] { const { enableStyles = true, enableThemes = true } = options; - const { contextId: serverContextId, clientContextId, edgeUrl } = this.initOptions.api.edge; + const { contextId: serverContextId, clientContextId } = this.initOptions.api.edge; const headLinks: HTMLLink[] = []; const contextId = serverContextId || clientContextId; + // Use default Edge URL for styles (ignore custom hostname) so stylesheets load from platform + const edgeUrlForStyles = resolveEdgeUrlForStaticFiles(); if (enableStyles) { - const contentStyles = getContentStylesheetLink(layoutData, contextId, edgeUrl); + const contentStyles = getContentStylesheetLink(layoutData, contextId, edgeUrlForStyles); if (contentStyles) headLinks.push(contentStyles); } if (enableThemes) { - headLinks.push(...getDesignLibraryStylesheetLinks(layoutData, contextId, edgeUrl)); + headLinks.push(...getDesignLibraryStylesheetLinks(layoutData, contextId, edgeUrlForStyles)); } return headLinks; @@ -474,10 +477,11 @@ export class SitecoreClient implements BaseSitecoreClient { if (!data) { throw new Error(`Unable to fetch editing data for preview ${JSON.stringify(previewData)}`); } + const layout = this.applyContentRewrite(data.layoutData); const page: Page = { locale: language, - layout: data.layoutData, - siteName: data.layoutData.sitecore.context.site?.name || site, + layout, + siteName: layout.sitecore.context.site?.name || site, mode: this.getPageMode(mode), }; const personalizeData = getGroomedVariantIds(variantIds); @@ -530,10 +534,11 @@ export class SitecoreClient implements BaseSitecoreClient { if (!componentData) { throw new Error(`Unable to fetch editing data for preview ${JSON.stringify(designLibData)}`); } + const layout = this.applyContentRewrite(componentData); const page: Page = { locale: designLibData.language, - layout: componentData, - siteName: componentData.sitecore.context.site?.name || site, + layout, + siteName: layout.sitecore.context.site?.name || site, mode: this.getPageMode(mode, generation), }; return page; @@ -562,7 +567,7 @@ export class SitecoreClient implements BaseSitecoreClient { fetchOptions ); - let layout = null; + let layout: LayoutServiceData | null = null; switch (code) { case ErrorPage.NotFound: @@ -579,6 +584,8 @@ export class SitecoreClient implements BaseSitecoreClient { return null; } + layout = this.applyContentRewrite(layout); + return { layout, locale, @@ -703,6 +710,23 @@ export class SitecoreClient implements BaseSitecoreClient { * @param { DesignLibraryVariantGeneration} generation - The variant generation mode, if applicable * @returns {PageMode} The page mode */ + /** + * Applies content URL rewrite when rewriteContentUrls is enabled (opt-in). + * Uses contentRewrite from config or the default Edge host rewriter. + * @param {LayoutServiceData} layout - Layout data from layout/editing/component/error service + * @returns {LayoutServiceData} Rewritten layout (or same reference if rewrite disabled) + * @internal + */ + protected applyContentRewrite(layout: LayoutServiceData): LayoutServiceData { + if (!this.initOptions.rewriteContentUrls) { + return layout; + } + const rewriter = this.initOptions.contentRewrite as ( + l: LayoutServiceData + ) => LayoutServiceData; + return rewriter(layout); + } + private getPageMode(mode: PageModeName, generation?: DesignLibraryVariantGeneration): PageMode { const pageMode: PageMode = { name: mode, diff --git a/packages/content/src/config/define-config.ts b/packages/content/src/config/define-config.ts index d775522028..7277f9ff41 100644 --- a/packages/content/src/config/define-config.ts +++ b/packages/content/src/config/define-config.ts @@ -2,6 +2,7 @@ import { DefaultRetryStrategy } from '@sitecore-content-sdk/core'; import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { DeepPartial, SitecoreConfig, SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; +import { rewriteEdgeHostInResponse } from '../layout'; /** * Provides default initial values for SitecoreConfig @@ -54,6 +55,8 @@ export const getFallbackConfig = (): SitecoreConfig => ({ timeout: 60, }, }, + rewriteContentUrls: false, + contentRewrite: rewriteEdgeHostInResponse, disableCodeGeneration: false, }); diff --git a/packages/content/src/config/models.ts b/packages/content/src/config/models.ts index 99fbc63c27..41d3fe2ee0 100644 --- a/packages/content/src/config/models.ts +++ b/packages/content/src/config/models.ts @@ -203,6 +203,19 @@ export type SitecoreConfigInput = { */ locales?: string[]; }; + /** + * Opt-in: rewrite URLs in layout content (media fields, rich text with img/src, href, etc.) + * to use the custom hostname. When true, uses the default rewriter (Edge host -> custom host) + * or contentRewrite if provided. Off by default to avoid unintended rewrites. + */ + rewriteContentUrls?: boolean; + /** + * Optional custom content rewriter. When rewriteContentUrls is true and this is set, + * it is used instead of the default. Use for custom rules (e.g. only certain fields or URL shapes). + * @param layout - Layout service data + * @returns Rewritten layout (do not mutate input) + */ + contentRewrite?: (layout: unknown) => unknown; /** * Opt-out setting for code generation feature * Disables code extraction procedure diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index dedf572b9a..013010710f 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -1,6 +1,6 @@ import { NativeDataFetcher, FetchOptions } from '@sitecore-content-sdk/core'; import { resolveUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; -import { LayoutServiceData, rewriteEdgeHostInResponse } from '../layout'; +import { LayoutServiceData } from '../layout'; import debug from '../debug'; import { DesignLibraryMode, DesignLibraryVariantGeneration } from './models'; @@ -111,10 +111,10 @@ export class ComponentLayoutService { sc_editMode: `${params.mode === DesignLibraryMode.Metadata}`, }, }) - .then((response) => rewriteEdgeHostInResponse(response.data)) + .then((response) => response.data) .catch((error) => { if (error.response?.status === 404) { - return rewriteEdgeHostInResponse(error.response.data); + return error.response.data; } throw error; }); diff --git a/packages/content/src/editing/editing-service.ts b/packages/content/src/editing/editing-service.ts index 704b9370de..6f3d04fb52 100644 --- a/packages/content/src/editing/editing-service.ts +++ b/packages/content/src/editing/editing-service.ts @@ -1,6 +1,6 @@ import { GraphQLClient, GraphQLRequestClientFactory, FetchOptions } from '@sitecore-content-sdk/core'; import debug from '../debug'; -import { LayoutServiceData, LayoutServicePageState, rewriteEdgeHostInResponse } from '../layout'; +import { LayoutServiceData, LayoutServicePageState } from '../layout'; import { LayoutKind } from './models'; /** @@ -107,9 +107,8 @@ export class EditingService { }, }; - // Rewrite Edge hostnames in response if custom hostname is configured return { - layoutData: rewriteEdgeHostInResponse(layoutData), + layoutData, }; } diff --git a/packages/content/src/layout/content-rewrite.test.ts b/packages/content/src/layout/content-rewrite.test.ts new file mode 100644 index 0000000000..782797afee --- /dev/null +++ b/packages/content/src/layout/content-rewrite.test.ts @@ -0,0 +1,162 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { rewriteContentInLayout } from './content-rewrite'; +import { LayoutServiceData } from './models'; + +describe('rewriteContentInLayout', () => { + it('should replace string source with target in all string values (type normal)', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: {}, + fields: { + image: { + value: { + src: 'https://edge.example.com/media/image.jpg', + }, + }, + }, + }, + }, + }; + const result = rewriteContentInLayout( + layout, + 'https://edge.example.com', + 'https://custom.example.com', + { type: 'normal' } + ); + expect(result.sitecore.route!.fields!.image.value.src).to.equal( + 'https://custom.example.com/media/image.jpg' + ); + }); + + it('should replace in rich text (HTML with img src)', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: { + main: [ + { + componentName: 'RichText', + fields: { + content: { + value: + '

and link

', + }, + }, + }, + ], + }, + }, + }, + }; + const result = rewriteContentInLayout( + layout, + 'https://edge.example.com', + 'https://cdn.example.com', + { type: 'normal' } + ); + expect(result.sitecore.route!.placeholders!.main[0].fields!.content.value).to.equal( + '

and link

' + ); + }); + + it('should replace with RegExp source', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: {}, + fields: { + link: { value: { href: 'https://edge-staging.sitecore-staging.cloud/path' } }, + }, + }, + }, + }; + const result = rewriteContentInLayout( + layout, + /https?:\/\/edge(-staging)?\.sitecore[^/]+/gi, + 'https://custom.example.com', + { type: 'normal' } + ); + expect((result.sitecore.route!.fields!.link.value as { href: string }).href).to.equal( + 'https://custom.example.com/path' + ); + }); + + it('should not mutate input layout', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: {}, + fields: { url: { value: 'https://old.com/path' } }, + }, + }, + }; + const original = (layout.sitecore.route!.fields as { url: { value: string } }).url.value; + rewriteContentInLayout(layout, 'https://old.com', 'https://new.com'); + expect((layout.sitecore.route!.fields as { url: { value: string } }).url.value).to.equal( + original + ); + }); + + it('should default to type normal', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: {}, + fields: { u: { value: 'https://a.com/xhttps://a.com/y' } }, + }, + }, + }; + const result = rewriteContentInLayout(layout, 'https://a.com', 'https://b.com'); + expect((result.sitecore.route!.fields as { u: { value: string } }).u.value).to.equal( + 'https://b.com/xhttps://b.com/y' + ); + }); + + it('should handle type prefix (string source)', () => { + const layout: LayoutServiceData = { + sitecore: { + context: {}, + route: { + name: 'test', + placeholders: {}, + fields: { + a: { value: 'https://edge.com/path' }, + b: { value: 'see https://edge.com/other' }, + }, + }, + }, + }; + const result = rewriteContentInLayout( + layout, + 'https://edge.com', + 'https://custom.com', + { type: 'prefix' } + ); + expect((result.sitecore.route!.fields as { a: { value: string }; b: { value: string } }).a.value).to.equal( + 'https://custom.com/path' + ); + expect((result.sitecore.route!.fields as { a: { value: string }; b: { value: string } }).b.value).to.equal( + 'see https://edge.com/other' + ); + }); + + it('should handle null route', () => { + const layout: LayoutServiceData = { + sitecore: { context: {}, route: null }, + }; + const result = rewriteContentInLayout(layout, 'https://old.com', 'https://new.com'); + expect(result.sitecore.route).to.be.null; + }); +}); diff --git a/packages/content/src/layout/content-rewrite.ts b/packages/content/src/layout/content-rewrite.ts new file mode 100644 index 0000000000..fb42e3f764 --- /dev/null +++ b/packages/content/src/layout/content-rewrite.ts @@ -0,0 +1,121 @@ +import { LayoutServiceData } from './models'; + +/** + * Options for content URL rewriting. + * Affects how source is matched in string values (e.g. URL fields, rich text with img/src, href). + * @public + */ +export interface ContentRewriteOptions { + /** + * - 'normal': replace every occurrence of source with target in each string. + * - 'prefix': replace only when the string starts with source (e.g. URL prefix). + * @default 'normal' + */ + type?: 'prefix' | 'normal'; +} + +/** + * Performs a single string replacement according to options. + * @param {string} str - String to process + * @param {string | RegExp} source - String or RegExp to replace + * @param {string} target - Replacement string + * @param {ContentRewriteOptions} options - Rewrite options + * @returns {string} Rewritten string + * @internal + */ +function replaceInString( + str: string, + source: string | RegExp, + target: string, + options: ContentRewriteOptions +): string { + if (options.type === 'prefix' && typeof source === 'string') { + return str.startsWith(source) ? target + str.slice(source.length) : str; + } + if (options.type === 'prefix' && source instanceof RegExp) { + const match = str.match(source); + if (match && match.index === 0) { + return str.replace(source, target); + } + return str; + } + // normal: replace all + if (typeof source === 'string') { + return str.split(source).join(target); + } + return str.replace(source, target); +} + +/** + * Deep traversal that replaces source with target in every string value. + * Covers URL fields (e.g. field.value.src), rich text (HTML with img src, a href), and any other string in the layout. + * @param {T} value - Any value (layout, object, array, string) + * @param {string | RegExp} source - String or RegExp to replace (e.g. Edge host URL or pattern) + * @param {string} target - Replacement string (e.g. custom host URL) + * @param {ContentRewriteOptions} options - Rewrite options + * @returns {T} New value with replacements applied + * @internal + */ +function deepReplace( + value: T, + source: string | RegExp, + target: string, + options: ContentRewriteOptions +): T { + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string') { + return replaceInString(value, source, target, options) as T; + } + + if (Array.isArray(value)) { + return value.map((item) => deepReplace(item, source, target, options)) as T; + } + + if (typeof value === 'object') { + if (Object.getPrototypeOf(value) !== Object.prototype) { + return value; + } + const result: Record = {}; + for (const key of Object.keys(value as Record)) { + result[key] = deepReplace( + (value as Record)[key], + source, + target, + options + ); + } + return result as T; + } + + return value; +} + +/** + * Rewrites content URLs in layout data by replacing source with target in every string value. + * Use for media URLs (Image field value.src), rich text (HTML with <img src="...">, <a href="...">), and link fields. + * This is an unopinionated helper: it replaces in all strings, so use a specific source/target to avoid unintended rewrites. + * @param {LayoutServiceData} layout - Layout service data (route, placeholders, component fields) + * @param {string | RegExp} source - String or RegExp to replace (e.g. 'https://edge-staging.sitecore-staging.cloud' or regex) + * @param {string} target - Replacement string (e.g. 'https://custom.example.com') + * @param {ContentRewriteOptions} options - Optional. type 'normal' (default) replaces all occurrences; 'prefix' replaces only at string start. + * @returns {LayoutServiceData} New layout with replacements applied (does not mutate input) + * @public + * @example + * // Replace Edge staging host with custom host in all content (URLs and rich text) + * const rewritten = rewriteContentInLayout(layout, 'https://edge-staging.sitecore-staging.cloud', 'https://my-cdn.example.com'); + * @example + * // Replace with regex (e.g. any Edge host) + * const rewritten = rewriteContentInLayout(layout, /https?:\/\/edge(-staging)?\.sitecore[^/]+/gi, 'https://custom.example.com'); + */ +export function rewriteContentInLayout( + layout: LayoutServiceData, + source: string | RegExp, + target: string, + options: ContentRewriteOptions = {} +): LayoutServiceData { + const opts: ContentRewriteOptions = { type: 'normal', ...options }; + return deepReplace(layout, source, target, opts); +} diff --git a/packages/content/src/layout/index.ts b/packages/content/src/layout/index.ts index e2702f92f8..de2a6bc2fd 100644 --- a/packages/content/src/layout/index.ts +++ b/packages/content/src/layout/index.ts @@ -37,3 +37,8 @@ export { LayoutService, LayoutServiceConfig, GRAPHQL_LAYOUT_QUERY_NAME } from '. export { getDesignLibraryStylesheetLinks } from './themes'; export { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; + +export { + rewriteContentInLayout, + ContentRewriteOptions, +} from './content-rewrite'; diff --git a/packages/content/src/layout/layout-service.ts b/packages/content/src/layout/layout-service.ts index 77e491a34e..8351604c18 100644 --- a/packages/content/src/layout/layout-service.ts +++ b/packages/content/src/layout/layout-service.ts @@ -1,7 +1,6 @@ import { FetchOptions } from '@sitecore-content-sdk/core'; import { GraphQLServiceConfig, SitecoreServiceBase } from '../sitecore-service-base'; import { LayoutServiceData, RouteOptions } from './models'; -import { rewriteEdgeHostInResponse } from './rewrite-edge-host'; import debug from '../debug'; import { SitecoreConfigInput } from '../config'; @@ -57,8 +56,7 @@ export class LayoutService extends SitecoreServiceBase { sitecore: { context: { pageEditing: false, language: routeOptions?.locale }, route: null }, }; - // Rewrite Edge hostnames in response if custom hostname is configured - return rewriteEdgeHostInResponse(layoutData); + return layoutData; } /** diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index 22e59a2990..00417f3f91 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -47,6 +47,24 @@ describe('rewriteEdgeHostInResponse', () => { expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); + it('should rewrite edge-staging.sitecore-staging.cloud in string values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'https://edge-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); + }); + + it('should rewrite edge-platform-staging.sitecore-staging.cloud in string values', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const response = { + url: 'https://edge-platform-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response); + expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); + }); + it('should rewrite multiple occurrences in a string', () => { process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; const response = { @@ -204,6 +222,12 @@ describe('rewriteEdgeHostInResponse', () => { expect(containsDefaultEdgeHost('https://edge.sitecorecloud.io/media/image.jpg')).to.be.true; }); + it('should return true for edge-staging.sitecore-staging.cloud', () => { + expect( + containsDefaultEdgeHost('https://edge-staging.sitecore-staging.cloud/tenant/media/a.jpg') + ).to.be.true; + }); + it('should return false for custom hostname', () => { expect(containsDefaultEdgeHost('https://custom.example.com/media/image.jpg')).to.be.false; }); diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index d934ce3a98..c8cb9ed406 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -3,11 +3,14 @@ import { hasCustomEdgeHostname, resolveEdgeUrl } from '@sitecore-content-sdk/cor /** * The default Edge Platform hostnames that may appear in responses. * These will be replaced with the custom hostname when configured. + * Includes production and staging so layout content (e.g. media URLs) can be rewritten. * @internal */ const DEFAULT_EDGE_HOSTNAMES = [ 'edge-platform.sitecorecloud.io', 'edge.sitecorecloud.io', // Legacy hostname included for safety replacement + 'edge-staging.sitecore-staging.cloud', + 'edge-platform-staging.sitecore-staging.cloud', ]; /** diff --git a/packages/content/src/site/error-pages-service.ts b/packages/content/src/site/error-pages-service.ts index 00a4a90140..97b1bbc5bd 100644 --- a/packages/content/src/site/error-pages-service.ts +++ b/packages/content/src/site/error-pages-service.ts @@ -1,7 +1,7 @@ import { GraphQLRequestClientFactory } from '@sitecore-content-sdk/core'; import { FetchOptions, GraphQLClient } from '../client'; import debug from '../debug'; -import { LayoutServiceData, rewriteEdgeHostInResponse } from '../layout'; +import { LayoutServiceData } from '../layout'; import { GraphQLServiceConfig } from '../sitecore-service-base'; import { siteNameError } from '../constants'; @@ -116,13 +116,12 @@ export class ErrorPagesService { .then((result: ErrorPagesQueryResult) => { if (!result.site.siteInfo) return null; - // Rewrite Edge hostnames in error page layouts if custom hostname is configured const errorHandling = result.site.siteInfo.errorHandling; const notFoundPage = errorHandling.notFoundPage?.rendered - ? { rendered: rewriteEdgeHostInResponse(errorHandling.notFoundPage.rendered) } + ? { rendered: errorHandling.notFoundPage.rendered } : null; const serverErrorPage = errorHandling.serverErrorPage?.rendered - ? { rendered: rewriteEdgeHostInResponse(errorHandling.serverErrorPage.rendered) } + ? { rendered: errorHandling.serverErrorPage.rendered } : null; return { diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 7e975c5038..4f1abadeab 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -271,6 +271,9 @@ export const normalizeUrl: (url: string) => string; // @public export function resolveEdgeUrl(edgeUrl?: string): string; +// @public +export function resolveEdgeUrlForStaticFiles(): string; + // @public export function resolveUrl(urlBase: string, params?: ParsedUrlQueryInput): string; @@ -311,7 +314,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:42:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 87d8cc62a5..f85098ef34 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -8,6 +8,7 @@ export { ensurePathExists } from './ensurePath'; export { normalizeUrl } from './normalize-url'; export { resolveEdgeUrl, + resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, SITECORE_EDGE_HOSTNAME_ENV, diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index a76fb11ca6..b85c986a90 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { resolveEdgeUrl, + resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, SITECORE_EDGE_HOSTNAME_ENV, @@ -156,4 +157,30 @@ describe('resolveEdgeUrl', () => { expect(getCustomEdgeUrl()).to.equal('https://custom.example.com'); }); }); + + describe('resolveEdgeUrlForStaticFiles()', () => { + it('should return SITECORE_EDGE_URL when set', () => { + process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-for-files.example.com'; + const result = resolveEdgeUrlForStaticFiles(); + expect(result).to.equal('https://edge-for-files.example.com'); + }); + + it('should return default when no URL env vars are set', () => { + const result = resolveEdgeUrlForStaticFiles(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + + it('should ignore custom hostname and use URL env when set', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-platform.example.com'; + const result = resolveEdgeUrlForStaticFiles(); + expect(result).to.equal('https://edge-platform.example.com'); + }); + + it('should ignore custom hostname and return default when URL env not set', () => { + process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + const result = resolveEdgeUrlForStaticFiles(); + expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + }); + }); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index f028176c73..a1e72e6729 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -39,17 +39,13 @@ export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; * The hostname env var can be provided as: * - Full URL: `https://my-custom-edge.example.com` * - Hostname only: `my-custom-edge.example.com` (will be prefixed with `https://`) - * * @param {string} [edgeUrl] - Optional explicit Edge URL to use (takes precedence if provided) * @returns {string} The resolved Edge Platform base URL (normalized, no trailing slash) * @public - * * @example * resolveEdgeUrl() // => 'https://my-tenant.edge.example.com' - * * @example * resolveEdgeUrl('https://custom.edge.com') // => 'https://custom.edge.com' - * * @example * resolveEdgeUrl() // => 'https://edge-platform.sitecorecloud.io' */ @@ -87,9 +83,27 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return SITECORE_EDGE_URL_DEFAULT; } +/** + * Resolves the Edge URL for static files (e.g. stylesheets) by ignoring the custom hostname. + * Use this when the custom host does not serve static file paths (e.g. /v1/files/...). + * Priority: SITECORE_EDGE_URL / NEXT_PUBLIC_SITECORE_EDGE_URL env, then default. + * @returns {string} The Edge Platform base URL for static files (no trailing slash) + * @public + */ +export function resolveEdgeUrlForStaticFiles(): string { + const isBrowser = typeof window !== 'undefined'; + const urlEnvVarRaw = isBrowser + ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] + : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + const urlEnvVar = normalizeMaybeEnvValue(urlEnvVarRaw); + if (urlEnvVar) { + return normalizeUrl(urlEnvVar); + } + return SITECORE_EDGE_URL_DEFAULT; +} + /** * Normalizes a hostname or URL to a full HTTPS URL without trailing slash. - * * @param {string} hostnameOrUrl - A hostname (e.g., 'my.domain.com') or full URL (e.g., 'https://my.domain.com') * @returns {string} A normalized HTTPS URL * @internal @@ -110,7 +124,6 @@ function normalizeHostnameToUrl(hostnameOrUrl: string): string { * Normalizes values that may come from environment variables. * In Node, setting `process.env.FOO = undefined` results in the string 'undefined', * which should be treated as if the variable is not set. - * * @param {string | undefined} value - Possibly undefined env-like value * @returns {string | undefined} A usable string value, or undefined if not meaningful * @internal @@ -129,7 +142,6 @@ function normalizeMaybeEnvValue(value: string | undefined): string | undefined { /** * Checks if a custom Edge hostname is configured via environment variables. - * * @returns {boolean} True if a custom hostname is configured * @public */ @@ -147,7 +159,6 @@ export function hasCustomEdgeHostname(): boolean { /** * Gets the custom Edge hostname if configured, otherwise returns undefined. - * * @returns {string | undefined} The custom Edge URL if configured, undefined otherwise * @public */ diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts index 24de77be3d..a8f28817fe 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts @@ -4,4 +4,7 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config'; * See the documentation for `defineConfig`: * https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html */ -export default defineConfig({}); +export default defineConfig({ + // Enable to use custom hostname for content/media URLs (default: false) + // rewriteContentUrls: true, +}); diff --git a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts index 24de77be3d..a8f28817fe 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts @@ -4,4 +4,7 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config'; * See the documentation for `defineConfig`: * https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html */ -export default defineConfig({}); +export default defineConfig({ + // Enable to use custom hostname for content/media URLs (default: false) + // rewriteContentUrls: true, +}); From c1b72b47112994f5f99edcfee4656e49020683d9 Mon Sep 17 00:00:00 2001 From: Menelaos Nasies <38861573+MenKNas@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:36:59 +0200 Subject: [PATCH 11/25] Potential fix for code scanning alert no. 50: Incomplete regular expression for hostnames Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- packages/content/src/layout/content-rewrite.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/content/src/layout/content-rewrite.test.ts b/packages/content/src/layout/content-rewrite.test.ts index 782797afee..261975be3b 100644 --- a/packages/content/src/layout/content-rewrite.test.ts +++ b/packages/content/src/layout/content-rewrite.test.ts @@ -56,7 +56,7 @@ describe('rewriteContentInLayout', () => { }; const result = rewriteContentInLayout( layout, - 'https://edge.example.com', + /https:\/\/edge\.example\.com/gi, 'https://cdn.example.com', { type: 'normal' } ); From c8b9073e9fcd68e206482964073914e675ed62db Mon Sep 17 00:00:00 2001 From: Menelaos Nasies <38861573+MenKNas@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:49:29 +0200 Subject: [PATCH 12/25] Potential fix for code scanning alert no. 49: Incomplete regular expression for hostnames Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- packages/content/src/layout/content-rewrite.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/content/src/layout/content-rewrite.test.ts b/packages/content/src/layout/content-rewrite.test.ts index 261975be3b..63049734b9 100644 --- a/packages/content/src/layout/content-rewrite.test.ts +++ b/packages/content/src/layout/content-rewrite.test.ts @@ -23,7 +23,7 @@ describe('rewriteContentInLayout', () => { }; const result = rewriteContentInLayout( layout, - 'https://edge.example.com', + /https:\/\/edge\.example\.com/g, 'https://custom.example.com', { type: 'normal' } ); From 25f5c7c7ad22a08f4689e2b5c9143b14877656c0 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 10 Feb 2026 19:07:30 +0200 Subject: [PATCH 13/25] Fix api extractor issue --- packages/core/api/content-sdk-core.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 78f0ce0ddf..72a013ad2d 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -309,7 +309,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:42:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) From eaeb11cf6f8695de25044c3531d8ede8a39d0026 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Fri, 13 Feb 2026 10:10:39 +0200 Subject: [PATCH 14/25] Address PR comments 1 --- CHANGELOG.md | 3 +- .../content/api/content-sdk-content.api.md | 11 +- packages/content/src/client/edge-proxy.ts | 22 +- .../src/client/sitecore-client.test.ts | 61 +++-- .../content/src/client/sitecore-client.ts | 30 ++- packages/content/src/config/define-config.ts | 24 +- packages/content/src/config/models.ts | 16 +- .../src/editing/component-layout-service.ts | 8 +- .../content/src/editing/design-library.ts | 6 +- .../src/layout/content-rewrite.test.ts | 162 ----------- .../content/src/layout/content-rewrite.ts | 121 --------- packages/content/src/layout/content-styles.ts | 6 +- packages/content/src/layout/index.ts | 10 +- .../src/layout/rewrite-edge-host.test.ts | 253 ++++++++++++++++-- .../content/src/layout/rewrite-edge-host.ts | 68 ++++- packages/content/src/layout/themes.ts | 6 +- packages/core/api/content-sdk-core.api.md | 9 +- packages/core/src/constants.ts | 18 ++ packages/core/src/tools/index.ts | 2 +- .../core/src/tools/resolve-edge-url.test.ts | 51 ++-- packages/core/src/tools/resolve-edge-url.ts | 34 +-- .../nextjs-app-router/.env.container.example | 6 - .../nextjs-app-router/.env.remote.example | 4 +- .../nextjs-app-router/sitecore.config.ts | 5 +- .../templates/nextjs/.env.container.example | 6 - .../src/templates/nextjs/.env.remote.example | 4 +- .../src/templates/nextjs/sitecore.config.ts | 5 +- packages/nextjs/src/config/define-config.ts | 14 +- packages/search/src/search-service.ts | 2 +- 29 files changed, 474 insertions(+), 493 deletions(-) delete mode 100644 packages/content/src/layout/content-rewrite.test.ts delete mode 100644 packages/content/src/layout/content-rewrite.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8161b98f..8e6d41f151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ Our versioning strategy is as follows: * `[nextjs]` `[create-content-sdk-app]` Enable Next.js 16 Cache Components and Turbopack File System Caching ([#334](https://github.com/Sitecore/content-sdk/pull/334)) -* `[core]` `[content]` `[nextjs]` Support custom Edge hostnames via `SITECORE_EDGE_HOSTNAME` / `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` ([#359](https://github.com/Sitecore/content-sdk/pull/359)) +* `[core]` `[content]` `[nextjs]` Support custom Edge hostnames via `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` ([#359](https://github.com/Sitecore/content-sdk/pull/359)) + - Consolidated `rewriteContentUrls` and `contentRewrite` into `rewriteMediaUrls: boolean | ((value: string) => string)`. When `true`, uses default Edge host rewriter; when a function, transforms each string (SDK traverses layout). Migration: `rewriteContentUrls: true` → `rewriteMediaUrls: true`; custom rewriter → `rewriteMediaUrls: (value: string) => string` * Search integration ([#295](https://github.com/Sitecore/content-sdk/pull/295)) * `[search]` New `@sitecore-content-sdk/search` package providing search functionality diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index d5b1e7eb0c..b9302bc78a 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -186,11 +186,6 @@ export interface ComponentUpdateEventArgs { // @public export function containsDefaultEdgeHost(str: string): boolean; -// @public -export interface ContentRewriteOptions { - type?: 'prefix' | 'normal'; -} - // @internal export const createComponentInstance: (importMap: ImportEntry[], previewEventArgs: ComponentPreviewEventArgs) => unknown; @@ -985,9 +980,6 @@ export const resetEditorChromes: () => void; export { RetryStrategy } -// @public -export function rewriteContentInLayout(layout: LayoutServiceData, source: string | RegExp, target: string, options?: ContentRewriteOptions): LayoutServiceData; - // @public export function rewriteEdgeHostInResponse(response: T): T; @@ -1206,8 +1198,7 @@ export type SitecoreConfigInput = { enabled?: boolean; locales?: string[]; }; - rewriteContentUrls?: boolean; - contentRewrite?: (layout: unknown) => unknown; + rewriteMediaUrls?: boolean | ((value: string) => string); disableCodeGeneration?: boolean; }; diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index 94057071ed..84a636a479 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -1,21 +1,29 @@ -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; + +/** + * Resolves the base Edge URL: uses provided value (from config) or falls back to resolveEdgeUrl. + * Normalizes trailing slash when a value is provided. + * @internal + */ +const getBaseEdgeUrl = (sitecoreEdgeUrl?: string): string => + sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl(); /** * Generates a URL for accessing Sitecore Edge Platform Content using the provided endpoint and context ID. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. If not provided, - * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). + * When not provided, resolves from env vars as fallback. * @returns {string} The complete URL for accessing content through the Edge Platform. * @public */ export const getEdgeProxyContentUrl = (sitecoreEdgeUrl?: string) => - `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; + `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; /** * Generates a URL for accessing Sitecore Edge Platform Forms using the provided form ID and context ID. * @param {string} sitecoreEdgeContextId - The unique context id. * @param {string} formId - The unique form id. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. If not provided, - * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). + * When not provided, resolves from env vars as fallback. * @returns {string} The complete URL for accessing forms through the Edge Platform. * @internal */ @@ -24,4 +32,4 @@ export const getEdgeProxyFormsUrl = ( formId: string, sitecoreEdgeUrl?: string ) => - `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; + `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index a66f22aba3..c11cdb78f0 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -5,6 +5,7 @@ import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import { DocumentNode } from 'graphql'; import { DefaultRetryStrategy, NativeDataFetcher } from '@sitecore-content-sdk/core'; +import { SITECORE_EDGE_HOSTNAME_PUBLIC_ENV } from '@sitecore-content-sdk/core/tools'; import { ErrorPage, SitecoreClient } from './sitecore-client'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; @@ -484,7 +485,7 @@ describe('SitecoreClient', () => { }); }); - it('should apply content rewrite when rewriteContentUrls is true', async () => { + it('should apply content rewrite when rewriteMediaUrls is a function', async () => { const path = '/test/path'; const locale = 'en-US'; const siteInfo = { name: 'default-site', hostName: 'example.com', language: 'en' }; @@ -495,25 +496,53 @@ describe('SitecoreClient', () => { }, }; layoutServiceStub.fetchLayoutData.returns(rawLayout); - const customRewriter = (layout: LayoutServiceData): LayoutServiceData => ({ - ...layout, - sitecore: { - ...layout.sitecore, - route: layout.sitecore.route - ? { ...layout.sitecore.route, displayName: 'rewritten' } - : null, - }, - }); + const stringTransformer = (value: string) => + value === 'home' ? 'rewritten' : value; const clientWithRewrite = new SitecoreClient({ ...defaultInitOptions, - rewriteContentUrls: true, - contentRewrite: customRewriter as any, + rewriteMediaUrls: stringTransformer, } as any); (clientWithRewrite as any).layoutService = layoutServiceStub; const result = await clientWithRewrite.getPage(path, { locale }); - expect(result?.layout.sitecore.route?.displayName).to.equal('rewritten'); + expect(result?.layout.sitecore.route?.name).to.equal('rewritten'); + }); + + it('should apply default Edge host rewrite when rewriteMediaUrls is true and custom hostname is set', async () => { + const originalEnv = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + try { + const path = '/test/path'; + const locale = 'en-US'; + const siteInfo = { name: 'default-site', hostName: 'example.com', language: 'en' }; + const rawLayout = { + sitecore: { + route: { + name: 'home', + placeholders: {}, + fields: { + image: { value: { src: 'https://edge-platform.sitecorecloud.io/-/media/hero.jpg' } }, + }, + }, + context: { site: siteInfo, pageState: LayoutServicePageState.Normal }, + }, + }; + layoutServiceStub.fetchLayoutData.returns(rawLayout); + const clientWithRewrite = new SitecoreClient({ + ...defaultInitOptions, + rewriteMediaUrls: true, + } as any); + (clientWithRewrite as any).layoutService = layoutServiceStub; + + const result = await clientWithRewrite.getPage(path, { locale }); + + expect( + (result?.layout.sitecore.route?.fields?.image?.value as { src: string }).src + ).to.equal('https://custom.example.com/-/media/hero.jpg'); + } finally { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = originalEnv; + } }); it('should pass fetchOptions to layoutService when calling getPage', async () => { @@ -1496,8 +1525,8 @@ describe('SitecoreClient', () => { }); it('should rewrite Edge hostnames in sitemap path and XML when custom hostname is configured', async () => { - const originalEnv = process.env.SITECORE_EDGE_HOSTNAME; - process.env.SITECORE_EDGE_HOSTNAME = 'https://custom.example.com'; + const originalEnv = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://custom.example.com'; const edgeSitemapPath = 'https://edge-platform.sitecorecloud.io/sitemap.xml'; const xmlContent = @@ -1513,7 +1542,7 @@ describe('SitecoreClient', () => { expect(dataFetcherStub.calledWith('https://custom.example.com/sitemap.xml')).to.be.true; expect(result).to.include('https://custom.example.com/a'); - process.env.SITECORE_EDGE_HOSTNAME = originalEnv; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = originalEnv; }); it('should fetch specific sitemap when ID is provided', async () => { diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 9e2f491cde..7583b1d701 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -17,6 +17,8 @@ import { RouteOptions, LayoutServicePageState, rewriteEdgeHostInResponse, + getDefaultMediaUrlTransformer, + applyMediaUrlRewrite, } from '../layout'; import { HTMLLink, StaticPath } from '../models'; import { getGroomedVariantIds, PersonalizedRewriteData } from '../personalize/utils'; @@ -360,17 +362,16 @@ export class SitecoreClient implements BaseSitecoreClient { if (!layout.sitecore.route) { return null; } - layout = this.applyContentRewrite(layout); - // Initialize links to be inserted on the page + // Apply personalization first to select the variant(s) that will be used, + // then rewrite content URLs so we only process the selected variant(s) if (pageOptions?.personalize?.variantId) { - // Modify layoutData to use specific variant(s) instead of default - // This will also set the variantId on the Sitecore context so that it is accessible here personalizeLayout( layout, pageOptions.personalize.variantId, pageOptions.personalize.componentVariantIds ); } + layout = this.applyContentRewrite(layout); return { layout, @@ -477,15 +478,16 @@ export class SitecoreClient implements BaseSitecoreClient { if (!data) { throw new Error(`Unable to fetch editing data for preview ${JSON.stringify(previewData)}`); } - const layout = this.applyContentRewrite(data.layoutData); + let layout = data.layoutData; + const personalizeData = getGroomedVariantIds(variantIds); + personalizeLayout(layout, personalizeData.variantId, personalizeData.componentVariantIds); + layout = this.applyContentRewrite(layout); const page: Page = { locale: language, layout, siteName: layout.sitecore.context.site?.name || site, mode: this.getPageMode(mode), }; - const personalizeData = getGroomedVariantIds(variantIds); - personalizeLayout(page.layout, personalizeData.variantId, personalizeData.componentVariantIds); return page; } @@ -711,20 +713,20 @@ export class SitecoreClient implements BaseSitecoreClient { * @returns {PageMode} The page mode */ /** - * Applies content URL rewrite when rewriteContentUrls is enabled (opt-in). - * Uses contentRewrite from config or the default Edge host rewriter. + * Applies media URL rewrite when rewriteMediaUrls is enabled. + * When true, uses default Edge host rewriter; when a function, transforms each string. * @param {LayoutServiceData} layout - Layout data from layout/editing/component/error service * @returns {LayoutServiceData} Rewritten layout (or same reference if rewrite disabled) * @internal */ protected applyContentRewrite(layout: LayoutServiceData): LayoutServiceData { - if (!this.initOptions.rewriteContentUrls) { + const opt = this.initOptions.rewriteMediaUrls; + if (!opt) { return layout; } - const rewriter = this.initOptions.contentRewrite as ( - l: LayoutServiceData - ) => LayoutServiceData; - return rewriter(layout); + const transformer = + opt === true ? getDefaultMediaUrlTransformer() : opt; + return applyMediaUrlRewrite(layout, transformer); } private getPageMode(mode: PageModeName, generation?: DesignLibraryVariantGeneration): PageMode { diff --git a/packages/content/src/config/define-config.ts b/packages/content/src/config/define-config.ts index 7277f9ff41..096aec599d 100644 --- a/packages/content/src/config/define-config.ts +++ b/packages/content/src/config/define-config.ts @@ -1,8 +1,12 @@ import { DefaultRetryStrategy } from '@sitecore-content-sdk/core'; -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { + resolveEdgeUrl, + SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, + SITECORE_EDGE_URL_ENV, + SITECORE_EDGE_URL_PUBLIC_ENV, +} from '@sitecore-content-sdk/core/tools'; import { DeepPartial, SitecoreConfig, SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; -import { rewriteEdgeHostInResponse } from '../layout'; /** * Provides default initial values for SitecoreConfig @@ -13,7 +17,11 @@ export const getFallbackConfig = (): SitecoreConfig => ({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: '', - edgeUrl: resolveEdgeUrl(), + edgeUrl: resolveEdgeUrl( + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_ENV] + ), }, local: { apiKey: process.env.SITECORE_API_KEY || process.env.NEXT_PUBLIC_SITECORE_API_KEY || '', @@ -55,8 +63,7 @@ export const getFallbackConfig = (): SitecoreConfig => ({ timeout: 60, }, }, - rewriteContentUrls: false, - contentRewrite: rewriteEdgeHostInResponse, + rewriteMediaUrls: false, disableCodeGeneration: false, }); @@ -112,6 +119,13 @@ const resolveConfig = (base: SitecoreConfig, override: SitecoreConfigInput): Sit if (Number.isNaN(result.personalize.edgeTimeout) || !result.personalize.edgeTimeout) { result.personalize.edgeTimeout = base.personalize.edgeTimeout; } + // Resolve edge URL at config level so consumers use the resolved value directly + result.api.edge.edgeUrl = resolveEdgeUrl( + result.api.edge.edgeUrl ?? + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_ENV] + ); return result; }; diff --git a/packages/content/src/config/models.ts b/packages/content/src/config/models.ts index b09cf8f98e..17acda07c3 100644 --- a/packages/content/src/config/models.ts +++ b/packages/content/src/config/models.ts @@ -204,18 +204,12 @@ export type SitecoreConfigInput = { locales?: string[]; }; /** - * Opt-in: rewrite URLs in layout content (media fields, rich text with img/src, href, etc.) - * to use the custom hostname. When true, uses the default rewriter (Edge host -> custom host) - * or contentRewrite if provided. Off by default to avoid unintended rewrites. + * Rewrite media/content URLs in layout (media fields, rich text img/src, href, etc.). + * - When `true`: use default rewriter (Edge hostnames -> custom hostname from env). + * - When a function: transform each string value; the SDK traverses the layout for you. + * @default false */ - rewriteContentUrls?: boolean; - /** - * Optional custom content rewriter. When rewriteContentUrls is true and this is set, - * it is used instead of the default. Use for custom rules (e.g. only certain fields or URL shapes). - * @param layout - Layout service data - * @returns Rewritten layout (do not mutate input) - */ - contentRewrite?: (layout: unknown) => unknown; + rewriteMediaUrls?: boolean | ((value: string) => string); /** * Opt-out setting for code generation feature * Disables code extraction procedure diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index 013010710f..e6dcee043c 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -1,5 +1,5 @@ import { NativeDataFetcher, FetchOptions } from '@sitecore-content-sdk/core'; -import { resolveUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl, resolveUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { LayoutServiceData } from '../layout'; import debug from '../debug'; import { DesignLibraryMode, DesignLibraryVariantGeneration } from './models'; @@ -141,9 +141,7 @@ export class ComponentLayoutService { * @returns {string} The fetch URL for the component data */ private getFetchUrl(params: ComponentLayoutRequestParams) { - return resolveUrl( - `${resolveEdgeUrl(this.config.edgeUrl)}/layout/component`, - this.getComponentFetchParams(params) - ); + const baseUrl = this.config.edgeUrl ? normalizeUrl(this.config.edgeUrl) : resolveEdgeUrl(); + return resolveUrl(`${baseUrl}/layout/component`, this.getComponentFetchParams(params)); } } diff --git a/packages/content/src/editing/design-library.ts b/packages/content/src/editing/design-library.ts index d7b2e926a5..42732fc30e 100644 --- a/packages/content/src/editing/design-library.ts +++ b/packages/content/src/editing/design-library.ts @@ -1,4 +1,4 @@ -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentFields, ComponentParams, @@ -221,12 +221,12 @@ export function getDesignLibraryStatusEvent( /** * Generates the URL for the design library script link. * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns The full URL to the design library script. * @internal */ export function getDesignLibraryScriptLink(sitecoreEdgeUrl?: string): string { - return `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; + return `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/designlibrary/lib/rh-lib-script.js`; } /** diff --git a/packages/content/src/layout/content-rewrite.test.ts b/packages/content/src/layout/content-rewrite.test.ts deleted file mode 100644 index 63049734b9..0000000000 --- a/packages/content/src/layout/content-rewrite.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { expect } from 'chai'; -import { rewriteContentInLayout } from './content-rewrite'; -import { LayoutServiceData } from './models'; - -describe('rewriteContentInLayout', () => { - it('should replace string source with target in all string values (type normal)', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: {}, - fields: { - image: { - value: { - src: 'https://edge.example.com/media/image.jpg', - }, - }, - }, - }, - }, - }; - const result = rewriteContentInLayout( - layout, - /https:\/\/edge\.example\.com/g, - 'https://custom.example.com', - { type: 'normal' } - ); - expect(result.sitecore.route!.fields!.image.value.src).to.equal( - 'https://custom.example.com/media/image.jpg' - ); - }); - - it('should replace in rich text (HTML with img src)', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: { - main: [ - { - componentName: 'RichText', - fields: { - content: { - value: - '

and link

', - }, - }, - }, - ], - }, - }, - }, - }; - const result = rewriteContentInLayout( - layout, - /https:\/\/edge\.example\.com/gi, - 'https://cdn.example.com', - { type: 'normal' } - ); - expect(result.sitecore.route!.placeholders!.main[0].fields!.content.value).to.equal( - '

and link

' - ); - }); - - it('should replace with RegExp source', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: {}, - fields: { - link: { value: { href: 'https://edge-staging.sitecore-staging.cloud/path' } }, - }, - }, - }, - }; - const result = rewriteContentInLayout( - layout, - /https?:\/\/edge(-staging)?\.sitecore[^/]+/gi, - 'https://custom.example.com', - { type: 'normal' } - ); - expect((result.sitecore.route!.fields!.link.value as { href: string }).href).to.equal( - 'https://custom.example.com/path' - ); - }); - - it('should not mutate input layout', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: {}, - fields: { url: { value: 'https://old.com/path' } }, - }, - }, - }; - const original = (layout.sitecore.route!.fields as { url: { value: string } }).url.value; - rewriteContentInLayout(layout, 'https://old.com', 'https://new.com'); - expect((layout.sitecore.route!.fields as { url: { value: string } }).url.value).to.equal( - original - ); - }); - - it('should default to type normal', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: {}, - fields: { u: { value: 'https://a.com/xhttps://a.com/y' } }, - }, - }, - }; - const result = rewriteContentInLayout(layout, 'https://a.com', 'https://b.com'); - expect((result.sitecore.route!.fields as { u: { value: string } }).u.value).to.equal( - 'https://b.com/xhttps://b.com/y' - ); - }); - - it('should handle type prefix (string source)', () => { - const layout: LayoutServiceData = { - sitecore: { - context: {}, - route: { - name: 'test', - placeholders: {}, - fields: { - a: { value: 'https://edge.com/path' }, - b: { value: 'see https://edge.com/other' }, - }, - }, - }, - }; - const result = rewriteContentInLayout( - layout, - 'https://edge.com', - 'https://custom.com', - { type: 'prefix' } - ); - expect((result.sitecore.route!.fields as { a: { value: string }; b: { value: string } }).a.value).to.equal( - 'https://custom.com/path' - ); - expect((result.sitecore.route!.fields as { a: { value: string }; b: { value: string } }).b.value).to.equal( - 'see https://edge.com/other' - ); - }); - - it('should handle null route', () => { - const layout: LayoutServiceData = { - sitecore: { context: {}, route: null }, - }; - const result = rewriteContentInLayout(layout, 'https://old.com', 'https://new.com'); - expect(result.sitecore.route).to.be.null; - }); -}); diff --git a/packages/content/src/layout/content-rewrite.ts b/packages/content/src/layout/content-rewrite.ts deleted file mode 100644 index fb42e3f764..0000000000 --- a/packages/content/src/layout/content-rewrite.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { LayoutServiceData } from './models'; - -/** - * Options for content URL rewriting. - * Affects how source is matched in string values (e.g. URL fields, rich text with img/src, href). - * @public - */ -export interface ContentRewriteOptions { - /** - * - 'normal': replace every occurrence of source with target in each string. - * - 'prefix': replace only when the string starts with source (e.g. URL prefix). - * @default 'normal' - */ - type?: 'prefix' | 'normal'; -} - -/** - * Performs a single string replacement according to options. - * @param {string} str - String to process - * @param {string | RegExp} source - String or RegExp to replace - * @param {string} target - Replacement string - * @param {ContentRewriteOptions} options - Rewrite options - * @returns {string} Rewritten string - * @internal - */ -function replaceInString( - str: string, - source: string | RegExp, - target: string, - options: ContentRewriteOptions -): string { - if (options.type === 'prefix' && typeof source === 'string') { - return str.startsWith(source) ? target + str.slice(source.length) : str; - } - if (options.type === 'prefix' && source instanceof RegExp) { - const match = str.match(source); - if (match && match.index === 0) { - return str.replace(source, target); - } - return str; - } - // normal: replace all - if (typeof source === 'string') { - return str.split(source).join(target); - } - return str.replace(source, target); -} - -/** - * Deep traversal that replaces source with target in every string value. - * Covers URL fields (e.g. field.value.src), rich text (HTML with img src, a href), and any other string in the layout. - * @param {T} value - Any value (layout, object, array, string) - * @param {string | RegExp} source - String or RegExp to replace (e.g. Edge host URL or pattern) - * @param {string} target - Replacement string (e.g. custom host URL) - * @param {ContentRewriteOptions} options - Rewrite options - * @returns {T} New value with replacements applied - * @internal - */ -function deepReplace( - value: T, - source: string | RegExp, - target: string, - options: ContentRewriteOptions -): T { - if (value === null || value === undefined) { - return value; - } - - if (typeof value === 'string') { - return replaceInString(value, source, target, options) as T; - } - - if (Array.isArray(value)) { - return value.map((item) => deepReplace(item, source, target, options)) as T; - } - - if (typeof value === 'object') { - if (Object.getPrototypeOf(value) !== Object.prototype) { - return value; - } - const result: Record = {}; - for (const key of Object.keys(value as Record)) { - result[key] = deepReplace( - (value as Record)[key], - source, - target, - options - ); - } - return result as T; - } - - return value; -} - -/** - * Rewrites content URLs in layout data by replacing source with target in every string value. - * Use for media URLs (Image field value.src), rich text (HTML with <img src="...">, <a href="...">), and link fields. - * This is an unopinionated helper: it replaces in all strings, so use a specific source/target to avoid unintended rewrites. - * @param {LayoutServiceData} layout - Layout service data (route, placeholders, component fields) - * @param {string | RegExp} source - String or RegExp to replace (e.g. 'https://edge-staging.sitecore-staging.cloud' or regex) - * @param {string} target - Replacement string (e.g. 'https://custom.example.com') - * @param {ContentRewriteOptions} options - Optional. type 'normal' (default) replaces all occurrences; 'prefix' replaces only at string start. - * @returns {LayoutServiceData} New layout with replacements applied (does not mutate input) - * @public - * @example - * // Replace Edge staging host with custom host in all content (URLs and rich text) - * const rewritten = rewriteContentInLayout(layout, 'https://edge-staging.sitecore-staging.cloud', 'https://my-cdn.example.com'); - * @example - * // Replace with regex (e.g. any Edge host) - * const rewritten = rewriteContentInLayout(layout, /https?:\/\/edge(-staging)?\.sitecore[^/]+/gi, 'https://custom.example.com'); - */ -export function rewriteContentInLayout( - layout: LayoutServiceData, - source: string | RegExp, - target: string, - options: ContentRewriteOptions = {} -): LayoutServiceData { - const opts: ContentRewriteOptions = { type: 'normal', ...options }; - return deepReplace(layout, source, target, opts); -} diff --git a/packages/content/src/layout/content-styles.ts b/packages/content/src/layout/content-styles.ts index 9b02bc821d..bc812e8aa5 100644 --- a/packages/content/src/layout/content-styles.ts +++ b/packages/content/src/layout/content-styles.ts @@ -1,4 +1,4 @@ -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, Field, Item, LayoutServiceData, RouteData } from './index'; import { HTMLLink } from '../models'; @@ -14,7 +14,7 @@ type Config = { loadStyles: boolean }; * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {HTMLLink | null} content styles link, null if no styles are used in layout * @public */ @@ -41,7 +41,7 @@ export const getContentStylesheetUrl = ( sitecoreEdgeContextId: string, sitecoreEdgeUrl?: string ): string => - `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; + `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; export const traversePlaceholder = (components: Array, config: Config) => { if (config.loadStyles) return; diff --git a/packages/content/src/layout/index.ts b/packages/content/src/layout/index.ts index de2a6bc2fd..2c0d6b154d 100644 --- a/packages/content/src/layout/index.ts +++ b/packages/content/src/layout/index.ts @@ -36,9 +36,9 @@ export { LayoutService, LayoutServiceConfig, GRAPHQL_LAYOUT_QUERY_NAME } from '. export { getDesignLibraryStylesheetLinks } from './themes'; -export { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; - export { - rewriteContentInLayout, - ContentRewriteOptions, -} from './content-rewrite'; + rewriteEdgeHostInResponse, + containsDefaultEdgeHost, + getDefaultMediaUrlTransformer, + applyMediaUrlRewrite, +} from './rewrite-edge-host'; diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index 00417f3f91..ffdab2b246 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -1,17 +1,13 @@ /* eslint-disable no-unused-expressions */ +import { performance } from 'perf_hooks'; import { expect } from 'chai'; import { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; -import { - SITECORE_EDGE_HOSTNAME_ENV, - SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, -} from '@sitecore-content-sdk/core/tools'; +import { SITECORE_EDGE_HOSTNAME_PUBLIC_ENV } from '@sitecore-content-sdk/core/tools'; describe('rewriteEdgeHostInResponse', () => { const originalEnv = { ...process.env }; beforeEach(() => { - // Clear all relevant env vars before each test - delete process.env[SITECORE_EDGE_HOSTNAME_ENV]; delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; }); @@ -30,7 +26,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite edge-platform.sitecorecloud.io in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', }; @@ -39,7 +35,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite edge.sitecorecloud.io in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge.sitecorecloud.io/media/image.jpg', }; @@ -48,7 +44,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite edge-staging.sitecore-staging.cloud in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; @@ -57,7 +53,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite edge-platform-staging.sitecore-staging.cloud in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-platform-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; @@ -66,7 +62,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite multiple occurrences in a string', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { html: '', }; @@ -77,7 +73,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite nested objects', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { sitecore: { context: {}, @@ -99,7 +95,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite arrays', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { urls: [ 'https://edge-platform.sitecorecloud.io/a.jpg', @@ -114,7 +110,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle null values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { value: null, }; @@ -123,7 +119,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle undefined values', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { value: undefined, }; @@ -132,7 +128,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should preserve non-string primitives', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { number: 42, boolean: true, @@ -145,7 +141,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle http protocol in edge URLs', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'http://edge-platform.sitecorecloud.io/media/image.jpg', }; @@ -154,7 +150,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle mixed case (case insensitive)', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://EDGE-PLATFORM.SITECORECLOUD.IO/media/image.jpg', }; @@ -163,7 +159,7 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle complex layout service data structure', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const layoutData = { sitecore: { context: { @@ -211,6 +207,225 @@ describe('rewriteEdgeHostInResponse', () => { }); }); + describe('performance benchmarks', () => { + const ITERATIONS = 500; + + /** Build a layout with N placeholder components (mix of Image + RichText). */ + function buildLayout(componentCount: number): Record { + const edgeUrl = 'https://edge-platform.sitecorecloud.io'; + const components = []; + for (let i = 0; i < componentCount; i++) { + components.push({ + componentName: i % 2 === 0 ? 'Image' : 'RichText', + fields: + i % 2 === 0 + ? { + Image: { + value: { + src: `${edgeUrl}/tenant/media/image-${i}.jpg`, + alt: `Image ${i}`, + }, + }, + } + : { + content: { + value: `

Block ${i}: and link

`, + }, + }, + }); + } + return { + sitecore: { + context: { site: { name: 'test' }, language: 'en' }, + route: { + name: 'page', + placeholders: { main: components }, + fields: { + title: { value: `Page with ${componentCount} components` }, + image: { + value: { src: `${edgeUrl}/tenant/media/hero.jpg` }, + }, + }, + }, + }, + }; + } + + it('benchmark: rewrite timing vs layout size (with custom hostname)', function () { + this.timeout(30000); + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + + const sizes = [ + { name: 'small (10 components)', count: 10 }, + { name: 'medium (50 components)', count: 50 }, + { name: 'large (150 components)', count: 150 }, + ]; + + const results: { size: string; msPerCall: number; totalMs: number; iterations: number }[] = []; + + for (const { name, count } of sizes) { + const layout = buildLayout(count); + const start = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + rewriteEdgeHostInResponse(layout); + } + const totalMs = performance.now() - start; + const msPerCall = totalMs / ITERATIONS; + results.push({ size: name, msPerCall, totalMs, iterations: ITERATIONS }); + } + + // Assert all under 5ms per call (reasonable for CI) + for (const r of results) { + expect(r.msPerCall, `${r.size} should be < 5ms per call`).to.be.lessThan(5); + } + + // Log metrics for presentation + console.log('\n--- rewriteEdgeHostInResponse performance (custom hostname enabled) ---'); + for (const r of results) { + console.log(` ${r.size}: ${r.msPerCall.toFixed(3)}ms per call (${r.iterations} iterations, ${r.totalMs.toFixed(0)}ms total)`); + } + console.log('---\n'); + }); + + it('benchmark: no-op when custom hostname disabled (baseline)', function () { + this.timeout(15000); + delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + + const layout = buildLayout(100); + const start = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + rewriteEdgeHostInResponse(layout); + } + const totalMs = performance.now() - start; + const msPerCall = totalMs / ITERATIONS; + + expect(msPerCall).to.be.lessThan(1); + console.log( + `\n--- rewriteEdgeHostInResponse when disabled (early return): ${msPerCall.toFixed(4)}ms per call (${ITERATIONS} iterations) ---\n` + ); + }); + + it('benchmark: comparison - rewrite enabled vs disabled', function () { + this.timeout(20000); + const layout = buildLayout(75); + const iterations = 1000; + + // Disabled + delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; + const startDisabled = performance.now(); + for (let i = 0; i < iterations; i++) { + rewriteEdgeHostInResponse(layout); + } + const disabledMs = performance.now() - startDisabled; + + // Enabled + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + const startEnabled = performance.now(); + for (let i = 0; i < iterations; i++) { + rewriteEdgeHostInResponse(layout); + } + const enabledMs = performance.now() - startEnabled; + + const disabledPerCall = disabledMs / iterations; + const enabledPerCall = enabledMs / iterations; + const overheadMs = enabledPerCall - disabledPerCall; + + console.log('\n--- Comparison (75 components, 1000 iterations) ---'); + console.log(` Disabled (early return): ${disabledPerCall.toFixed(4)}ms per call`); + console.log(` Enabled (full rewrite): ${enabledPerCall.toFixed(4)}ms per call`); + console.log(` Overhead per request: ${overheadMs.toFixed(4)}ms`); + console.log('---\n'); + + expect(enabledPerCall).to.be.lessThan(2); + }); + + it('resilience: regex completes quickly on long strings (no catastrophic backtracking)', function () { + this.timeout(5000); + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + + // Long string with many Edge URLs (could trigger backtracking with bad regex) + const urls = Array(100).fill('https://edge-platform.sitecorecloud.io/tenant/media/image.jpg').join(' '); + const layout = { sitecore: { route: { fields: { content: { value: urls } } } } }; + + const start = performance.now(); + for (let i = 0; i < 100; i++) { + rewriteEdgeHostInResponse(layout); + } + const elapsed = performance.now() - start; + const msPerCall = elapsed / 100; + + expect(msPerCall, '100 URLs in one string should complete in < 50ms per call').to.be.lessThan(50); + const result = rewriteEdgeHostInResponse(layout) as typeof layout; + expect((result.sitecore.route.fields.content.value as string).includes('custom.example.com')).to.be + .true; + expect((result.sitecore.route.fields.content.value as string).includes('edge-platform.sitecorecloud.io')) + .to.be.false; + console.log(`\n--- Regex resilience: 100 URLs in one string = ${msPerCall.toFixed(2)}ms per call ---\n`); + }); + + it('resilience: memory stable under repeated rewrites', function () { + this.timeout(30000); + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + + const layout = buildLayout(80); + const iterations = 2000; + + if (global.gc) { + global.gc(); + } + const memBefore = process.memoryUsage().heapUsed; + + for (let i = 0; i < iterations; i++) { + rewriteEdgeHostInResponse(layout); + } + + if (global.gc) { + global.gc(); + } + const memAfter = process.memoryUsage().heapUsed; + const growthMb = (memAfter - memBefore) / 1024 / 1024; + + expect(growthMb, 'memory growth should be < 50MB after 2000 rewrites').to.be.lessThan(50); + console.log( + `\n--- Memory: ${growthMb.toFixed(2)}MB growth after ${iterations} rewrites (heap before: ${(memBefore / 1024 / 1024).toFixed(1)}MB, after: ${(memAfter / 1024 / 1024).toFixed(1)}MB) ---\n` + ); + }); + + it('resilience: worst-case layout (400 components) completes in reasonable time', function () { + this.timeout(60000); + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + + const layout = buildLayout(400); + const start = performance.now(); + for (let i = 0; i < 100; i++) { + rewriteEdgeHostInResponse(layout); + } + const elapsed = performance.now() - start; + const msPerCall = elapsed / 100; + + expect(msPerCall, '400 components should complete in < 15ms per call').to.be.lessThan(15); + console.log( + `\n--- Worst-case (400 components): ${msPerCall.toFixed(3)}ms per call ---\n` + ); + }); + + it('correctness: no false positives - similar but non-Edge URLs unchanged', function () { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + + const layout = { + a: 'https://other-cdn.com/path/edge-platform/image.jpg', + b: 'https://my-edge-store.example.com/media/file.jpg', + c: 'https://edge-platform.sitecorecloud.io/real-edge/media.jpg', + }; + + const result = rewriteEdgeHostInResponse(layout) as typeof layout; + + expect(result.a).to.equal('https://other-cdn.com/path/edge-platform/image.jpg'); + expect(result.b).to.equal('https://my-edge-store.example.com/media/file.jpg'); + expect(result.c).to.equal('https://custom.example.com/real-edge/media.jpg'); + }); + }); + describe('containsDefaultEdgeHost()', () => { it('should return true for edge-platform.sitecorecloud.io', () => { expect( diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index c8cb9ed406..3e875992c4 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -1,17 +1,8 @@ -import { hasCustomEdgeHostname, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; - -/** - * The default Edge Platform hostnames that may appear in responses. - * These will be replaced with the custom hostname when configured. - * Includes production and staging so layout content (e.g. media URLs) can be rewritten. - * @internal - */ -const DEFAULT_EDGE_HOSTNAMES = [ - 'edge-platform.sitecorecloud.io', - 'edge.sitecorecloud.io', // Legacy hostname included for safety replacement - 'edge-staging.sitecore-staging.cloud', - 'edge-platform-staging.sitecore-staging.cloud', -]; +import { + DEFAULT_EDGE_HOSTNAMES, + hasCustomEdgeHostname, + resolveEdgeUrl, +} from '@sitecore-content-sdk/core/tools'; /** * Regular expression patterns for matching Edge hostnames in URLs. @@ -37,6 +28,12 @@ function escapeRegExp(input: string): string { * This function performs a deep traversal of the object and replaces any string values * containing the default Edge hostnames with the custom hostname. * Only performs rewriting when a custom Edge hostname is configured via environment variables. + * + * Use case: Experience Edge returns Layout Service output (layout, placeholders, component fields). + * Field values can contain URLs with the Edge hostname—e.g. Image field `value.src` + * (`https://edge-platform.sitecorecloud.io/-/media/...`), Rich Text HTML (``), + * or link `href`. When using a custom hostname (e.g. CDN in front of Edge), these URLs + * must be rewritten so layout API and media requests both go through the custom host. * @param {T} response - The response object to process (typically LayoutServiceData) * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) * @public @@ -115,6 +112,49 @@ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { return result; } +/** + * Returns the default media URL transformer: rewrites Edge hostnames when custom hostname is configured. + * @returns {(value: string) => string} Transformer function; returns string unchanged when no custom hostname + * @internal + */ +export function getDefaultMediaUrlTransformer(): (value: string) => string { + if (!hasCustomEdgeHostname()) { + return (s) => s; + } + const customEdgeUrl = resolveEdgeUrl(); + return (s) => rewriteEdgeHostInString(s, customEdgeUrl); +} + +/** + * Deeply traverses a value and applies a string transformer to every string. + * @param {T} value - Value to process (layout, object, array, string) + * @param {(s: string) => string} transform - Function that transforms each string + * @returns {T} New value with transformed strings + * @internal + */ +export function applyMediaUrlRewrite(value: T, transform: (s: string) => string): T { + if (value === null || value === undefined) { + return value; + } + if (typeof value === 'string') { + return transform(value) as T; + } + if (Array.isArray(value)) { + return value.map((item) => applyMediaUrlRewrite(item, transform)) as T; + } + if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) { + const result: Record = {}; + for (const key of Object.keys(value as Record)) { + result[key] = applyMediaUrlRewrite( + (value as Record)[key], + transform + ); + } + return result as T; + } + return value; +} + /** * Checks if a string contains any default Edge Platform hostnames. * @param {string} str - The string to check diff --git a/packages/content/src/layout/themes.ts b/packages/content/src/layout/themes.ts index 598fb84dbf..845e2e7649 100644 --- a/packages/content/src/layout/themes.ts +++ b/packages/content/src/layout/themes.ts @@ -1,4 +1,4 @@ -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, LayoutServiceData, RouteData, getFieldValue } from '.'; import { HTMLLink } from '../models'; @@ -13,7 +13,7 @@ const STYLES_LIBRARY_ID_REGEX = /-library--([^\s]+)/; * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. * @returns {HTMLLink[]} library stylesheet links * @public */ @@ -39,7 +39,7 @@ export const getStylesheetUrl = ( sitecoreEdgeContextId: string, sitecoreEdgeUrl?: string ) => { - return `${resolveEdgeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; + return `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; }; /** diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 72a013ad2d..1f8aab9956 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -39,6 +39,7 @@ export { ClientError } declare namespace constants { export { SITECORE_EDGE_URL_DEFAULT, + DEFAULT_EDGE_HOSTNAMES, CLAIMS, DEFAULT_SITECORE_AUTH_DOMAIN, DEFAULT_SITECORE_AUTH_AUDIENCE, @@ -66,6 +67,9 @@ export const debugModule: debug_3.Debug & { // @public export const debugNamespace = "content-sdk"; +// @public +export const DEFAULT_EDGE_HOSTNAMES: readonly ["edge-platform.sitecorecloud.io", "edge.sitecorecloud.io", "edge-staging.sitecore-staging.cloud", "edge-platform-staging.sitecore-staging.cloud"]; + // @internal const DEFAULT_SITECORE_AUTH_AUDIENCE = "https://api.sitecorecloud.io"; @@ -281,9 +285,6 @@ export interface RetryStrategy { // @internal export function setCache(key: string, data: unknown): void; -// @public -export const SITECORE_EDGE_HOSTNAME_ENV = "SITECORE_EDGE_HOSTNAME"; - // @public export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = "NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME"; @@ -309,7 +310,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:42:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index d2cd828fb6..dfc160ee91 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -4,6 +4,24 @@ */ export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; +/** + * Default Edge Platform hostnames that may appear in layout/editing responses. + * Used when rewriting URLs to a custom hostname. Includes production and staging. + * + * These hostnames can appear in any string in the response, including: + * - Media URLs (Image field value.src, Rich Text ) + * - Link field href values + * - Other URL fields in component data + * + * @public + */ +export const DEFAULT_EDGE_HOSTNAMES = [ + 'edge-platform.sitecorecloud.io', + 'edge.sitecorecloud.io', + 'edge-staging.sitecore-staging.cloud', + 'edge-platform-staging.sitecore-staging.cloud', +] as const; + /** * Claims URL * @internal diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 1d304e5891..4b7842853a 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -6,12 +6,12 @@ export * from './metadata'; export { default as isServer } from './is-server'; export { ensurePathExists } from './ensurePath'; export { normalizeUrl } from './normalize-url'; +export { DEFAULT_EDGE_HOSTNAMES } from '../constants'; export { resolveEdgeUrl, resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, - SITECORE_EDGE_HOSTNAME_ENV, SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, SITECORE_EDGE_URL_ENV, SITECORE_EDGE_URL_PUBLIC_ENV, diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index b85c986a90..9a836c9733 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -5,7 +5,6 @@ import { resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, - SITECORE_EDGE_HOSTNAME_ENV, SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, SITECORE_EDGE_URL_ENV, SITECORE_EDGE_URL_PUBLIC_ENV, @@ -16,8 +15,6 @@ describe('resolveEdgeUrl', () => { const originalEnv = { ...process.env }; beforeEach(() => { - // Clear all relevant env vars before each test - delete process.env[SITECORE_EDGE_HOSTNAME_ENV]; delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; delete process.env[SITECORE_EDGE_URL_ENV]; delete process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; @@ -30,7 +27,7 @@ describe('resolveEdgeUrl', () => { describe('resolveEdgeUrl()', () => { it('should return explicit edgeUrl parameter when provided', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const result = resolveEdgeUrl('https://explicit.example.com'); expect(result).to.equal('https://explicit.example.com'); }); @@ -40,43 +37,36 @@ describe('resolveEdgeUrl', () => { expect(result).to.equal('https://explicit.example.com'); }); - it('should use SITECORE_EDGE_HOSTNAME when set (hostname only)', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'my-tenant.edge.example.com'; + it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set (hostname only)', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'my-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should use SITECORE_EDGE_HOSTNAME when set (full URL)', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com'; + it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set (full URL)', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://my-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should normalize trailing slash from SITECORE_EDGE_HOSTNAME', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com/'; + it('should normalize trailing slash from hostname env var', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://my-tenant.edge.example.com/'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should trim whitespace from SITECORE_EDGE_HOSTNAME', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = ' my-tenant.edge.example.com '; + it('should trim whitespace from hostname env var', () => { + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = ' my-tenant.edge.example.com '; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME as fallback', () => { + it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set', () => { process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'public-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://public-tenant.edge.example.com'); }); - it('should prefer SITECORE_EDGE_HOSTNAME over NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'server-tenant.edge.example.com'; - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'public-tenant.edge.example.com'; - const result = resolveEdgeUrl(); - expect(result).to.equal('https://server-tenant.edge.example.com'); - }); - it('should use SITECORE_EDGE_URL when no hostname is set', () => { process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-url.example.com'; const result = resolveEdgeUrl(); @@ -90,7 +80,7 @@ describe('resolveEdgeUrl', () => { }); it('should prefer hostname env var over URL env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'hostname.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'hostname.example.com'; process.env[SITECORE_EDGE_URL_ENV] = 'https://url.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://hostname.example.com'); @@ -102,25 +92,25 @@ describe('resolveEdgeUrl', () => { }); it('should treat the string "undefined" as an unset hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'undefined'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'undefined'; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should treat the string "null" as an unset hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'null'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'null'; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should treat whitespace-only hostname env var as unset', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = ' '; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = ' '; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should handle http protocol in hostname', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'http://insecure.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'http://insecure.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('http://insecure.example.com'); }); @@ -131,11 +121,6 @@ describe('resolveEdgeUrl', () => { expect(hasCustomEdgeHostname()).to.be.false; }); - it('should return true when SITECORE_EDGE_HOSTNAME is set', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; - expect(hasCustomEdgeHostname()).to.be.true; - }); - it('should return true when NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME is set', () => { process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; expect(hasCustomEdgeHostname()).to.be.true; @@ -153,7 +138,7 @@ describe('resolveEdgeUrl', () => { }); it('should return resolved URL when custom hostname is configured', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; expect(getCustomEdgeUrl()).to.equal('https://custom.example.com'); }); }); @@ -171,14 +156,14 @@ describe('resolveEdgeUrl', () => { }); it('should ignore custom hostname and use URL env when set', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-platform.example.com'; const result = resolveEdgeUrlForStaticFiles(); expect(result).to.equal('https://edge-platform.example.com'); }); it('should ignore custom hostname and return default when URL env not set', () => { - process.env[SITECORE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const result = resolveEdgeUrlForStaticFiles(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index a1e72e6729..354f84b3c9 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -2,15 +2,8 @@ import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; import { normalizeUrl } from './normalize-url'; /** - * Environment variable name for the custom Edge hostname (server-side). - * When set, this hostname replaces the default Edge Platform hostname. - * @public - */ -export const SITECORE_EDGE_HOSTNAME_ENV = 'SITECORE_EDGE_HOSTNAME'; - -/** - * Environment variable name for the custom Edge hostname (client-side / browser). - * Required for Next.js client bundles where server-only env vars are not available. + * Environment variable name for the custom Edge hostname. + * Available on both server and client (e.g. NEXT_PUBLIC_* in Next.js). * @public */ export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME'; @@ -32,7 +25,7 @@ export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; * * Priority order: * 1. Explicit `edgeUrl` parameter (if provided and not empty) - * 2. `SITECORE_EDGE_HOSTNAME` / `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` environment variable + * 2. `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` environment variable * 3. `SITECORE_EDGE_URL` / `NEXT_PUBLIC_SITECORE_EDGE_URL` environment variable * 4. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) * @@ -56,20 +49,15 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeUrl(explicit); } - // Determine if we're in browser context - const isBrowser = typeof window !== 'undefined'; - - // Check for custom hostname env var (prioritize custom hostname over URL) - const hostnameEnvVarRaw = isBrowser - ? process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] - : process.env[SITECORE_EDGE_HOSTNAME_ENV] || process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - + // Check for custom hostname env var (available on both server and client) + const hostnameEnvVarRaw = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; const hostnameEnvVar = normalizeMaybeEnvValue(hostnameEnvVarRaw); if (hostnameEnvVar) { return normalizeHostnameToUrl(hostnameEnvVar); } // Check for Edge URL env var + const isBrowser = typeof window !== 'undefined'; const urlEnvVarRaw = isBrowser ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; @@ -146,15 +134,7 @@ function normalizeMaybeEnvValue(value: string | undefined): string | undefined { * @public */ export function hasCustomEdgeHostname(): boolean { - const isBrowser = typeof window !== 'undefined'; - - if (isBrowser) { - return !!normalizeMaybeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); - } - - return !!normalizeMaybeEnvValue( - process.env[SITECORE_EDGE_HOSTNAME_ENV] || process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] - ); + return !!normalizeMaybeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); } /** diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example index 2a30f59747..07d7f83866 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.container.example @@ -17,12 +17,6 @@ NEXT_PUBLIC_SITECORE_API_KEY= # Your Sitecore API hostname is needed to build the app. NEXT_PUBLIC_SITECORE_API_HOST= -# Optional: custom Experience Edge hostname override (XM Cloud/Edge). -SITECORE_EDGE_HOSTNAME= - -# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). -NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= - # Sitecore Content SDK npm packages utilize the debug module for debug logging. # https://www.npmjs.com/package/debug # Set the DEBUG environment variable to 'content-sdk:*' to see all logs: diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index 18bd18a59e..b4ba0ac02a 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -20,9 +20,7 @@ SITECORE_EDGE_CONTEXT_ID= NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). -SITECORE_EDGE_HOSTNAME= - -# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). +# Available on both server and client. NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= # An optional Sitecore Personalize scope identifier. diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts index a8f28817fe..24de77be3d 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts @@ -4,7 +4,4 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config'; * See the documentation for `defineConfig`: * https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html */ -export default defineConfig({ - // Enable to use custom hostname for content/media URLs (default: false) - // rewriteContentUrls: true, -}); +export default defineConfig({}); diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example index 2a30f59747..07d7f83866 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.container.example @@ -17,12 +17,6 @@ NEXT_PUBLIC_SITECORE_API_KEY= # Your Sitecore API hostname is needed to build the app. NEXT_PUBLIC_SITECORE_API_HOST= -# Optional: custom Experience Edge hostname override (XM Cloud/Edge). -SITECORE_EDGE_HOSTNAME= - -# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). -NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= - # Sitecore Content SDK npm packages utilize the debug module for debug logging. # https://www.npmjs.com/package/debug # Set the DEBUG environment variable to 'content-sdk:*' to see all logs: diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index 18bd18a59e..b4ba0ac02a 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -20,9 +20,7 @@ SITECORE_EDGE_CONTEXT_ID= NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). -SITECORE_EDGE_HOSTNAME= - -# Optional: custom Experience Edge hostname override for client-side use (XM Cloud/Edge). +# Available on both server and client. NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= # An optional Sitecore Personalize scope identifier. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts index a8f28817fe..24de77be3d 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts @@ -4,7 +4,4 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config'; * See the documentation for `defineConfig`: * https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html */ -export default defineConfig({ - // Enable to use custom hostname for content/media URLs (default: false) - // rewriteContentUrls: true, -}); +export default defineConfig({}); diff --git a/packages/nextjs/src/config/define-config.ts b/packages/nextjs/src/config/define-config.ts index 2dd3dd3a36..299ae66a0c 100644 --- a/packages/nextjs/src/config/define-config.ts +++ b/packages/nextjs/src/config/define-config.ts @@ -3,7 +3,12 @@ import { defineConfig as defineConfigCore, SitecoreConfigInput as SitecoreConfigInputCore, } from '@sitecore-content-sdk/content/config'; -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { + resolveEdgeUrl, + SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, + SITECORE_EDGE_URL_ENV, + SITECORE_EDGE_URL_PUBLIC_ENV, +} from '@sitecore-content-sdk/core/tools'; /** * Provides default NextJs initial values from env variables for SitecoreConfig @@ -20,7 +25,12 @@ export const getNextFallbackConfig = (config?: SitecoreConfigInput): SitecoreCon contextId: config?.api?.edge?.contextId || '', clientContextId: config?.api?.edge?.clientContextId || process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: resolveEdgeUrl(config?.api?.edge?.edgeUrl), + edgeUrl: resolveEdgeUrl( + config?.api?.edge?.edgeUrl ?? + process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? + process.env[SITECORE_EDGE_URL_ENV] + ), }, local: { ...config?.api?.local, diff --git a/packages/search/src/search-service.ts b/packages/search/src/search-service.ts index e5691890e4..3b28bf1658 100644 --- a/packages/search/src/search-service.ts +++ b/packages/search/src/search-service.ts @@ -101,7 +101,7 @@ export class SearchService { private fetcher: NativeDataFetcher; constructor(private config: SearchServiceConfig) { - this.config.edgeUrl = resolveEdgeUrl(this.config.edgeUrl); + this.config.edgeUrl = this.config.edgeUrl ?? resolveEdgeUrl(); this.fetcher = new NativeDataFetcher({ debugger: debug, From 7ed5bb48b318d678703a026cd2cd923c54f21391 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Fri, 13 Feb 2026 10:22:19 +0200 Subject: [PATCH 15/25] Fix test related issue --- .../src/layout/rewrite-edge-host.test.ts | 206 +----------------- 1 file changed, 1 insertion(+), 205 deletions(-) diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index ffdab2b246..d54d909ffa 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -1,5 +1,4 @@ /* eslint-disable no-unused-expressions */ -import { performance } from 'perf_hooks'; import { expect } from 'chai'; import { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; import { SITECORE_EDGE_HOSTNAME_PUBLIC_ENV } from '@sitecore-content-sdk/core/tools'; @@ -205,211 +204,8 @@ describe('rewriteEdgeHostInResponse', () => { '

Image:

' ); }); - }); - - describe('performance benchmarks', () => { - const ITERATIONS = 500; - - /** Build a layout with N placeholder components (mix of Image + RichText). */ - function buildLayout(componentCount: number): Record { - const edgeUrl = 'https://edge-platform.sitecorecloud.io'; - const components = []; - for (let i = 0; i < componentCount; i++) { - components.push({ - componentName: i % 2 === 0 ? 'Image' : 'RichText', - fields: - i % 2 === 0 - ? { - Image: { - value: { - src: `${edgeUrl}/tenant/media/image-${i}.jpg`, - alt: `Image ${i}`, - }, - }, - } - : { - content: { - value: `

Block ${i}: and link

`, - }, - }, - }); - } - return { - sitecore: { - context: { site: { name: 'test' }, language: 'en' }, - route: { - name: 'page', - placeholders: { main: components }, - fields: { - title: { value: `Page with ${componentCount} components` }, - image: { - value: { src: `${edgeUrl}/tenant/media/hero.jpg` }, - }, - }, - }, - }, - }; - } - - it('benchmark: rewrite timing vs layout size (with custom hostname)', function () { - this.timeout(30000); - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - - const sizes = [ - { name: 'small (10 components)', count: 10 }, - { name: 'medium (50 components)', count: 50 }, - { name: 'large (150 components)', count: 150 }, - ]; - - const results: { size: string; msPerCall: number; totalMs: number; iterations: number }[] = []; - - for (const { name, count } of sizes) { - const layout = buildLayout(count); - const start = performance.now(); - for (let i = 0; i < ITERATIONS; i++) { - rewriteEdgeHostInResponse(layout); - } - const totalMs = performance.now() - start; - const msPerCall = totalMs / ITERATIONS; - results.push({ size: name, msPerCall, totalMs, iterations: ITERATIONS }); - } - - // Assert all under 5ms per call (reasonable for CI) - for (const r of results) { - expect(r.msPerCall, `${r.size} should be < 5ms per call`).to.be.lessThan(5); - } - - // Log metrics for presentation - console.log('\n--- rewriteEdgeHostInResponse performance (custom hostname enabled) ---'); - for (const r of results) { - console.log(` ${r.size}: ${r.msPerCall.toFixed(3)}ms per call (${r.iterations} iterations, ${r.totalMs.toFixed(0)}ms total)`); - } - console.log('---\n'); - }); - - it('benchmark: no-op when custom hostname disabled (baseline)', function () { - this.timeout(15000); - delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - - const layout = buildLayout(100); - const start = performance.now(); - for (let i = 0; i < ITERATIONS; i++) { - rewriteEdgeHostInResponse(layout); - } - const totalMs = performance.now() - start; - const msPerCall = totalMs / ITERATIONS; - - expect(msPerCall).to.be.lessThan(1); - console.log( - `\n--- rewriteEdgeHostInResponse when disabled (early return): ${msPerCall.toFixed(4)}ms per call (${ITERATIONS} iterations) ---\n` - ); - }); - - it('benchmark: comparison - rewrite enabled vs disabled', function () { - this.timeout(20000); - const layout = buildLayout(75); - const iterations = 1000; - - // Disabled - delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - const startDisabled = performance.now(); - for (let i = 0; i < iterations; i++) { - rewriteEdgeHostInResponse(layout); - } - const disabledMs = performance.now() - startDisabled; - - // Enabled - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - const startEnabled = performance.now(); - for (let i = 0; i < iterations; i++) { - rewriteEdgeHostInResponse(layout); - } - const enabledMs = performance.now() - startEnabled; - - const disabledPerCall = disabledMs / iterations; - const enabledPerCall = enabledMs / iterations; - const overheadMs = enabledPerCall - disabledPerCall; - - console.log('\n--- Comparison (75 components, 1000 iterations) ---'); - console.log(` Disabled (early return): ${disabledPerCall.toFixed(4)}ms per call`); - console.log(` Enabled (full rewrite): ${enabledPerCall.toFixed(4)}ms per call`); - console.log(` Overhead per request: ${overheadMs.toFixed(4)}ms`); - console.log('---\n'); - - expect(enabledPerCall).to.be.lessThan(2); - }); - - it('resilience: regex completes quickly on long strings (no catastrophic backtracking)', function () { - this.timeout(5000); - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - - // Long string with many Edge URLs (could trigger backtracking with bad regex) - const urls = Array(100).fill('https://edge-platform.sitecorecloud.io/tenant/media/image.jpg').join(' '); - const layout = { sitecore: { route: { fields: { content: { value: urls } } } } }; - - const start = performance.now(); - for (let i = 0; i < 100; i++) { - rewriteEdgeHostInResponse(layout); - } - const elapsed = performance.now() - start; - const msPerCall = elapsed / 100; - - expect(msPerCall, '100 URLs in one string should complete in < 50ms per call').to.be.lessThan(50); - const result = rewriteEdgeHostInResponse(layout) as typeof layout; - expect((result.sitecore.route.fields.content.value as string).includes('custom.example.com')).to.be - .true; - expect((result.sitecore.route.fields.content.value as string).includes('edge-platform.sitecorecloud.io')) - .to.be.false; - console.log(`\n--- Regex resilience: 100 URLs in one string = ${msPerCall.toFixed(2)}ms per call ---\n`); - }); - - it('resilience: memory stable under repeated rewrites', function () { - this.timeout(30000); - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - - const layout = buildLayout(80); - const iterations = 2000; - - if (global.gc) { - global.gc(); - } - const memBefore = process.memoryUsage().heapUsed; - - for (let i = 0; i < iterations; i++) { - rewriteEdgeHostInResponse(layout); - } - - if (global.gc) { - global.gc(); - } - const memAfter = process.memoryUsage().heapUsed; - const growthMb = (memAfter - memBefore) / 1024 / 1024; - - expect(growthMb, 'memory growth should be < 50MB after 2000 rewrites').to.be.lessThan(50); - console.log( - `\n--- Memory: ${growthMb.toFixed(2)}MB growth after ${iterations} rewrites (heap before: ${(memBefore / 1024 / 1024).toFixed(1)}MB, after: ${(memAfter / 1024 / 1024).toFixed(1)}MB) ---\n` - ); - }); - - it('resilience: worst-case layout (400 components) completes in reasonable time', function () { - this.timeout(60000); - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - - const layout = buildLayout(400); - const start = performance.now(); - for (let i = 0; i < 100; i++) { - rewriteEdgeHostInResponse(layout); - } - const elapsed = performance.now() - start; - const msPerCall = elapsed / 100; - - expect(msPerCall, '400 components should complete in < 15ms per call').to.be.lessThan(15); - console.log( - `\n--- Worst-case (400 components): ${msPerCall.toFixed(3)}ms per call ---\n` - ); - }); - it('correctness: no false positives - similar but non-Edge URLs unchanged', function () { + it('should not rewrite similar but non-Edge URLs (no false positives)', () => { process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const layout = { From 1191572f5521a02228d93d14d47ffc0c1bdf5b3a Mon Sep 17 00:00:00 2001 From: MenKNas Date: Fri, 13 Feb 2026 10:29:08 +0200 Subject: [PATCH 16/25] Fix api-extractor issues and html related false warning --- api-extractor.json | 3 +++ packages/content/api/content-sdk-content.api.md | 8 +++++++- packages/core/api/content-sdk-core.api.md | 2 +- packages/core/src/constants.ts | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/api-extractor.json b/api-extractor.json index 3bc9e928ce..be99d360d9 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -67,6 +67,9 @@ "tsdoc-html-tag-missing-greater-than": { "logLevel": "none" }, + "tsdoc-html-tag-missing-equals": { + "logLevel": "none" + }, "tsdoc-at-sign-in-word": { "logLevel": "none" } diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index b9302bc78a..ee2233a763 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -30,6 +30,9 @@ export const addServerComponentPreviewHandler: (callback: (eventArgs: ComponentP // @internal export function addStyleElement(stylesContent: string): void; +// @internal +export function applyMediaUrlRewrite(value: T, transform: (s: string) => string): T; + // @public export class CdpHelper { static getComponentFriendlyId(pageId: string, componentId: string, language: string, scope?: string): string; @@ -535,6 +538,9 @@ export const getContentSdkPagesClientData: () => Record HTMLLink | null; +// @internal +export function getDefaultMediaUrlTransformer(): (value: string) => string; + // Warning: (ae-forgotten-export) The symbol "DesignLibraryComponentPreviewErrorEvent" needs to be exported by the entry point api-surface.d.ts // // @internal @@ -1347,7 +1353,7 @@ export type WriteImportMapArgsInternal = WriteImportMapArgs & { // Warnings were encountered during analysis: // -// src/client/sitecore-client.ts:60:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts +// src/client/sitecore-client.ts:62:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts // src/editing/codegen/preview.ts:108:5 - (ae-forgotten-export) The symbol "ComponentImport_2" needs to be exported by the entry point api-surface.d.ts // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "ComponentMapTemplate" which is marked as @internal // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "EnhancedComponentMapTemplate" which is marked as @internal diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 1f8aab9956..3ae4638ff2 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -310,7 +310,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:42:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index dfc160ee91..e7fcefb533 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -9,7 +9,7 @@ export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io * Used when rewriting URLs to a custom hostname. Includes production and staging. * * These hostnames can appear in any string in the response, including: - * - Media URLs (Image field value.src, Rich Text ) + * - Media URLs (Image field value.src, Rich Text markup) * - Link field href values * - Other URL fields in component data * From 7b92bc5c0a785c65af075bc06bac1e428365227d Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 16 Feb 2026 17:51:40 +0200 Subject: [PATCH 17/25] Address PR comments 2 --- packages/content/src/client/edge-proxy.ts | 1 + .../src/client/sitecore-client.test.ts | 75 ++++++++++--------- .../content/src/client/sitecore-client.ts | 14 +++- packages/content/src/config/define-config.ts | 22 ++---- .../src/layout/rewrite-edge-host.test.ts | 8 ++ .../content/src/layout/rewrite-edge-host.ts | 45 ++++++++--- packages/core/src/constants.ts | 1 - .../core/src/tools/normalize-env-value.ts | 19 +++++ packages/core/src/tools/resolve-edge-url.ts | 46 ++++-------- 9 files changed, 134 insertions(+), 97 deletions(-) create mode 100644 packages/core/src/tools/normalize-env-value.ts diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index 84a636a479..527239c118 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -3,6 +3,7 @@ import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; /** * Resolves the base Edge URL: uses provided value (from config) or falls back to resolveEdgeUrl. * Normalizes trailing slash when a value is provided. + * @param {string} [sitecoreEdgeUrl] - The base Edge URL from config; when not provided, resolves from env vars. * @internal */ const getBaseEdgeUrl = (sitecoreEdgeUrl?: string): string => diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index c11cdb78f0..8abe9d6159 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -5,7 +5,6 @@ import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import { DocumentNode } from 'graphql'; import { DefaultRetryStrategy, NativeDataFetcher } from '@sitecore-content-sdk/core'; -import { SITECORE_EDGE_HOSTNAME_PUBLIC_ENV } from '@sitecore-content-sdk/core/tools'; import { ErrorPage, SitecoreClient } from './sitecore-client'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; @@ -510,39 +509,40 @@ describe('SitecoreClient', () => { }); it('should apply default Edge host rewrite when rewriteMediaUrls is true and custom hostname is set', async () => { - const originalEnv = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - try { - const path = '/test/path'; - const locale = 'en-US'; - const siteInfo = { name: 'default-site', hostName: 'example.com', language: 'en' }; - const rawLayout = { - sitecore: { - route: { - name: 'home', - placeholders: {}, - fields: { - image: { value: { src: 'https://edge-platform.sitecorecloud.io/-/media/hero.jpg' } }, - }, + const path = '/test/path'; + const locale = 'en-US'; + const siteInfo = { name: 'default-site', hostName: 'example.com', language: 'en' }; + const rawLayout = { + sitecore: { + route: { + name: 'home', + placeholders: {}, + fields: { + image: { value: { src: 'https://edge-platform.sitecorecloud.io/-/media/hero.jpg' } }, }, - context: { site: siteInfo, pageState: LayoutServicePageState.Normal }, }, - }; - layoutServiceStub.fetchLayoutData.returns(rawLayout); - const clientWithRewrite = new SitecoreClient({ - ...defaultInitOptions, - rewriteMediaUrls: true, - } as any); + context: { site: siteInfo, pageState: LayoutServicePageState.Normal }, + }, + }; + layoutServiceStub.fetchLayoutData.returns(rawLayout); + const clientWithRewrite = new SitecoreClient({ + ...defaultInitOptions, + api: { + ...defaultInitOptions.api, + edge: { + ...defaultInitOptions.api.edge, + edgeUrl: 'https://custom.example.com', + }, + }, + rewriteMediaUrls: true, + } as any); (clientWithRewrite as any).layoutService = layoutServiceStub; - const result = await clientWithRewrite.getPage(path, { locale }); + const result = await clientWithRewrite.getPage(path, { locale }); - expect( - (result?.layout.sitecore.route?.fields?.image?.value as { src: string }).src - ).to.equal('https://custom.example.com/-/media/hero.jpg'); - } finally { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = originalEnv; - } + expect( + (result?.layout.sitecore.route?.fields?.image?.value as { src: string }).src + ).to.equal('https://custom.example.com/-/media/hero.jpg'); }); it('should pass fetchOptions to layoutService when calling getPage', async () => { @@ -1525,8 +1525,16 @@ describe('SitecoreClient', () => { }); it('should rewrite Edge hostnames in sitemap path and XML when custom hostname is configured', async () => { - const originalEnv = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://custom.example.com'; + const clientWithCustomEdge = new SitecoreClient({ + ...defaultInitOptions, + api: { + ...defaultInitOptions.api, + edge: { + ...defaultInitOptions.api.edge, + edgeUrl: 'https://custom.example.com', + }, + }, + } as any); const edgeSitemapPath = 'https://edge-platform.sitecorecloud.io/sitemap.xml'; const xmlContent = @@ -1537,12 +1545,11 @@ describe('SitecoreClient', () => { .stub(NativeDataFetcher.prototype, 'fetch') .resolves({ data: xmlContent, status: 200, statusText: 'OK' }); - const result = await sitecoreClient.getSiteMap({ ...defaultReqConfig }); + const result = await clientWithCustomEdge.getSiteMap({ ...defaultReqConfig }); + expect(getGraphqlSitemapXMLServiceStub.calledWith(defaultReqConfig.siteName)).to.be.true; expect(dataFetcherStub.calledWith('https://custom.example.com/sitemap.xml')).to.be.true; expect(result).to.include('https://custom.example.com/a'); - - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = originalEnv; }); it('should fetch specific sitemap when ID is provided', async () => { diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 7583b1d701..2632f5398f 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -633,13 +633,19 @@ export class SitecoreClient implements BaseSitecoreClient { // regular sitemap if (sitemapPath) { try { - const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath); + const rewrittenSitemapPath = rewriteEdgeHostInResponse( + sitemapPath, + this.initOptions.api.edge.edgeUrl + ); const fetcher = new NativeDataFetcher(); const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); if (!xmlResponse.data) { throw new Error('REDIRECT_404'); } - return rewriteEdgeHostInResponse(xmlResponse.data); + return rewriteEdgeHostInResponse( + xmlResponse.data, + this.initOptions.api.edge.edgeUrl + ); // eslint-disable-next-line no-unused-vars } catch (error) { throw new Error('REDIRECT_404'); @@ -725,7 +731,9 @@ export class SitecoreClient implements BaseSitecoreClient { return layout; } const transformer = - opt === true ? getDefaultMediaUrlTransformer() : opt; + opt === true + ? getDefaultMediaUrlTransformer(this.initOptions.api.edge.edgeUrl) + : opt; return applyMediaUrlRewrite(layout, transformer); } diff --git a/packages/content/src/config/define-config.ts b/packages/content/src/config/define-config.ts index 096aec599d..92d9712a68 100644 --- a/packages/content/src/config/define-config.ts +++ b/packages/content/src/config/define-config.ts @@ -1,10 +1,5 @@ import { DefaultRetryStrategy } from '@sitecore-content-sdk/core'; -import { - resolveEdgeUrl, - SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, - SITECORE_EDGE_URL_ENV, - SITECORE_EDGE_URL_PUBLIC_ENV, -} from '@sitecore-content-sdk/core/tools'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { DeepPartial, SitecoreConfig, SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; @@ -17,11 +12,7 @@ export const getFallbackConfig = (): SitecoreConfig => ({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: '', - edgeUrl: resolveEdgeUrl( - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_ENV] - ), + edgeUrl: resolveEdgeUrl(), }, local: { apiKey: process.env.SITECORE_API_KEY || process.env.NEXT_PUBLIC_SITECORE_API_KEY || '', @@ -120,12 +111,9 @@ const resolveConfig = (base: SitecoreConfig, override: SitecoreConfigInput): Sit result.personalize.edgeTimeout = base.personalize.edgeTimeout; } // Resolve edge URL at config level so consumers use the resolved value directly - result.api.edge.edgeUrl = resolveEdgeUrl( - result.api.edge.edgeUrl ?? - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_ENV] - ); + result.api.edge.edgeUrl = result.api.edge.edgeUrl + ? resolveEdgeUrl(result.api.edge.edgeUrl) + : resolveEdgeUrl(); return result; }; diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index d54d909ffa..c6dabb1e59 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -24,6 +24,14 @@ describe('rewriteEdgeHostInResponse', () => { expect(result).to.deep.equal(response); }); + it('should rewrite when edgeUrl is provided from config (no env vars)', () => { + const response = { + url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + }; + const result = rewriteEdgeHostInResponse(response, 'https://custom.edge.example.com'); + expect(result.url).to.equal('https://custom.edge.example.com/media/image.jpg'); + }); + it('should rewrite edge-platform.sitecorecloud.io in string values', () => { process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index 3e875992c4..aa72d625b8 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -4,6 +4,22 @@ import { resolveEdgeUrl, } from '@sitecore-content-sdk/core/tools'; +/** + * Returns true if the given URL has a custom (non-default) Edge hostname. + * @param {string} url - Full URL or hostname + * @returns {boolean} True if URL host is not a default Edge hostname + * @internal + */ +function isCustomEdgeUrl(url: string): boolean { + try { + const u = url.startsWith('http') ? new URL(url) : new URL(`https://${url}`); + const host = u.hostname.toLowerCase(); + return !DEFAULT_EDGE_HOSTNAMES.some((h) => host === h); + } catch { + return false; + } +} + /** * Regular expression patterns for matching Edge hostnames in URLs. * Matches both http:// and https:// protocols. @@ -27,7 +43,7 @@ function escapeRegExp(input: string): string { * Rewrites Edge Platform hostnames in a response object to use the custom hostname. * This function performs a deep traversal of the object and replaces any string values * containing the default Edge hostnames with the custom hostname. - * Only performs rewriting when a custom Edge hostname is configured via environment variables. + * Uses `edgeUrl` when provided (e.g. from config); otherwise resolves from env vars. * * Use case: Experience Edge returns Layout Service output (layout, placeholders, component fields). * Field values can contain URLs with the Edge hostname—e.g. Image field `value.src` @@ -35,20 +51,24 @@ function escapeRegExp(input: string): string { * or link `href`. When using a custom hostname (e.g. CDN in front of Edge), these URLs * must be rewritten so layout API and media requests both go through the custom host. * @param {T} response - The response object to process (typically LayoutServiceData) + * @param {string} [edgeUrl] - Optional Edge URL from config. When provided, used for rewriting instead of env vars. * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) * @public * @example * const layout = await layoutService.fetchLayoutData(path, options); * const rewritten = rewriteEdgeHostInResponse(layout); */ -export function rewriteEdgeHostInResponse(response: T): T { - // Skip if no custom hostname is configured - if (!hasCustomEdgeHostname()) { +export function rewriteEdgeHostInResponse(response: T, edgeUrl?: string): T { + const customEdgeUrl = edgeUrl ? resolveEdgeUrl(edgeUrl) : resolveEdgeUrl(); + const shouldRewrite = + edgeUrl !== undefined && edgeUrl !== '' + ? isCustomEdgeUrl(customEdgeUrl) + : hasCustomEdgeHostname(); + + if (!shouldRewrite) { return response; } - const customEdgeUrl = resolveEdgeUrl(); - return deepRewriteEdgeHost(response, customEdgeUrl); } @@ -114,14 +134,21 @@ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { /** * Returns the default media URL transformer: rewrites Edge hostnames when custom hostname is configured. + * Uses `edgeUrl` when provided (e.g. from config); otherwise resolves from env vars. + * @param {string} [edgeUrl] - Optional Edge URL from config. When provided, used for rewriting instead of env vars. * @returns {(value: string) => string} Transformer function; returns string unchanged when no custom hostname * @internal */ -export function getDefaultMediaUrlTransformer(): (value: string) => string { - if (!hasCustomEdgeHostname()) { +export function getDefaultMediaUrlTransformer(edgeUrl?: string): (value: string) => string { + const customEdgeUrl = edgeUrl ? resolveEdgeUrl(edgeUrl) : resolveEdgeUrl(); + const shouldRewrite = + edgeUrl !== undefined && edgeUrl !== '' + ? isCustomEdgeUrl(customEdgeUrl) + : hasCustomEdgeHostname(); + + if (!shouldRewrite) { return (s) => s; } - const customEdgeUrl = resolveEdgeUrl(); return (s) => rewriteEdgeHostInString(s, customEdgeUrl); } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index e7fcefb533..085b17f769 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -12,7 +12,6 @@ export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io * - Media URLs (Image field value.src, Rich Text markup) * - Link field href values * - Other URL fields in component data - * * @public */ export const DEFAULT_EDGE_HOSTNAMES = [ diff --git a/packages/core/src/tools/normalize-env-value.ts b/packages/core/src/tools/normalize-env-value.ts new file mode 100644 index 0000000000..9b033bcc32 --- /dev/null +++ b/packages/core/src/tools/normalize-env-value.ts @@ -0,0 +1,19 @@ +/** + * Normalizes values that may come from environment variables. + * In Node, setting `process.env.FOO = undefined` results in the string 'undefined', + * which should be treated as if the variable is not set. + * @param {string | undefined} value - Possibly undefined env-like value + * @returns {string | undefined} A usable string value, or undefined if not meaningful + * @internal + */ +export function normalizeEnvValue(value: string | undefined): string | undefined { + if (!value) return undefined; + + const trimmed = value.trim(); + if (!trimmed) return undefined; + + const lowered = trimmed.toLowerCase(); + if (lowered === 'undefined' || lowered === 'null') return undefined; + + return trimmed; +} diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index 354f84b3c9..ba12c4898f 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -1,4 +1,6 @@ import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; +import isServer from './is-server'; +import { normalizeEnvValue } from './normalize-env-value'; import { normalizeUrl } from './normalize-url'; /** @@ -44,25 +46,24 @@ export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; */ export function resolveEdgeUrl(edgeUrl?: string): string { // Use explicit edgeUrl if provided and not empty - const explicit = normalizeMaybeEnvValue(edgeUrl); + const explicit = normalizeEnvValue(edgeUrl); if (explicit) { return normalizeUrl(explicit); } // Check for custom hostname env var (available on both server and client) const hostnameEnvVarRaw = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - const hostnameEnvVar = normalizeMaybeEnvValue(hostnameEnvVarRaw); + const hostnameEnvVar = normalizeEnvValue(hostnameEnvVarRaw); if (hostnameEnvVar) { return normalizeHostnameToUrl(hostnameEnvVar); } // Check for Edge URL env var - const isBrowser = typeof window !== 'undefined'; - const urlEnvVarRaw = isBrowser - ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] - : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + const urlEnvVarRaw = isServer() + ? process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV] + : process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; - const urlEnvVar = normalizeMaybeEnvValue(urlEnvVarRaw); + const urlEnvVar = normalizeEnvValue(urlEnvVarRaw); if (urlEnvVar) { return normalizeUrl(urlEnvVar); } @@ -79,11 +80,10 @@ export function resolveEdgeUrl(edgeUrl?: string): string { * @public */ export function resolveEdgeUrlForStaticFiles(): string { - const isBrowser = typeof window !== 'undefined'; - const urlEnvVarRaw = isBrowser - ? process.env[SITECORE_EDGE_URL_PUBLIC_ENV] - : process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; - const urlEnvVar = normalizeMaybeEnvValue(urlEnvVarRaw); + const urlEnvVarRaw = isServer() + ? process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV] + : process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + const urlEnvVar = normalizeEnvValue(urlEnvVarRaw); if (urlEnvVar) { return normalizeUrl(urlEnvVar); } @@ -108,33 +108,13 @@ function normalizeHostnameToUrl(hostnameOrUrl: string): string { return normalizeUrl(`https://${trimmed}`); } -/** - * Normalizes values that may come from environment variables. - * In Node, setting `process.env.FOO = undefined` results in the string 'undefined', - * which should be treated as if the variable is not set. - * @param {string | undefined} value - Possibly undefined env-like value - * @returns {string | undefined} A usable string value, or undefined if not meaningful - * @internal - */ -function normalizeMaybeEnvValue(value: string | undefined): string | undefined { - if (!value) return undefined; - - const trimmed = value.trim(); - if (!trimmed) return undefined; - - const lowered = trimmed.toLowerCase(); - if (lowered === 'undefined' || lowered === 'null') return undefined; - - return trimmed; -} - /** * Checks if a custom Edge hostname is configured via environment variables. * @returns {boolean} True if a custom hostname is configured * @public */ export function hasCustomEdgeHostname(): boolean { - return !!normalizeMaybeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); + return !!normalizeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); } /** From cadf3fcbab51a0c5c0f5df151d719c5b6e9d937c Mon Sep 17 00:00:00 2001 From: MenKNas Date: Mon, 16 Feb 2026 17:57:11 +0200 Subject: [PATCH 18/25] Fix API extractor issue --- packages/content/api/content-sdk-content.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index ee2233a763..7304d9ac41 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -539,7 +539,7 @@ export const getContentSdkPagesClientData: () => Record HTMLLink | null; // @internal -export function getDefaultMediaUrlTransformer(): (value: string) => string; +export function getDefaultMediaUrlTransformer(edgeUrl?: string): (value: string) => string; // Warning: (ae-forgotten-export) The symbol "DesignLibraryComponentPreviewErrorEvent" needs to be exported by the entry point api-surface.d.ts // @@ -987,7 +987,7 @@ export const resetEditorChromes: () => void; export { RetryStrategy } // @public -export function rewriteEdgeHostInResponse(response: T): T; +export function rewriteEdgeHostInResponse(response: T, edgeUrl?: string): T; // @public export type RobotsQueryResult = { From f34cd5f9f57f504232bbb0301d61e8da65359490 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 17 Feb 2026 13:02:04 +0200 Subject: [PATCH 19/25] Address PR comments 3 --- CHANGELOG.md | 4 +- .../content/api/content-sdk-content.api.md | 6 +- packages/content/src/client/edge-proxy.ts | 26 +++--- .../src/client/sitecore-client.test.ts | 25 ++---- .../content/src/client/sitecore-client.ts | 19 ++--- .../content/src/config/define-config.test.ts | 6 +- .../src/editing/component-layout-service.ts | 8 +- .../content/src/editing/design-library.ts | 13 +-- packages/content/src/layout/content-styles.ts | 12 +-- .../src/layout/rewrite-edge-host.test.ts | 65 +++++---------- .../content/src/layout/rewrite-edge-host.ts | 39 +++------ packages/content/src/layout/themes.ts | 15 ++-- packages/core/api/content-sdk-core.api.md | 14 ++-- packages/core/src/constants.ts | 6 ++ packages/core/src/tools/index.ts | 4 +- .../src/tools/normalize-env-value.test.ts | 51 ++++++++++++ .../core/src/tools/resolve-edge-url.test.ts | 83 +++++-------------- packages/core/src/tools/resolve-edge-url.ts | 52 ++---------- .../.cursor/rules/sitecore.mdc | 5 +- .../nextjs-app-router/.env.remote.example | 6 +- .../nextjs-app-router/.windsurfrules | 5 +- .../src/templates/nextjs-app-router/CLAUDE.md | 5 +- .../nextjs-app-router/copilot-instructions.md | 5 +- .../sitecore.config.ts.example | 4 +- .../nextjs/.cursor/rules/sitecore.mdc | 5 +- .../src/templates/nextjs/.env.remote.example | 6 +- .../src/templates/nextjs/.windsurfrules | 5 +- .../src/templates/nextjs/CLAUDE.md | 5 +- .../templates/nextjs/copilot-instructions.md | 5 +- .../nextjs/sitecore.config.ts.example | 4 +- .../nextjs/src/config/define-config.test.ts | 4 +- packages/nextjs/src/config/define-config.ts | 15 ++-- 32 files changed, 246 insertions(+), 281 deletions(-) create mode 100644 packages/core/src/tools/normalize-env-value.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e6d41f151..d25546e9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ Our versioning strategy is as follows: * `[nextjs]` `[create-content-sdk-app]` Enable Next.js 16 Cache Components and Turbopack File System Caching ([#334](https://github.com/Sitecore/content-sdk/pull/334)) -* `[core]` `[content]` `[nextjs]` Support custom Edge hostnames via `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` ([#359](https://github.com/Sitecore/content-sdk/pull/359)) - - Consolidated `rewriteContentUrls` and `contentRewrite` into `rewriteMediaUrls: boolean | ((value: string) => string)`. When `true`, uses default Edge host rewriter; when a function, transforms each string (SDK traverses layout). Migration: `rewriteContentUrls: true` → `rewriteMediaUrls: true`; custom rewriter → `rewriteMediaUrls: (value: string) => string` +* `[core]` `[content]` `[nextjs]` Support custom Edge hostnames via `SITECORE_EDGE_PLATFORM_HOSTNAME` (Next.js: `NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME`) ([#359](https://github.com/Sitecore/content-sdk/pull/359)) + - New `rewriteMediaUrls` option: when `true`, rewrites layout media URLs to the custom Edge hostname; when a function, applies a custom string transformer. * Search integration ([#295](https://github.com/Sitecore/content-sdk/pull/295)) * `[search]` New `@sitecore-content-sdk/search` package providing search functionality diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index 7304d9ac41..b887c0be7a 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -539,7 +539,7 @@ export const getContentSdkPagesClientData: () => Record HTMLLink | null; // @internal -export function getDefaultMediaUrlTransformer(edgeUrl?: string): (value: string) => string; +export function getDefaultMediaUrlTransformer(edgeUrl: string): (value: string) => string; // Warning: (ae-forgotten-export) The symbol "DesignLibraryComponentPreviewErrorEvent" needs to be exported by the entry point api-surface.d.ts // @@ -987,7 +987,7 @@ export const resetEditorChromes: () => void; export { RetryStrategy } // @public -export function rewriteEdgeHostInResponse(response: T, edgeUrl?: string): T; +export function rewriteEdgeHostInResponse(response: T, edgeUrl: string): T; // @public export type RobotsQueryResult = { @@ -1353,7 +1353,7 @@ export type WriteImportMapArgsInternal = WriteImportMapArgs & { // Warnings were encountered during analysis: // -// src/client/sitecore-client.ts:62:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts +// src/client/sitecore-client.ts:63:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts // src/editing/codegen/preview.ts:108:5 - (ae-forgotten-export) The symbol "ComponentImport_2" needs to be exported by the entry point api-surface.d.ts // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "ComponentMapTemplate" which is marked as @internal // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "EnhancedComponentMapTemplate" which is marked as @internal diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index 527239c118..a18524ea3e 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -1,36 +1,36 @@ -import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { constants } from '@sitecore-content-sdk/core'; +import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; /** - * Resolves the base Edge URL: uses provided value (from config) or falls back to resolveEdgeUrl. - * Normalizes trailing slash when a value is provided. - * @param {string} [sitecoreEdgeUrl] - The base Edge URL from config; when not provided, resolves from env vars. + * Resolves the base Edge URL from config. Caller should pass the resolved Edge URL from config. + * @param {string} [sitecoreEdgeUrl] - The base Edge URL from config. Defaults to platform URL. * @internal */ -const getBaseEdgeUrl = (sitecoreEdgeUrl?: string): string => - sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl(); +const getBaseEdgeUrl = ( + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT +): string => normalizeUrl(sitecoreEdgeUrl); /** * Generates a URL for accessing Sitecore Edge Platform Content using the provided endpoint and context ID. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). - * When not provided, resolves from env vars as fallback. + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). Defaults to platform URL. * @returns {string} The complete URL for accessing content through the Edge Platform. * @public */ -export const getEdgeProxyContentUrl = (sitecoreEdgeUrl?: string) => - `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; +export const getEdgeProxyContentUrl = ( + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT +) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; /** * Generates a URL for accessing Sitecore Edge Platform Forms using the provided form ID and context ID. * @param {string} sitecoreEdgeContextId - The unique context id. * @param {string} formId - The unique form id. - * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). - * When not provided, resolves from env vars as fallback. + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform (resolved at config level). Defaults to platform URL. * @returns {string} The complete URL for accessing forms through the Edge Platform. * @internal */ export const getEdgeProxyFormsUrl = ( sitecoreEdgeContextId: string, formId: string, - sitecoreEdgeUrl?: string + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT ) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index 8abe9d6159..372a328764 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -4,7 +4,9 @@ import chai, { expect } from 'chai'; import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import { DocumentNode } from 'graphql'; -import { DefaultRetryStrategy, NativeDataFetcher } from '@sitecore-content-sdk/core'; +import { DefaultRetryStrategy, NativeDataFetcher, constants } from '@sitecore-content-sdk/core'; + +const { SITECORE_EDGE_URL_DEFAULT } = constants; import { ErrorPage, SitecoreClient } from './sitecore-client'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; @@ -1370,15 +1372,6 @@ describe('SitecoreClient', () => { }); describe('getHeadLinks', function () { - const SITECORE_EDGE_URL_ENV = 'SITECORE_EDGE_URL'; - - beforeEach(() => { - process.env[SITECORE_EDGE_URL_ENV] = 'https://edge.example.com'; - }); - afterEach(() => { - delete process.env[SITECORE_EDGE_URL_ENV]; - }); - const truthyValue = { value: '

bar

', }; @@ -1420,11 +1413,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: 'https://edge.example.com/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, { - href: 'https://edge.example.com/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1446,11 +1439,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: 'https://edge.example.com/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, { - href: 'https://edge.example.com/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, ]); @@ -1463,7 +1456,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: 'https://edge.example.com/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1476,7 +1469,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: 'https://edge.example.com/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id', + href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 2632f5398f..a8505c8455 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -1,5 +1,6 @@ import { DocumentNode } from 'graphql'; import { + constants, GraphQLClient, GraphQLRequestClientFactory, FetchOptions, @@ -633,19 +634,15 @@ export class SitecoreClient implements BaseSitecoreClient { // regular sitemap if (sitemapPath) { try { - const rewrittenSitemapPath = rewriteEdgeHostInResponse( - sitemapPath, - this.initOptions.api.edge.edgeUrl - ); + const edgeUrl = + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT; + const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath, edgeUrl); const fetcher = new NativeDataFetcher(); const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); if (!xmlResponse.data) { throw new Error('REDIRECT_404'); } - return rewriteEdgeHostInResponse( - xmlResponse.data, - this.initOptions.api.edge.edgeUrl - ); + return rewriteEdgeHostInResponse(xmlResponse.data, edgeUrl); // eslint-disable-next-line no-unused-vars } catch (error) { throw new Error('REDIRECT_404'); @@ -730,10 +727,10 @@ export class SitecoreClient implements BaseSitecoreClient { if (!opt) { return layout; } + const edgeUrl = + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT; const transformer = - opt === true - ? getDefaultMediaUrlTransformer(this.initOptions.api.edge.edgeUrl) - : opt; + opt === true ? getDefaultMediaUrlTransformer(edgeUrl) : opt; return applyMediaUrlRewrite(layout, transformer); } diff --git a/packages/content/src/config/define-config.test.ts b/packages/content/src/config/define-config.test.ts index 887fb47dff..ad4e234197 100644 --- a/packages/content/src/config/define-config.test.ts +++ b/packages/content/src/config/define-config.test.ts @@ -99,7 +99,7 @@ describe('define-config', () => { describe('getFallbackConfig', () => { it('populates env variables in fallback config', () => { process.env.SITECORE_EDGE_CONTEXT_ID = 'env-context'; - process.env.SITECORE_EDGE_URL = 'env-edge-url'; + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME = 'https://env-edge-url'; process.env.SITECORE_EDITING_SECRET = 'env-secret'; process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT = '111'; process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT = '222'; @@ -108,7 +108,7 @@ describe('define-config', () => { const cfg = getFallbackConfig(); expect(cfg.api.edge.contextId).to.equal('env-context'); - expect(cfg.api.edge.edgeUrl).to.equal('env-edge-url'); + expect(cfg.api.edge.edgeUrl).to.equal('https://env-edge-url'); expect(cfg.editingSecret).to.equal('env-secret'); expect(cfg.personalize.edgeTimeout).to.equal(111); expect(cfg.personalize.cdpTimeout).to.equal(222); @@ -118,7 +118,7 @@ describe('define-config', () => { it('falls back to defaults when env variables are absent', () => { delete process.env.SITECORE_EDGE_CONTEXT_ID; - delete process.env.SITECORE_EDGE_URL; + delete process.env.SITECORE_EDGE_PLATFORM_HOSTNAME; delete process.env.SITECORE_EDITING_SECRET; delete process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT; delete process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT; diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index e6dcee043c..590df8a9d1 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -1,5 +1,5 @@ -import { NativeDataFetcher, FetchOptions } from '@sitecore-content-sdk/core'; -import { normalizeUrl, resolveUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { constants, NativeDataFetcher, FetchOptions } from '@sitecore-content-sdk/core'; +import { normalizeUrl, resolveUrl } from '@sitecore-content-sdk/core/tools'; import { LayoutServiceData } from '../layout'; import debug from '../debug'; import { DesignLibraryMode, DesignLibraryVariantGeneration } from './models'; @@ -141,7 +141,9 @@ export class ComponentLayoutService { * @returns {string} The fetch URL for the component data */ private getFetchUrl(params: ComponentLayoutRequestParams) { - const baseUrl = this.config.edgeUrl ? normalizeUrl(this.config.edgeUrl) : resolveEdgeUrl(); + const baseUrl = normalizeUrl( + this.config.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT + ); return resolveUrl(`${baseUrl}/layout/component`, this.getComponentFetchParams(params)); } } diff --git a/packages/content/src/editing/design-library.ts b/packages/content/src/editing/design-library.ts index 42732fc30e..e54aa74417 100644 --- a/packages/content/src/editing/design-library.ts +++ b/packages/content/src/editing/design-library.ts @@ -1,4 +1,5 @@ -import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { constants } from '@sitecore-content-sdk/core'; +import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentFields, ComponentParams, @@ -220,13 +221,15 @@ export function getDesignLibraryStatusEvent( /** * Generates the URL for the design library script link. - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * Caller should pass the resolved Edge URL from config. + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL (resolved at config level). Defaults to platform URL. * @returns The full URL to the design library script. * @internal */ -export function getDesignLibraryScriptLink(sitecoreEdgeUrl?: string): string { - return `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/designlibrary/lib/rh-lib-script.js`; +export function getDesignLibraryScriptLink( + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT +): string { + return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; } /** diff --git a/packages/content/src/layout/content-styles.ts b/packages/content/src/layout/content-styles.ts index bc812e8aa5..3549c93114 100644 --- a/packages/content/src/layout/content-styles.ts +++ b/packages/content/src/layout/content-styles.ts @@ -1,4 +1,5 @@ -import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { constants } from '@sitecore-content-sdk/core'; +import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, Field, Item, LayoutServiceData, RouteData } from './index'; import { HTMLLink } from '../models'; @@ -13,15 +14,14 @@ type Config = { loadStyles: boolean }; * Get the content styles link to be loaded from the Sitecore Edge Platform * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL (resolved at config level). Defaults to platform URL. * @returns {HTMLLink | null} content styles link, null if no styles are used in layout * @public */ export const getContentStylesheetLink = ( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl?: string + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT ): HTMLLink | null => { if (!layoutData.sitecore.route) return null; @@ -39,9 +39,9 @@ export const getContentStylesheetLink = ( export const getContentStylesheetUrl = ( sitecoreEdgeContextId: string, - sitecoreEdgeUrl?: string + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT ): string => - `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; + `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; export const traversePlaceholder = (components: Array, config: Config) => { if (config.loadStyles) return; diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index c6dabb1e59..bd75a4db9e 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -1,30 +1,22 @@ /* eslint-disable no-unused-expressions */ import { expect } from 'chai'; +import { constants } from '@sitecore-content-sdk/core'; import { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; -import { SITECORE_EDGE_HOSTNAME_PUBLIC_ENV } from '@sitecore-content-sdk/core/tools'; -describe('rewriteEdgeHostInResponse', () => { - const originalEnv = { ...process.env }; - - beforeEach(() => { - delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - }); - - afterEach(() => { - // Restore original env - process.env = { ...originalEnv }; - }); +const DEFAULT_EDGE_URL = constants.SITECORE_EDGE_URL_DEFAULT; +const CUSTOM_EDGE_URL = 'https://custom.example.com'; +describe('rewriteEdgeHostInResponse', () => { describe('rewriteEdgeHostInResponse()', () => { - it('should return response unchanged when no custom hostname is configured', () => { + it('should return response unchanged when default edge URL is passed', () => { const response = { url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, DEFAULT_EDGE_URL); expect(result).to.deep.equal(response); }); - it('should rewrite when edgeUrl is provided from config (no env vars)', () => { + it('should rewrite when edgeUrl is provided from config (custom hostname)', () => { const response = { url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', }; @@ -33,54 +25,48 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should rewrite edge-platform.sitecorecloud.io in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); it('should rewrite edge.sitecorecloud.io in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge.sitecorecloud.io/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); it('should rewrite edge-staging.sitecore-staging.cloud in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); }); it('should rewrite edge-platform-staging.sitecore-staging.cloud in string values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://edge-platform-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); }); it('should rewrite multiple occurrences in a string', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { html: '', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.html).to.equal( '' ); }); it('should rewrite nested objects', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { sitecore: { context: {}, @@ -95,21 +81,20 @@ describe('rewriteEdgeHostInResponse', () => { }, }, }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.sitecore.route.fields.image.value.src).to.equal( 'https://custom.example.com/media/image.jpg' ); }); it('should rewrite arrays', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { urls: [ 'https://edge-platform.sitecorecloud.io/a.jpg', 'https://edge-platform.sitecorecloud.io/b.jpg', ], }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.urls).to.deep.equal([ 'https://custom.example.com/a.jpg', 'https://custom.example.com/b.jpg', @@ -117,56 +102,50 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should handle null values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { value: null, }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.value).to.be.null; }); it('should handle undefined values', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { value: undefined, }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.value).to.be.undefined; }); it('should preserve non-string primitives', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { number: 42, boolean: true, string: 'no edge url here', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.number).to.equal(42); expect(result.boolean).to.be.true; expect(result.string).to.equal('no edge url here'); }); it('should handle http protocol in edge URLs', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'http://edge-platform.sitecorecloud.io/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); it('should handle mixed case (case insensitive)', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const response = { url: 'https://EDGE-PLATFORM.SITECORECLOUD.IO/media/image.jpg', }; - const result = rewriteEdgeHostInResponse(response); + const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); it('should handle complex layout service data structure', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; const layoutData = { sitecore: { context: { @@ -203,7 +182,7 @@ describe('rewriteEdgeHostInResponse', () => { }, }; - const result = rewriteEdgeHostInResponse(layoutData); + const result = rewriteEdgeHostInResponse(layoutData, CUSTOM_EDGE_URL); expect(result.sitecore.route.placeholders.main[0].fields.image.value.src).to.equal( 'https://custom.example.com/-/media/image.jpg' @@ -214,15 +193,13 @@ describe('rewriteEdgeHostInResponse', () => { }); it('should not rewrite similar but non-Edge URLs (no false positives)', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - const layout = { a: 'https://other-cdn.com/path/edge-platform/image.jpg', b: 'https://my-edge-store.example.com/media/file.jpg', c: 'https://edge-platform.sitecorecloud.io/real-edge/media.jpg', }; - const result = rewriteEdgeHostInResponse(layout) as typeof layout; + const result = rewriteEdgeHostInResponse(layout, CUSTOM_EDGE_URL) as typeof layout; expect(result.a).to.equal('https://other-cdn.com/path/edge-platform/image.jpg'); expect(result.b).to.equal('https://my-edge-store.example.com/media/file.jpg'); diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index aa72d625b8..f65ea63e83 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -1,8 +1,4 @@ -import { - DEFAULT_EDGE_HOSTNAMES, - hasCustomEdgeHostname, - resolveEdgeUrl, -} from '@sitecore-content-sdk/core/tools'; +import { DEFAULT_EDGE_HOSTNAMES, normalizeUrl } from '@sitecore-content-sdk/core/tools'; /** * Returns true if the given URL has a custom (non-default) Edge hostname. @@ -43,7 +39,7 @@ function escapeRegExp(input: string): string { * Rewrites Edge Platform hostnames in a response object to use the custom hostname. * This function performs a deep traversal of the object and replaces any string values * containing the default Edge hostnames with the custom hostname. - * Uses `edgeUrl` when provided (e.g. from config); otherwise resolves from env vars. + * Caller must pass the resolved Edge URL from config (no env resolution here). * * Use case: Experience Edge returns Layout Service output (layout, placeholders, component fields). * Field values can contain URLs with the Edge hostname—e.g. Image field `value.src` @@ -51,24 +47,18 @@ function escapeRegExp(input: string): string { * or link `href`. When using a custom hostname (e.g. CDN in front of Edge), these URLs * must be rewritten so layout API and media requests both go through the custom host. * @param {T} response - The response object to process (typically LayoutServiceData) - * @param {string} [edgeUrl] - Optional Edge URL from config. When provided, used for rewriting instead of env vars. + * @param {string} edgeUrl - Edge URL from config (resolved at config level). * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) * @public * @example * const layout = await layoutService.fetchLayoutData(path, options); - * const rewritten = rewriteEdgeHostInResponse(layout); + * const rewritten = rewriteEdgeHostInResponse(layout, config.api.edge.edgeUrl); */ -export function rewriteEdgeHostInResponse(response: T, edgeUrl?: string): T { - const customEdgeUrl = edgeUrl ? resolveEdgeUrl(edgeUrl) : resolveEdgeUrl(); - const shouldRewrite = - edgeUrl !== undefined && edgeUrl !== '' - ? isCustomEdgeUrl(customEdgeUrl) - : hasCustomEdgeHostname(); - - if (!shouldRewrite) { +export function rewriteEdgeHostInResponse(response: T, edgeUrl: string): T { + const customEdgeUrl = normalizeUrl(edgeUrl); + if (!isCustomEdgeUrl(customEdgeUrl)) { return response; } - return deepRewriteEdgeHost(response, customEdgeUrl); } @@ -134,19 +124,14 @@ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { /** * Returns the default media URL transformer: rewrites Edge hostnames when custom hostname is configured. - * Uses `edgeUrl` when provided (e.g. from config); otherwise resolves from env vars. - * @param {string} [edgeUrl] - Optional Edge URL from config. When provided, used for rewriting instead of env vars. + * Caller must pass the resolved Edge URL from config. + * @param {string} edgeUrl - Edge URL from config (resolved at config level). * @returns {(value: string) => string} Transformer function; returns string unchanged when no custom hostname * @internal */ -export function getDefaultMediaUrlTransformer(edgeUrl?: string): (value: string) => string { - const customEdgeUrl = edgeUrl ? resolveEdgeUrl(edgeUrl) : resolveEdgeUrl(); - const shouldRewrite = - edgeUrl !== undefined && edgeUrl !== '' - ? isCustomEdgeUrl(customEdgeUrl) - : hasCustomEdgeHostname(); - - if (!shouldRewrite) { +export function getDefaultMediaUrlTransformer(edgeUrl: string): (value: string) => string { + const customEdgeUrl = normalizeUrl(edgeUrl); + if (!isCustomEdgeUrl(customEdgeUrl)) { return (s) => s; } return (s) => rewriteEdgeHostInString(s, customEdgeUrl); diff --git a/packages/content/src/layout/themes.ts b/packages/content/src/layout/themes.ts index 845e2e7649..d0394d6374 100644 --- a/packages/content/src/layout/themes.ts +++ b/packages/content/src/layout/themes.ts @@ -1,4 +1,5 @@ -import { normalizeUrl, resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; +import { constants } from '@sitecore-content-sdk/core'; +import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentRendering, LayoutServiceData, RouteData, getFieldValue } from '.'; import { HTMLLink } from '../models'; @@ -12,15 +13,14 @@ const STYLES_LIBRARY_ID_REGEX = /-library--([^\s]+)/; * Walks through rendering tree and returns list of links of all FEAAS, BYOC or SXA Design Library Stylesheets that are used * @param {LayoutServiceData} layoutData Layout service data * @param {string} sitecoreEdgeContextId Sitecore Edge Context ID - * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL. If not provided, - * resolves from NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME or SITECORE_EDGE_URL env vars, falling back to default. + * @param {string} [sitecoreEdgeUrl] Sitecore Edge Platform URL (resolved at config level). Defaults to platform URL. * @returns {HTMLLink[]} library stylesheet links * @public */ export function getDesignLibraryStylesheetLinks( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl?: string + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT ): HTMLLink[] { const ids = new Set(); @@ -37,10 +37,9 @@ export function getDesignLibraryStylesheetLinks( export const getStylesheetUrl = ( id: string, sitecoreEdgeContextId: string, - sitecoreEdgeUrl?: string -) => { - return `${(sitecoreEdgeUrl ? normalizeUrl(sitecoreEdgeUrl) : resolveEdgeUrl())}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; -}; + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT +) => + `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; /** * Traverse placeholder and components to add library ids diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 3ae4638ff2..06f43ec6eb 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -39,6 +39,7 @@ export { ClientError } declare namespace constants { export { SITECORE_EDGE_URL_DEFAULT, + SITECORE_EDGE_PLATFORM_URL_DEFAULT, DEFAULT_EDGE_HOSTNAMES, CLAIMS, DEFAULT_SITECORE_AUTH_DOMAIN, @@ -286,16 +287,13 @@ export interface RetryStrategy { export function setCache(key: string, data: unknown): void; // @public -export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = "NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME"; +export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = "SITECORE_EDGE_PLATFORM_HOSTNAME"; // @internal -const SITECORE_EDGE_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; - -// @public -export const SITECORE_EDGE_URL_ENV = "SITECORE_EDGE_URL"; +const SITECORE_EDGE_PLATFORM_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; -// @public -export const SITECORE_EDGE_URL_PUBLIC_ENV = "NEXT_PUBLIC_SITECORE_EDGE_URL"; +// @internal +const SITECORE_EDGE_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; // @public export interface TenantArgs { @@ -310,7 +308,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:41:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:39:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 085b17f769..d40a19e2ee 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -4,6 +4,12 @@ */ export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; +/** + * Default Edge Platform URL (alias for naming consistency with SITECORE_EDGE_PLATFORM_HOSTNAME). + * @internal + */ +export const SITECORE_EDGE_PLATFORM_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; + /** * Default Edge Platform hostnames that may appear in layout/editing responses. * Used when rewriting URLs to a custom hostname. Includes production and staging. diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 4b7842853a..2025321fe9 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -12,9 +12,7 @@ export { resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, - SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, - SITECORE_EDGE_URL_ENV, - SITECORE_EDGE_URL_PUBLIC_ENV, + SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, } from './resolve-edge-url'; export { resolveUrl, diff --git a/packages/core/src/tools/normalize-env-value.test.ts b/packages/core/src/tools/normalize-env-value.test.ts new file mode 100644 index 0000000000..b4fc92d955 --- /dev/null +++ b/packages/core/src/tools/normalize-env-value.test.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { normalizeEnvValue } from './normalize-env-value'; + +describe('normalizeEnvValue', () => { + it('should return undefined for undefined', () => { + expect(normalizeEnvValue(undefined)).to.be.undefined; + }); + + it('should return undefined for empty string', () => { + expect(normalizeEnvValue('')).to.be.undefined; + }); + + it('should return undefined for whitespace-only string', () => { + expect(normalizeEnvValue(' ')).to.be.undefined; + expect(normalizeEnvValue('\t\n')).to.be.undefined; + }); + + it('should return undefined for the string "undefined"', () => { + expect(normalizeEnvValue('undefined')).to.be.undefined; + }); + + it('should return undefined for the string "null"', () => { + expect(normalizeEnvValue('null')).to.be.undefined; + }); + + it('should treat "undefined" and "null" case-insensitively', () => { + expect(normalizeEnvValue('UNDEFINED')).to.be.undefined; + expect(normalizeEnvValue('Undefined')).to.be.undefined; + expect(normalizeEnvValue('NULL')).to.be.undefined; + expect(normalizeEnvValue('Null')).to.be.undefined; + }); + + it('should return undefined when trimmed value is "undefined" or "null"', () => { + expect(normalizeEnvValue(' undefined ')).to.be.undefined; + expect(normalizeEnvValue(' null ')).to.be.undefined; + }); + + it('should return trimmed string for valid values', () => { + expect(normalizeEnvValue('custom.example.com')).to.equal('custom.example.com'); + expect(normalizeEnvValue(' custom.example.com ')).to.equal('custom.example.com'); + }); + + it('should preserve case of valid values', () => { + expect(normalizeEnvValue('My-Host.example.com')).to.equal('My-Host.example.com'); + }); + + it('should return valid value that contains "undefined" as substring', () => { + expect(normalizeEnvValue('my-undefined-host.com')).to.equal('my-undefined-host.com'); + }); +}); diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index 9a836c9733..4b57c8ecf8 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -5,9 +5,7 @@ import { resolveEdgeUrlForStaticFiles, hasCustomEdgeHostname, getCustomEdgeUrl, - SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, - SITECORE_EDGE_URL_ENV, - SITECORE_EDGE_URL_PUBLIC_ENV, + SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, } from './resolve-edge-url'; import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; @@ -15,9 +13,7 @@ describe('resolveEdgeUrl', () => { const originalEnv = { ...process.env }; beforeEach(() => { - delete process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - delete process.env[SITECORE_EDGE_URL_ENV]; - delete process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; + delete process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV]; }); afterEach(() => { @@ -27,7 +23,7 @@ describe('resolveEdgeUrl', () => { describe('resolveEdgeUrl()', () => { it('should return explicit edgeUrl parameter when provided', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; const result = resolveEdgeUrl('https://explicit.example.com'); expect(result).to.equal('https://explicit.example.com'); }); @@ -37,80 +33,61 @@ describe('resolveEdgeUrl', () => { expect(result).to.equal('https://explicit.example.com'); }); - it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set (hostname only)', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'my-tenant.edge.example.com'; + it('should use SITECORE_EDGE_PLATFORM_HOSTNAME when set (hostname only)', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'my-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set (full URL)', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://my-tenant.edge.example.com'; + it('should use SITECORE_EDGE_PLATFORM_HOSTNAME when set (full URL)', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); it('should normalize trailing slash from hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'https://my-tenant.edge.example.com/'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'https://my-tenant.edge.example.com/'; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); it('should trim whitespace from hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = ' my-tenant.edge.example.com '; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = ' my-tenant.edge.example.com '; const result = resolveEdgeUrl(); expect(result).to.equal('https://my-tenant.edge.example.com'); }); - it('should use NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME when set', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'public-tenant.edge.example.com'; + it('should use SITECORE_EDGE_PLATFORM_HOSTNAME when set', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'public-tenant.edge.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('https://public-tenant.edge.example.com'); }); - it('should use SITECORE_EDGE_URL when no hostname is set', () => { - process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-url.example.com'; - const result = resolveEdgeUrl(); - expect(result).to.equal('https://edge-url.example.com'); - }); - - it('should use NEXT_PUBLIC_SITECORE_EDGE_URL as fallback', () => { - process.env[SITECORE_EDGE_URL_PUBLIC_ENV] = 'https://public-edge-url.example.com'; - const result = resolveEdgeUrl(); - expect(result).to.equal('https://public-edge-url.example.com'); - }); - - it('should prefer hostname env var over URL env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'hostname.example.com'; - process.env[SITECORE_EDGE_URL_ENV] = 'https://url.example.com'; - const result = resolveEdgeUrl(); - expect(result).to.equal('https://hostname.example.com'); - }); - it('should return default when no env vars are set', () => { const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should treat the string "undefined" as an unset hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'undefined'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'undefined'; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should treat the string "null" as an unset hostname env var', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'null'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'null'; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should treat whitespace-only hostname env var as unset', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = ' '; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = ' '; const result = resolveEdgeUrl(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); it('should handle http protocol in hostname', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'http://insecure.example.com'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'http://insecure.example.com'; const result = resolveEdgeUrl(); expect(result).to.equal('http://insecure.example.com'); }); @@ -121,15 +98,10 @@ describe('resolveEdgeUrl', () => { expect(hasCustomEdgeHostname()).to.be.false; }); - it('should return true when NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME is set', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + it('should return true when SITECORE_EDGE_PLATFORM_HOSTNAME is set', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; expect(hasCustomEdgeHostname()).to.be.true; }); - - it('should return false when only SITECORE_EDGE_URL is set', () => { - process.env[SITECORE_EDGE_URL_ENV] = 'https://url.example.com'; - expect(hasCustomEdgeHostname()).to.be.false; - }); }); describe('getCustomEdgeUrl()', () => { @@ -138,32 +110,19 @@ describe('resolveEdgeUrl', () => { }); it('should return resolved URL when custom hostname is configured', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; expect(getCustomEdgeUrl()).to.equal('https://custom.example.com'); }); }); describe('resolveEdgeUrlForStaticFiles()', () => { - it('should return SITECORE_EDGE_URL when set', () => { - process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-for-files.example.com'; - const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal('https://edge-for-files.example.com'); - }); - - it('should return default when no URL env vars are set', () => { + it('should return default Edge URL', () => { const result = resolveEdgeUrlForStaticFiles(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); - it('should ignore custom hostname and use URL env when set', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; - process.env[SITECORE_EDGE_URL_ENV] = 'https://edge-platform.example.com'; - const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal('https://edge-platform.example.com'); - }); - - it('should ignore custom hostname and return default when URL env not set', () => { - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] = 'custom.example.com'; + it('should return default even when custom hostname is set', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; const result = resolveEdgeUrlForStaticFiles(); expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index ba12c4898f..0436a38b6c 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -1,35 +1,20 @@ import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; -import isServer from './is-server'; import { normalizeEnvValue } from './normalize-env-value'; import { normalizeUrl } from './normalize-url'; /** - * Environment variable name for the custom Edge hostname. - * Available on both server and client (e.g. NEXT_PUBLIC_* in Next.js). + * Environment variable name for the custom Edge Platform hostname (framework-agnostic). * @public */ -export const SITECORE_EDGE_HOSTNAME_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME'; +export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTNAME'; /** - * Environment variable name for the Edge URL override. - * @public - */ -export const SITECORE_EDGE_URL_ENV = 'SITECORE_EDGE_URL'; - -/** - * Environment variable name for the Edge URL override (client-side / browser). - * @public - */ -export const SITECORE_EDGE_URL_PUBLIC_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_URL'; - -/** - * Resolves the Sitecore Edge URL based on environment variables and configuration. + * Resolves the Sitecore Edge URL based on configuration and environment. * * Priority order: * 1. Explicit `edgeUrl` parameter (if provided and not empty) - * 2. `NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME` environment variable - * 3. `SITECORE_EDGE_URL` / `NEXT_PUBLIC_SITECORE_EDGE_URL` environment variable - * 4. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) + * 2. `SITECORE_EDGE_PLATFORM_HOSTNAME` environment variable + * 3. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) * * The hostname env var can be provided as: * - Full URL: `https://my-custom-edge.example.com` @@ -51,42 +36,23 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeUrl(explicit); } - // Check for custom hostname env var (available on both server and client) - const hostnameEnvVarRaw = process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]; - const hostnameEnvVar = normalizeEnvValue(hostnameEnvVarRaw); + // Check for custom hostname env var + const hostnameEnvVar = normalizeEnvValue(process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV]); if (hostnameEnvVar) { return normalizeHostnameToUrl(hostnameEnvVar); } - // Check for Edge URL env var - const urlEnvVarRaw = isServer() - ? process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV] - : process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; - - const urlEnvVar = normalizeEnvValue(urlEnvVarRaw); - if (urlEnvVar) { - return normalizeUrl(urlEnvVar); - } - - // Fall back to default return SITECORE_EDGE_URL_DEFAULT; } /** * Resolves the Edge URL for static files (e.g. stylesheets) by ignoring the custom hostname. * Use this when the custom host does not serve static file paths (e.g. /v1/files/...). - * Priority: SITECORE_EDGE_URL / NEXT_PUBLIC_SITECORE_EDGE_URL env, then default. + * Returns the default Edge Platform URL. * @returns {string} The Edge Platform base URL for static files (no trailing slash) * @public */ export function resolveEdgeUrlForStaticFiles(): string { - const urlEnvVarRaw = isServer() - ? process.env[SITECORE_EDGE_URL_ENV] || process.env[SITECORE_EDGE_URL_PUBLIC_ENV] - : process.env[SITECORE_EDGE_URL_PUBLIC_ENV]; - const urlEnvVar = normalizeEnvValue(urlEnvVarRaw); - if (urlEnvVar) { - return normalizeUrl(urlEnvVar); - } return SITECORE_EDGE_URL_DEFAULT; } @@ -114,7 +80,7 @@ function normalizeHostnameToUrl(hostnameOrUrl: string): string { * @public */ export function hasCustomEdgeHostname(): boolean { - return !!normalizeEnvValue(process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV]); + return !!normalizeEnvValue(process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV]); } /** diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.cursor/rules/sitecore.mdc b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.cursor/rules/sitecore.mdc index 6ac0f59d4f..2d40d55b1c 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.cursor/rules/sitecore.mdc +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.cursor/rules/sitecore.mdc @@ -135,7 +135,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index b4ba0ac02a..6d085749ed 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -19,9 +19,9 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= -# Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). -# Available on both server and client. -NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= +# Optional: custom Sitecore Edge Platform hostname (hostname or full URL). +# Next.js: use NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME for client; SITECORE_EDGE_PLATFORM_HOSTNAME for server-only. +NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.windsurfrules b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.windsurfrules index cb44a1ae2c..615491a7aa 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.windsurfrules +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.windsurfrules @@ -196,7 +196,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/CLAUDE.md b/packages/create-content-sdk-app/src/templates/nextjs-app-router/CLAUDE.md index 7f88bd0606..1297e5e6b6 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/CLAUDE.md +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/CLAUDE.md @@ -186,7 +186,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/copilot-instructions.md b/packages/create-content-sdk-app/src/templates/nextjs-app-router/copilot-instructions.md index 2edbe642af..26d1855438 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/copilot-instructions.md +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/copilot-instructions.md @@ -186,7 +186,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts.example index 41503a7902..befae0d08a 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.config.ts.example @@ -12,7 +12,9 @@ export default defineConfig({ process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || process.env.NEXT_PUBLIC_SITECORE_EDGE_URL, + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME, }, local: { apiKey: process.env.NEXT_PUBLIC_SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.cursor/rules/sitecore.mdc b/packages/create-content-sdk-app/src/templates/nextjs/.cursor/rules/sitecore.mdc index c9c0d54454..6164102642 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.cursor/rules/sitecore.mdc +++ b/packages/create-content-sdk-app/src/templates/nextjs/.cursor/rules/sitecore.mdc @@ -89,7 +89,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index b4ba0ac02a..6d085749ed 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -19,9 +19,9 @@ SITECORE_EDGE_CONTEXT_ID= # Will be used as a fallback if separate SITECORE_EDGE_CONTEXT_ID value is not provided NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= -# Optional: custom Experience Edge hostname override (XM Cloud/Edge; hostname or full URL). -# Available on both server and client. -NEXT_PUBLIC_SITECORE_EDGE_HOSTNAME= +# Optional: custom Sitecore Edge Platform hostname (hostname or full URL). +# Next.js: use NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME for client; SITECORE_EDGE_PLATFORM_HOSTNAME for server-only. +NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.windsurfrules b/packages/create-content-sdk-app/src/templates/nextjs/.windsurfrules index 3e4c95c045..faab8d6bfe 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.windsurfrules +++ b/packages/create-content-sdk-app/src/templates/nextjs/.windsurfrules @@ -136,7 +136,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs/CLAUDE.md b/packages/create-content-sdk-app/src/templates/nextjs/CLAUDE.md index 4ed39e6362..f0d6611300 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/CLAUDE.md +++ b/packages/create-content-sdk-app/src/templates/nextjs/CLAUDE.md @@ -126,7 +126,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs/copilot-instructions.md b/packages/create-content-sdk-app/src/templates/nextjs/copilot-instructions.md index 898f38fdc3..789794f152 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/copilot-instructions.md +++ b/packages/create-content-sdk-app/src/templates/nextjs/copilot-instructions.md @@ -126,7 +126,10 @@ export default defineConfig({ edge: { contextId: process.env.SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || 'https://edge-platform.sitecorecloud.io', + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME || + 'https://edge-platform.sitecorecloud.io', }, local: { apiKey: process.env.SITECORE_API_KEY || '', diff --git a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts.example b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts.example index 41503a7902..befae0d08a 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.config.ts.example @@ -12,7 +12,9 @@ export default defineConfig({ process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID || '', clientContextId: process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, - edgeUrl: process.env.SITECORE_EDGE_URL || process.env.NEXT_PUBLIC_SITECORE_EDGE_URL, + edgeUrl: + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME || + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME, }, local: { apiKey: process.env.NEXT_PUBLIC_SITECORE_API_KEY || '', diff --git a/packages/nextjs/src/config/define-config.test.ts b/packages/nextjs/src/config/define-config.test.ts index c3bf3cf611..288b55c930 100644 --- a/packages/nextjs/src/config/define-config.test.ts +++ b/packages/nextjs/src/config/define-config.test.ts @@ -157,11 +157,11 @@ describe('defineConfig', () => { }); describe('environment variable is set', () => { before(() => { - process.env.NEXT_PUBLIC_SITECORE_EDGE_URL = 'next-public-sitecore-edgeUrl'; + process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME = 'next-public-sitecore-edgeUrl'; }); after(() => { - delete process.env.NEXT_PUBLIC_SITECORE_EDGE_URL; + delete process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME; }); it('should use the value from the config if present', () => { diff --git a/packages/nextjs/src/config/define-config.ts b/packages/nextjs/src/config/define-config.ts index 299ae66a0c..a350183043 100644 --- a/packages/nextjs/src/config/define-config.ts +++ b/packages/nextjs/src/config/define-config.ts @@ -5,11 +5,15 @@ import { } from '@sitecore-content-sdk/content/config'; import { resolveEdgeUrl, - SITECORE_EDGE_HOSTNAME_PUBLIC_ENV, - SITECORE_EDGE_URL_ENV, - SITECORE_EDGE_URL_PUBLIC_ENV, + SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, } from '@sitecore-content-sdk/core/tools'; +/** + * Next.js environment variable for Edge Platform hostname (client-exposed). + * Used so the hostname is available in the browser; falls back to server-only env in getNextFallbackConfig. + */ +const NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME'; + /** * Provides default NextJs initial values from env variables for SitecoreConfig * @param {SitecoreConfigInput} config optional override values to be written over default config settings @@ -27,9 +31,8 @@ export const getNextFallbackConfig = (config?: SitecoreConfigInput): SitecoreCon config?.api?.edge?.clientContextId || process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, edgeUrl: resolveEdgeUrl( config?.api?.edge?.edgeUrl ?? - process.env[SITECORE_EDGE_HOSTNAME_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_PUBLIC_ENV] ?? - process.env[SITECORE_EDGE_URL_ENV] + process.env[NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] ?? + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] ), }, local: { From 243bcf9aef1d95990ece39450d114cb899b1b9ed Mon Sep 17 00:00:00 2001 From: Menelaos Nasies <38861573+MenKNas@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:07:25 +0200 Subject: [PATCH 20/25] Update packages/core/src/constants.ts Co-authored-by: Illia Kovalenko <23364749+illiakovalenko@users.noreply.github.com> --- packages/core/src/constants.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index d40a19e2ee..72a04aa78c 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -20,12 +20,7 @@ export const SITECORE_EDGE_PLATFORM_URL_DEFAULT = 'https://edge-platform.sitecor * - Other URL fields in component data * @public */ -export const DEFAULT_EDGE_HOSTNAMES = [ - 'edge-platform.sitecorecloud.io', - 'edge.sitecorecloud.io', - 'edge-staging.sitecore-staging.cloud', - 'edge-platform-staging.sitecore-staging.cloud', -] as const; +export const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = 'https://edge.sitecorecloud.io' /** * Claims URL From f7a82fcec2d833bcbc4f37cf0bd99d0783cd767d Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 17 Feb 2026 13:27:58 +0200 Subject: [PATCH 21/25] Adjustments --- packages/content/src/client/edge-proxy.test.ts | 6 +++--- packages/content/src/client/edge-proxy.ts | 6 +++--- .../content/src/client/sitecore-client.test.ts | 14 +++++++------- packages/content/src/client/sitecore-client.ts | 4 ++-- .../content/src/config/define-config.test.ts | 4 ++-- packages/content/src/config/models.ts | 2 +- .../editing/component-layout-service.test.ts | 14 +++++++------- .../src/editing/component-layout-service.ts | 4 ++-- .../content/src/editing/design-library.test.ts | 4 ++-- packages/content/src/editing/design-library.ts | 2 +- .../content/src/layout/content-styles.test.ts | 6 +++--- packages/content/src/layout/content-styles.ts | 4 ++-- .../src/layout/rewrite-edge-host.test.ts | 2 +- packages/content/src/layout/themes.test.ts | 4 ++-- packages/content/src/layout/themes.ts | 4 ++-- .../tools/codegen/component-generation.test.ts | 6 +++--- .../src/tools/codegen/component-generation.ts | 6 +++--- .../src/tools/codegen/extract-files.test.ts | 8 ++++---- packages/core/api/content-sdk-core.api.md | 8 ++------ packages/core/src/constants.ts | 17 ++++++++--------- .../core/src/tools/resolve-edge-url.test.ts | 14 +++++++------- packages/core/src/tools/resolve-edge-url.ts | 14 +++++++------- .../nextjs/src/config/define-config.test.ts | 2 +- packages/search/src/search-service.test.ts | 16 ++++++++-------- packages/search/src/search-service.ts | 2 +- 25 files changed, 84 insertions(+), 89 deletions(-) diff --git a/packages/content/src/client/edge-proxy.test.ts b/packages/content/src/client/edge-proxy.test.ts index 29bc72659e..5255b57bb2 100644 --- a/packages/content/src/client/edge-proxy.test.ts +++ b/packages/content/src/client/edge-proxy.test.ts @@ -3,14 +3,14 @@ import { expect } from 'chai'; import { constants } from '@sitecore-content-sdk/core'; import { getEdgeProxyContentUrl, getEdgeProxyFormsUrl } from './edge-proxy'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('edge-proxy', () => { describe('getEdgeProxyContentUrl', () => { it('should return url', () => { const url = getEdgeProxyContentUrl(); - expect(url).to.equal(`${SITECORE_EDGE_URL_DEFAULT}/v1/content/api/graphql/v1`); + expect(url).to.equal(`${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/content/api/graphql/v1`); }); it('should return url when custom sitecoreEdgeUrl is provided', () => { @@ -38,7 +38,7 @@ describe('edge-proxy', () => { const url = getEdgeProxyFormsUrl(sitecoreEdgeContextId, formId); expect(url).to.equal( - `${SITECORE_EDGE_URL_DEFAULT}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index a18524ea3e..fe5f02ce81 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -7,7 +7,7 @@ import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; * @internal */ const getBaseEdgeUrl = ( - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ): string => normalizeUrl(sitecoreEdgeUrl); /** @@ -17,7 +17,7 @@ const getBaseEdgeUrl = ( * @public */ export const getEdgeProxyContentUrl = ( - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; /** @@ -31,6 +31,6 @@ export const getEdgeProxyContentUrl = ( export const getEdgeProxyFormsUrl = ( sitecoreEdgeContextId: string, formId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index 372a328764..a4357c7b4d 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -6,7 +6,7 @@ import sinon from 'sinon'; import { DocumentNode } from 'graphql'; import { DefaultRetryStrategy, NativeDataFetcher, constants } from '@sitecore-content-sdk/core'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; import { ErrorPage, SitecoreClient } from './sitecore-client'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; @@ -1413,11 +1413,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1439,11 +1439,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, ]); @@ -1456,7 +1456,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1469,7 +1469,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index a8505c8455..3ff7313334 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -635,7 +635,7 @@ export class SitecoreClient implements BaseSitecoreClient { if (sitemapPath) { try { const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT; + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath, edgeUrl); const fetcher = new NativeDataFetcher(); const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); @@ -728,7 +728,7 @@ export class SitecoreClient implements BaseSitecoreClient { return layout; } const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT; + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; const transformer = opt === true ? getDefaultMediaUrlTransformer(edgeUrl) : opt; return applyMediaUrlRewrite(layout, transformer); diff --git a/packages/content/src/config/define-config.test.ts b/packages/content/src/config/define-config.test.ts index ad4e234197..207e28c9a5 100644 --- a/packages/content/src/config/define-config.test.ts +++ b/packages/content/src/config/define-config.test.ts @@ -5,7 +5,7 @@ import { deepMerge, defineConfig, getFallbackConfig } from './define-config'; import { SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('define-config', () => { const mockConfig: SitecoreConfigInput = { @@ -127,7 +127,7 @@ describe('define-config', () => { const cfg = getFallbackConfig(); expect(cfg.api.edge.contextId).to.equal(''); - expect(cfg.api.edge.edgeUrl).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(cfg.api.edge.edgeUrl).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); expect(cfg.editingSecret).to.equal('editing-secret-missing'); expect(cfg.personalize.edgeTimeout).to.equal(400); expect(cfg.personalize.cdpTimeout).to.equal(400); diff --git a/packages/content/src/config/models.ts b/packages/content/src/config/models.ts index 17acda07c3..78f2fb6681 100644 --- a/packages/content/src/config/models.ts +++ b/packages/content/src/config/models.ts @@ -44,7 +44,7 @@ export type SitecoreConfigInput = { clientContextId?: string; /** * XM Cloud endpoint that the app will communicate and retrieve data from - * @default https://edge-platform.sitecorecloud.io + * @default https://edge.sitecorecloud.io */ edgeUrl?: string; }; diff --git a/packages/content/src/editing/component-layout-service.test.ts b/packages/content/src/editing/component-layout-service.test.ts index f03cbdee0e..7499b9f82f 100644 --- a/packages/content/src/editing/component-layout-service.test.ts +++ b/packages/content/src/editing/component-layout-service.test.ts @@ -8,7 +8,7 @@ import { ComponentLayoutRequestParams, ComponentLayoutService } from './componen import { LayoutServiceData } from '../layout/models'; import { DesignLibraryMode } from './models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; use(spies); @@ -38,7 +38,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data', () => { - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -60,7 +60,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data in metadata mode', () => { - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -115,7 +115,7 @@ describe('ComponentLayoutService', () => { }, }; - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -139,7 +139,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data with custom fetch options', () => { - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { my_header: 'my_value', sc_editMode: 'false', @@ -189,7 +189,7 @@ describe('ComponentLayoutService', () => { }); it('should catch 404 when request layout data', () => { - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, sc_editMode: 'false', @@ -224,7 +224,7 @@ describe('ComponentLayoutService', () => { }); it('should allow non 404 errors through', () => { - nock(SITECORE_EDGE_URL_DEFAULT, { + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, sc_editMode: 'false', diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index 590df8a9d1..bc4d9a0d6e 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -64,7 +64,7 @@ export interface ComponentLayoutServiceConfig { contextId: string; /** * XM Cloud endpoint that the app will communicate and retrieve data from - * @default https://edge-platform.sitecorecloud.io + * @default https://edge.sitecorecloud.io */ edgeUrl?: string; } @@ -142,7 +142,7 @@ export class ComponentLayoutService { */ private getFetchUrl(params: ComponentLayoutRequestParams) { const baseUrl = normalizeUrl( - this.config.edgeUrl ?? constants.SITECORE_EDGE_URL_DEFAULT + this.config.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ); return resolveUrl(`${baseUrl}/layout/component`, this.getComponentFetchParams(params)); } diff --git a/packages/content/src/editing/design-library.test.ts b/packages/content/src/editing/design-library.test.ts index b4eb3f707e..c00f94bc09 100644 --- a/packages/content/src/editing/design-library.test.ts +++ b/packages/content/src/editing/design-library.test.ts @@ -17,7 +17,7 @@ import testComponent from '../test-data/component-editing-data'; import { DesignLibraryMode } from './models'; import { ComponentRendering } from '../layout'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('component library utils', () => { let debugSpy: sinon.SinonSpy; @@ -487,7 +487,7 @@ describe('component library utils', () => { it('should return the default design library script link when no URL is provided', () => { const scriptLink = getDesignLibraryScriptLink(); expect(scriptLink).to.equal( - `${SITECORE_EDGE_URL_DEFAULT}/v1/files/designlibrary/lib/rh-lib-script.js` + `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/designlibrary/lib/rh-lib-script.js` ); }); diff --git a/packages/content/src/editing/design-library.ts b/packages/content/src/editing/design-library.ts index e54aa74417..6698ad6512 100644 --- a/packages/content/src/editing/design-library.ts +++ b/packages/content/src/editing/design-library.ts @@ -227,7 +227,7 @@ export function getDesignLibraryStatusEvent( * @internal */ export function getDesignLibraryScriptLink( - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ): string { return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; } diff --git a/packages/content/src/layout/content-styles.test.ts b/packages/content/src/layout/content-styles.test.ts index 070c6b246c..f68a77a8b0 100644 --- a/packages/content/src/layout/content-styles.test.ts +++ b/packages/content/src/layout/content-styles.test.ts @@ -10,7 +10,7 @@ import { } from './content-styles'; import { ComponentRendering, Field, Item, LayoutServiceData } from './models'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('content-styles', () => { const truthyValue = { value: '

bar

' }; @@ -92,7 +92,7 @@ describe('content-styles', () => { }; expect(getContentStylesheetLink(layoutData, sitecoreEdgeContextId)).to.deep.equal({ - href: `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`, + href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`, rel: 'stylesheet', }); }); @@ -352,7 +352,7 @@ describe('content-styles', () => { describe('getContentStylesheetUrl', () => { it('should return the default url', () => { expect(getContentStylesheetUrl(sitecoreEdgeContextId)).to.equal( - `${SITECORE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/layout/content-styles.ts b/packages/content/src/layout/content-styles.ts index 3549c93114..a51029f038 100644 --- a/packages/content/src/layout/content-styles.ts +++ b/packages/content/src/layout/content-styles.ts @@ -21,7 +21,7 @@ type Config = { loadStyles: boolean }; export const getContentStylesheetLink = ( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ): HTMLLink | null => { if (!layoutData.sitecore.route) return null; @@ -39,7 +39,7 @@ export const getContentStylesheetLink = ( export const getContentStylesheetUrl = ( sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ): string => `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index bd75a4db9e..edda966215 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { constants } from '@sitecore-content-sdk/core'; import { rewriteEdgeHostInResponse, containsDefaultEdgeHost } from './rewrite-edge-host'; -const DEFAULT_EDGE_URL = constants.SITECORE_EDGE_URL_DEFAULT; +const DEFAULT_EDGE_URL = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; const CUSTOM_EDGE_URL = 'https://custom.example.com'; describe('rewriteEdgeHostInResponse', () => { diff --git a/packages/content/src/layout/themes.test.ts b/packages/content/src/layout/themes.test.ts index 97ded41a93..5c8f541ef2 100644 --- a/packages/content/src/layout/themes.test.ts +++ b/packages/content/src/layout/themes.test.ts @@ -3,7 +3,7 @@ import { constants } from '@sitecore-content-sdk/core'; import { getDesignLibraryStylesheetLinks, getStylesheetUrl } from './themes'; import { ComponentRendering } from '.'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('themes', () => { const sitecoreEdgeContextId = 'test'; @@ -425,7 +425,7 @@ describe('themes', () => { describe('getStylesheetUrl', () => { it('should use prod edge url by default', () => { expect(getStylesheetUrl('foo', sitecoreEdgeContextId)).to.equal( - `${SITECORE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/layout/themes.ts b/packages/content/src/layout/themes.ts index d0394d6374..e2140c6cbc 100644 --- a/packages/content/src/layout/themes.ts +++ b/packages/content/src/layout/themes.ts @@ -20,7 +20,7 @@ const STYLES_LIBRARY_ID_REGEX = /-library--([^\s]+)/; export function getDesignLibraryStylesheetLinks( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ): HTMLLink[] { const ids = new Set(); @@ -37,7 +37,7 @@ export function getDesignLibraryStylesheetLinks( export const getStylesheetUrl = ( id: string, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT ) => `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/tools/codegen/component-generation.test.ts b/packages/content/src/tools/codegen/component-generation.test.ts index bfd97fe960..f99988c751 100644 --- a/packages/content/src/tools/codegen/component-generation.test.ts +++ b/packages/content/src/tools/codegen/component-generation.test.ts @@ -7,7 +7,7 @@ import { getComponentSpec, } from './component-generation'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('component-generation', () => { const token = '456'; @@ -21,7 +21,7 @@ describe('component-generation', () => { }); expect(url).to.equal( - `${SITECORE_EDGE_URL_DEFAULT}/authoring/api/v1/components/generated/123?token=456&targetPath=.%2Fcomponents%2Fpromo-block%2FPromoBlock.variantA.ts` + `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/authoring/api/v1/components/generated/123?token=456&targetPath=.%2Fcomponents%2Fpromo-block%2FPromoBlock.variantA.ts` ); }); @@ -41,7 +41,7 @@ describe('component-generation', () => { describe('getComponentSpec', () => { const mockComponentSpecApi = ({ - edgeUrl = SITECORE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, componentId, targetPath, token, diff --git a/packages/content/src/tools/codegen/component-generation.ts b/packages/content/src/tools/codegen/component-generation.ts index e9961b8fee..5deb964986 100644 --- a/packages/content/src/tools/codegen/component-generation.ts +++ b/packages/content/src/tools/codegen/component-generation.ts @@ -1,6 +1,6 @@ import { constants, debug } from '@sitecore-content-sdk/core'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; /** * The parameters for fetching the component spec. @@ -46,7 +46,7 @@ export interface ComponentSpec { */ export const getComponentSpecUrl = ({ componentId, - edgeUrl = SITECORE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, targetPath, token, }: GetComponentSpecParams) => { @@ -67,7 +67,7 @@ export const getComponentSpecUrl = ({ */ export const getComponentSpec = async ({ componentId, - edgeUrl = SITECORE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, targetPath, token, }: GetComponentSpecParams) => { diff --git a/packages/content/src/tools/codegen/extract-files.test.ts b/packages/content/src/tools/codegen/extract-files.test.ts index 5b89ba84ab..7d78bb4367 100644 --- a/packages/content/src/tools/codegen/extract-files.test.ts +++ b/packages/content/src/tools/codegen/extract-files.test.ts @@ -11,7 +11,7 @@ import { extractFiles, ExtractFilesConfig } from './extract-files'; import nock from 'nock'; import { constants } from '@sitecore-content-sdk/core'; -const { SITECORE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; describe('extract-files', () => { const RENDERINGHOST_NAME = 'testRenderingHost'; @@ -233,7 +233,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EDGE_URL_DEFAULT) + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); @@ -265,7 +265,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EDGE_URL_DEFAULT) + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); @@ -297,7 +297,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EDGE_URL_DEFAULT) + nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 06f43ec6eb..59dc022f34 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -38,8 +38,7 @@ export { ClientError } declare namespace constants { export { - SITECORE_EDGE_URL_DEFAULT, - SITECORE_EDGE_PLATFORM_URL_DEFAULT, + SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, DEFAULT_EDGE_HOSTNAMES, CLAIMS, DEFAULT_SITECORE_AUTH_DOMAIN, @@ -290,10 +289,7 @@ export function setCache(key: string, data: unknown): void; export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = "SITECORE_EDGE_PLATFORM_HOSTNAME"; // @internal -const SITECORE_EDGE_PLATFORM_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; - -// @internal -const SITECORE_EDGE_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; +const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = "https://edge.sitecorecloud.io"; // @public export interface TenantArgs { diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 72a04aa78c..c7f862ebab 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,14 +1,8 @@ /** - * Default Sitecore edge URL + * Default Experience Edge URL (edge.sitecorecloud.io). Used when no custom hostname is configured. * @internal */ -export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; - -/** - * Default Edge Platform URL (alias for naming consistency with SITECORE_EDGE_PLATFORM_HOSTNAME). - * @internal - */ -export const SITECORE_EDGE_PLATFORM_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; +export const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = 'https://edge.sitecorecloud.io'; /** * Default Edge Platform hostnames that may appear in layout/editing responses. @@ -20,7 +14,12 @@ export const SITECORE_EDGE_PLATFORM_URL_DEFAULT = 'https://edge-platform.sitecor * - Other URL fields in component data * @public */ -export const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = 'https://edge.sitecorecloud.io' +export const DEFAULT_EDGE_HOSTNAMES = [ + 'edge-platform.sitecorecloud.io', + 'edge.sitecorecloud.io', + 'edge-staging.sitecore-staging.cloud', + 'edge-platform-staging.sitecore-staging.cloud', +] as const; /** * Claims URL diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index 4b57c8ecf8..db84ca1881 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -7,7 +7,7 @@ import { getCustomEdgeUrl, SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, } from './resolve-edge-url'; -import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; +import { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } from '../constants'; describe('resolveEdgeUrl', () => { const originalEnv = { ...process.env }; @@ -65,25 +65,25 @@ describe('resolveEdgeUrl', () => { it('should return default when no env vars are set', () => { const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); it('should treat the string "undefined" as an unset hostname env var', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'undefined'; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); it('should treat the string "null" as an unset hostname env var', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'null'; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); it('should treat whitespace-only hostname env var as unset', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = ' '; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); it('should handle http protocol in hostname', () => { @@ -118,13 +118,13 @@ describe('resolveEdgeUrl', () => { describe('resolveEdgeUrlForStaticFiles()', () => { it('should return default Edge URL', () => { const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); it('should return default even when custom hostname is set', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal(SITECORE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); }); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index 0436a38b6c..55bb3dec02 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -1,4 +1,4 @@ -import { SITECORE_EDGE_URL_DEFAULT } from '../constants'; +import { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } from '../constants'; import { normalizeEnvValue } from './normalize-env-value'; import { normalizeUrl } from './normalize-url'; @@ -14,7 +14,7 @@ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTN * Priority order: * 1. Explicit `edgeUrl` parameter (if provided and not empty) * 2. `SITECORE_EDGE_PLATFORM_HOSTNAME` environment variable - * 3. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) + * 3. Default Experience Edge URL (`https://edge.sitecorecloud.io`) * * The hostname env var can be provided as: * - Full URL: `https://my-custom-edge.example.com` @@ -27,7 +27,7 @@ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTN * @example * resolveEdgeUrl('https://custom.edge.com') // => 'https://custom.edge.com' * @example - * resolveEdgeUrl() // => 'https://edge-platform.sitecorecloud.io' + * resolveEdgeUrl() // => 'https://edge.sitecorecloud.io' */ export function resolveEdgeUrl(edgeUrl?: string): string { // Use explicit edgeUrl if provided and not empty @@ -42,18 +42,18 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeHostnameToUrl(hostnameEnvVar); } - return SITECORE_EDGE_URL_DEFAULT; + return SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; } /** * Resolves the Edge URL for static files (e.g. stylesheets) by ignoring the custom hostname. * Use this when the custom host does not serve static file paths (e.g. /v1/files/...). - * Returns the default Edge Platform URL. - * @returns {string} The Edge Platform base URL for static files (no trailing slash) + * Returns the default Experience Edge URL. + * @returns {string} The Experience Edge base URL for static files (no trailing slash) * @public */ export function resolveEdgeUrlForStaticFiles(): string { - return SITECORE_EDGE_URL_DEFAULT; + return SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; } /** diff --git a/packages/nextjs/src/config/define-config.test.ts b/packages/nextjs/src/config/define-config.test.ts index 288b55c930..ae27046fc9 100644 --- a/packages/nextjs/src/config/define-config.test.ts +++ b/packages/nextjs/src/config/define-config.test.ts @@ -143,7 +143,7 @@ describe('defineConfig', () => { it('should default to Edge Platform URL', () => { defineConfigModule.defineConfig(defaultConfig()); const resultConfig = defineConfigCoreStub.getCalls()[0].args[0]; - expect(resultConfig.api?.edge?.edgeUrl).to.equal('https://edge-platform.sitecorecloud.io'); + expect(resultConfig.api?.edge?.edgeUrl).to.equal('https://edge.sitecorecloud.io'); }); it('should use the value from the config', () => { diff --git a/packages/search/src/search-service.test.ts b/packages/search/src/search-service.test.ts index 00a9427cac..e160589e85 100644 --- a/packages/search/src/search-service.test.ts +++ b/packages/search/src/search-service.test.ts @@ -13,7 +13,7 @@ describe('SearchService', () => { }); it('should send a request with the keyphrase', async () => { - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -50,7 +50,7 @@ describe('SearchService', () => { }); it('should send a request with empty keyphrase', async () => { - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -128,7 +128,7 @@ describe('SearchService', () => { it('should send a request with custom limit', async () => { const limit = 20; - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -168,7 +168,7 @@ describe('SearchService', () => { it('should sent a request with custom offset', async () => { const offset = 50; - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -208,7 +208,7 @@ describe('SearchService', () => { it('should send a request with custom sort', async () => { const sort: SortSetting = { name: 'event', order: 'asc' }; - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -251,7 +251,7 @@ describe('SearchService', () => { { name: 'title', order: 'desc' }, ]; - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -289,7 +289,7 @@ describe('SearchService', () => { }); it('should return a default response when no results are found', async () => { - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -323,7 +323,7 @@ describe('SearchService', () => { }); it('should throw an error if the request fails', async () => { - nock(constants.SITECORE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, diff --git a/packages/search/src/search-service.ts b/packages/search/src/search-service.ts index 3b28bf1658..34ffebf3e2 100644 --- a/packages/search/src/search-service.ts +++ b/packages/search/src/search-service.ts @@ -19,7 +19,7 @@ export type SortSetting = { export interface SearchServiceConfig { /** * XM Cloud endpoint that the app will communicate and retrieve data from. - * @default https://edge-platform.sitecorecloud.io + * @default https://edge.sitecorecloud.io */ edgeUrl?: string; /** From 8fce771aeac4b1bd764c24c678de3a3e4e30b14f Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 17 Feb 2026 13:42:55 +0200 Subject: [PATCH 22/25] Adjustments --- .../content/src/layout/rewrite-edge-host.ts | 14 +++++++++++++- packages/core/api/content-sdk-core.api.md | 6 +----- packages/core/src/constants.ts | 17 ----------------- packages/core/src/tools/index.ts | 1 - 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index f65ea63e83..0616bd072a 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -1,4 +1,16 @@ -import { DEFAULT_EDGE_HOSTNAMES, normalizeUrl } from '@sitecore-content-sdk/core/tools'; +import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; + +/** + * Default Edge hostnames that may appear in layout/editing responses. + * Used when rewriting URLs to a custom hostname. Includes production and staging. + * @internal + */ +const DEFAULT_EDGE_HOSTNAMES = [ + 'edge-platform.sitecorecloud.io', + 'edge.sitecorecloud.io', + 'edge-staging.sitecore-staging.cloud', + 'edge-platform-staging.sitecore-staging.cloud', +] as const; /** * Returns true if the given URL has a custom (non-default) Edge hostname. diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 59dc022f34..62cef68ab5 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -39,7 +39,6 @@ export { ClientError } declare namespace constants { export { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, - DEFAULT_EDGE_HOSTNAMES, CLAIMS, DEFAULT_SITECORE_AUTH_DOMAIN, DEFAULT_SITECORE_AUTH_AUDIENCE, @@ -67,9 +66,6 @@ export const debugModule: debug_3.Debug & { // @public export const debugNamespace = "content-sdk"; -// @public -export const DEFAULT_EDGE_HOSTNAMES: readonly ["edge-platform.sitecorecloud.io", "edge.sitecorecloud.io", "edge-staging.sitecore-staging.cloud", "edge-platform-staging.sitecore-staging.cloud"]; - // @internal const DEFAULT_SITECORE_AUTH_AUDIENCE = "https://api.sitecorecloud.io"; @@ -304,7 +300,7 @@ export interface TenantArgs { // Warnings were encountered during analysis: // -// src/tools/index.ts:39:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts +// src/tools/index.ts:38:3 - (ae-forgotten-export) The symbol "authModule" needs to be exported by the entry point api-surface.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index c7f862ebab..162bca4a0f 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -4,23 +4,6 @@ */ export const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = 'https://edge.sitecorecloud.io'; -/** - * Default Edge Platform hostnames that may appear in layout/editing responses. - * Used when rewriting URLs to a custom hostname. Includes production and staging. - * - * These hostnames can appear in any string in the response, including: - * - Media URLs (Image field value.src, Rich Text markup) - * - Link field href values - * - Other URL fields in component data - * @public - */ -export const DEFAULT_EDGE_HOSTNAMES = [ - 'edge-platform.sitecorecloud.io', - 'edge.sitecorecloud.io', - 'edge-staging.sitecore-staging.cloud', - 'edge-platform-staging.sitecore-staging.cloud', -] as const; - /** * Claims URL * @internal diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 2025321fe9..b9ab52c943 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -6,7 +6,6 @@ export * from './metadata'; export { default as isServer } from './is-server'; export { ensurePathExists } from './ensurePath'; export { normalizeUrl } from './normalize-url'; -export { DEFAULT_EDGE_HOSTNAMES } from '../constants'; export { resolveEdgeUrl, resolveEdgeUrlForStaticFiles, From f22711f1bd59e7c2199ec72338960bd8ca99653e Mon Sep 17 00:00:00 2001 From: MenKNas Date: Tue, 17 Feb 2026 19:11:05 +0200 Subject: [PATCH 23/25] Address PR comments 4 --- .../content/src/client/edge-proxy.test.ts | 6 +-- packages/content/src/client/edge-proxy.ts | 6 +-- .../src/client/sitecore-client.test.ts | 20 ++++---- .../content/src/client/sitecore-client.ts | 4 +- .../content/src/config/define-config.test.ts | 4 +- packages/content/src/config/models.ts | 2 +- .../editing/component-layout-service.test.ts | 14 +++--- .../src/editing/component-layout-service.ts | 4 +- .../src/editing/design-library.test.ts | 4 +- .../content/src/editing/design-library.ts | 2 +- .../content/src/layout/content-styles.test.ts | 6 +-- packages/content/src/layout/content-styles.ts | 4 +- .../src/layout/rewrite-edge-host.test.ts | 48 +++++++++++-------- .../content/src/layout/rewrite-edge-host.ts | 44 ++++++----------- packages/content/src/layout/themes.test.ts | 4 +- packages/content/src/layout/themes.ts | 4 +- .../codegen/component-generation.test.ts | 6 +-- .../src/tools/codegen/component-generation.ts | 6 +-- .../src/tools/codegen/extract-files.test.ts | 8 ++-- packages/core/api/content-sdk-core.api.md | 4 ++ packages/core/src/constants.ts | 10 +++- .../core/src/tools/resolve-edge-url.test.ts | 14 +++--- packages/core/src/tools/resolve-edge-url.ts | 14 +++--- .../nextjs-app-router/.env.remote.example | 1 - .../src/templates/nextjs/.env.remote.example | 1 - .../nextjs/src/config/define-config.test.ts | 2 +- packages/search/src/search-service.test.ts | 16 +++---- packages/search/src/search-service.ts | 2 +- 28 files changed, 131 insertions(+), 129 deletions(-) diff --git a/packages/content/src/client/edge-proxy.test.ts b/packages/content/src/client/edge-proxy.test.ts index 5255b57bb2..03d0e7090f 100644 --- a/packages/content/src/client/edge-proxy.test.ts +++ b/packages/content/src/client/edge-proxy.test.ts @@ -3,14 +3,14 @@ import { expect } from 'chai'; import { constants } from '@sitecore-content-sdk/core'; import { getEdgeProxyContentUrl, getEdgeProxyFormsUrl } from './edge-proxy'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('edge-proxy', () => { describe('getEdgeProxyContentUrl', () => { it('should return url', () => { const url = getEdgeProxyContentUrl(); - expect(url).to.equal(`${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/content/api/graphql/v1`); + expect(url).to.equal(`${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/content/api/graphql/v1`); }); it('should return url when custom sitecoreEdgeUrl is provided', () => { @@ -38,7 +38,7 @@ describe('edge-proxy', () => { const url = getEdgeProxyFormsUrl(sitecoreEdgeContextId, formId); expect(url).to.equal( - `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/client/edge-proxy.ts b/packages/content/src/client/edge-proxy.ts index fe5f02ce81..35c596ba83 100644 --- a/packages/content/src/client/edge-proxy.ts +++ b/packages/content/src/client/edge-proxy.ts @@ -7,7 +7,7 @@ import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; * @internal */ const getBaseEdgeUrl = ( - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ): string => normalizeUrl(sitecoreEdgeUrl); /** @@ -17,7 +17,7 @@ const getBaseEdgeUrl = ( * @public */ export const getEdgeProxyContentUrl = ( - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/content/api/graphql/v1`; /** @@ -31,6 +31,6 @@ export const getEdgeProxyContentUrl = ( export const getEdgeProxyFormsUrl = ( sitecoreEdgeContextId: string, formId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ) => `${getBaseEdgeUrl(sitecoreEdgeUrl)}/v1/forms/publisher/${formId}?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index a4357c7b4d..5ba9e1a5c8 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -6,7 +6,7 @@ import sinon from 'sinon'; import { DocumentNode } from 'graphql'; import { DefaultRetryStrategy, NativeDataFetcher, constants } from '@sitecore-content-sdk/core'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; import { ErrorPage, SitecoreClient } from './sitecore-client'; import { LayoutKind, DesignLibraryMode } from '../../src/editing'; import { LayoutServiceData } from '../../layout'; @@ -520,7 +520,7 @@ describe('SitecoreClient', () => { name: 'home', placeholders: {}, fields: { - image: { value: { src: 'https://edge-platform.sitecorecloud.io/-/media/hero.jpg' } }, + image: { value: { src: 'https://edge.sitecorecloud.io/-/media/hero.jpg' } }, }, }, context: { site: siteInfo, pageState: LayoutServicePageState.Normal }, @@ -1413,11 +1413,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1439,11 +1439,11 @@ describe('SitecoreClient', () => { expect(result).to.deep.equal([ { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=client-context-id`, rel: 'stylesheet', }, ]); @@ -1456,7 +1456,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1469,7 +1469,7 @@ describe('SitecoreClient', () => { }); expect(result).to.deep.equal([ { - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=test-context-id`, rel: 'stylesheet', }, ]); @@ -1529,9 +1529,9 @@ describe('SitecoreClient', () => { }, } as any); - const edgeSitemapPath = 'https://edge-platform.sitecorecloud.io/sitemap.xml'; + const edgeSitemapPath = 'https://edge.sitecorecloud.io/sitemap.xml'; const xmlContent = - 'https://edge-platform.sitecorecloud.io/a'; + 'https://edge.sitecorecloud.io/a'; sitemapXmlServiceStub.getSitemap.resolves(edgeSitemapPath); const dataFetcherStub = sandbox diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index 3ff7313334..c29fb12809 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -635,7 +635,7 @@ export class SitecoreClient implements BaseSitecoreClient { if (sitemapPath) { try { const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT; const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath, edgeUrl); const fetcher = new NativeDataFetcher(); const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); @@ -728,7 +728,7 @@ export class SitecoreClient implements BaseSitecoreClient { return layout; } const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; + this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT; const transformer = opt === true ? getDefaultMediaUrlTransformer(edgeUrl) : opt; return applyMediaUrlRewrite(layout, transformer); diff --git a/packages/content/src/config/define-config.test.ts b/packages/content/src/config/define-config.test.ts index 207e28c9a5..f25eeeccc8 100644 --- a/packages/content/src/config/define-config.test.ts +++ b/packages/content/src/config/define-config.test.ts @@ -5,7 +5,7 @@ import { deepMerge, defineConfig, getFallbackConfig } from './define-config'; import { SitecoreConfigInput } from './models'; import { SITECORE_CLI_MODE_ENV_VAR } from '../config-cli'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('define-config', () => { const mockConfig: SitecoreConfigInput = { @@ -127,7 +127,7 @@ describe('define-config', () => { const cfg = getFallbackConfig(); expect(cfg.api.edge.contextId).to.equal(''); - expect(cfg.api.edge.edgeUrl).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(cfg.api.edge.edgeUrl).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); expect(cfg.editingSecret).to.equal('editing-secret-missing'); expect(cfg.personalize.edgeTimeout).to.equal(400); expect(cfg.personalize.cdpTimeout).to.equal(400); diff --git a/packages/content/src/config/models.ts b/packages/content/src/config/models.ts index 78f2fb6681..17acda07c3 100644 --- a/packages/content/src/config/models.ts +++ b/packages/content/src/config/models.ts @@ -44,7 +44,7 @@ export type SitecoreConfigInput = { clientContextId?: string; /** * XM Cloud endpoint that the app will communicate and retrieve data from - * @default https://edge.sitecorecloud.io + * @default https://edge-platform.sitecorecloud.io */ edgeUrl?: string; }; diff --git a/packages/content/src/editing/component-layout-service.test.ts b/packages/content/src/editing/component-layout-service.test.ts index 7499b9f82f..47876018f6 100644 --- a/packages/content/src/editing/component-layout-service.test.ts +++ b/packages/content/src/editing/component-layout-service.test.ts @@ -8,7 +8,7 @@ import { ComponentLayoutRequestParams, ComponentLayoutService } from './componen import { LayoutServiceData } from '../layout/models'; import { DesignLibraryMode } from './models'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; use(spies); @@ -38,7 +38,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data', () => { - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -60,7 +60,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data in metadata mode', () => { - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -115,7 +115,7 @@ describe('ComponentLayoutService', () => { }, }; - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, 'content-type': 'application/json', @@ -139,7 +139,7 @@ describe('ComponentLayoutService', () => { }); it('should fetch component data with custom fetch options', () => { - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { my_header: 'my_value', sc_editMode: 'false', @@ -189,7 +189,7 @@ describe('ComponentLayoutService', () => { }); it('should catch 404 when request layout data', () => { - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, sc_editMode: 'false', @@ -224,7 +224,7 @@ describe('ComponentLayoutService', () => { }); it('should allow non 404 errors through', () => { - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, sc_editMode: 'false', diff --git a/packages/content/src/editing/component-layout-service.ts b/packages/content/src/editing/component-layout-service.ts index bc4d9a0d6e..81a388af96 100644 --- a/packages/content/src/editing/component-layout-service.ts +++ b/packages/content/src/editing/component-layout-service.ts @@ -64,7 +64,7 @@ export interface ComponentLayoutServiceConfig { contextId: string; /** * XM Cloud endpoint that the app will communicate and retrieve data from - * @default https://edge.sitecorecloud.io + * @default https://edge-platform.sitecorecloud.io */ edgeUrl?: string; } @@ -142,7 +142,7 @@ export class ComponentLayoutService { */ private getFetchUrl(params: ComponentLayoutRequestParams) { const baseUrl = normalizeUrl( - this.config.edgeUrl ?? constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + this.config.edgeUrl ?? constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ); return resolveUrl(`${baseUrl}/layout/component`, this.getComponentFetchParams(params)); } diff --git a/packages/content/src/editing/design-library.test.ts b/packages/content/src/editing/design-library.test.ts index c00f94bc09..67c6f55d70 100644 --- a/packages/content/src/editing/design-library.test.ts +++ b/packages/content/src/editing/design-library.test.ts @@ -17,7 +17,7 @@ import testComponent from '../test-data/component-editing-data'; import { DesignLibraryMode } from './models'; import { ComponentRendering } from '../layout'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('component library utils', () => { let debugSpy: sinon.SinonSpy; @@ -487,7 +487,7 @@ describe('component library utils', () => { it('should return the default design library script link when no URL is provided', () => { const scriptLink = getDesignLibraryScriptLink(); expect(scriptLink).to.equal( - `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/designlibrary/lib/rh-lib-script.js` + `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/designlibrary/lib/rh-lib-script.js` ); }); diff --git a/packages/content/src/editing/design-library.ts b/packages/content/src/editing/design-library.ts index 6698ad6512..9c720ec2fb 100644 --- a/packages/content/src/editing/design-library.ts +++ b/packages/content/src/editing/design-library.ts @@ -227,7 +227,7 @@ export function getDesignLibraryStatusEvent( * @internal */ export function getDesignLibraryScriptLink( - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ): string { return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`; } diff --git a/packages/content/src/layout/content-styles.test.ts b/packages/content/src/layout/content-styles.test.ts index f68a77a8b0..bdfc29ef00 100644 --- a/packages/content/src/layout/content-styles.test.ts +++ b/packages/content/src/layout/content-styles.test.ts @@ -10,7 +10,7 @@ import { } from './content-styles'; import { ComponentRendering, Field, Item, LayoutServiceData } from './models'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('content-styles', () => { const truthyValue = { value: '

bar

' }; @@ -92,7 +92,7 @@ describe('content-styles', () => { }; expect(getContentStylesheetLink(layoutData, sitecoreEdgeContextId)).to.deep.equal({ - href: `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`, + href: `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`, rel: 'stylesheet', }); }); @@ -352,7 +352,7 @@ describe('content-styles', () => { describe('getContentStylesheetUrl', () => { it('should return the default url', () => { expect(getContentStylesheetUrl(sitecoreEdgeContextId)).to.equal( - `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/layout/content-styles.ts b/packages/content/src/layout/content-styles.ts index a51029f038..f12aff2fa6 100644 --- a/packages/content/src/layout/content-styles.ts +++ b/packages/content/src/layout/content-styles.ts @@ -21,7 +21,7 @@ type Config = { loadStyles: boolean }; export const getContentStylesheetLink = ( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ): HTMLLink | null => { if (!layoutData.sitecore.route) return null; @@ -39,7 +39,7 @@ export const getContentStylesheetLink = ( export const getContentStylesheetUrl = ( sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ): string => `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/layout/rewrite-edge-host.test.ts b/packages/content/src/layout/rewrite-edge-host.test.ts index edda966215..23e4c093f1 100644 --- a/packages/content/src/layout/rewrite-edge-host.test.ts +++ b/packages/content/src/layout/rewrite-edge-host.test.ts @@ -18,18 +18,18 @@ describe('rewriteEdgeHostInResponse', () => { it('should rewrite when edgeUrl is provided from config (custom hostname)', () => { const response = { - url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + url: 'https://edge.sitecorecloud.io/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, 'https://custom.edge.example.com'); expect(result.url).to.equal('https://custom.edge.example.com/media/image.jpg'); }); - it('should rewrite edge-platform.sitecorecloud.io in string values', () => { + it('should not rewrite edge-platform.sitecorecloud.io (only default hostname is rewritten)', () => { const response = { url: 'https://edge-platform.sitecorecloud.io/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); - expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); + expect(result.url).to.equal('https://edge-platform.sitecorecloud.io/media/image.jpg'); }); it('should rewrite edge.sitecorecloud.io in string values', () => { @@ -40,25 +40,29 @@ describe('rewriteEdgeHostInResponse', () => { expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); }); - it('should rewrite edge-staging.sitecore-staging.cloud in string values', () => { + it('should not rewrite edge-staging.sitecore-staging.cloud (only default hostname is rewritten)', () => { const response = { url: 'https://edge-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); - expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); + expect(result.url).to.equal( + 'https://edge-staging.sitecore-staging.cloud/tenant-id/media/image.jpg' + ); }); - it('should rewrite edge-platform-staging.sitecore-staging.cloud in string values', () => { + it('should not rewrite edge-platform-staging.sitecore-staging.cloud (only default hostname is rewritten)', () => { const response = { url: 'https://edge-platform-staging.sitecore-staging.cloud/tenant-id/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); - expect(result.url).to.equal('https://custom.example.com/tenant-id/media/image.jpg'); + expect(result.url).to.equal( + 'https://edge-platform-staging.sitecore-staging.cloud/tenant-id/media/image.jpg' + ); }); it('should rewrite multiple occurrences in a string', () => { const response = { - html: '', + html: '', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.html).to.equal( @@ -74,7 +78,7 @@ describe('rewriteEdgeHostInResponse', () => { fields: { image: { value: { - src: 'https://edge-platform.sitecorecloud.io/media/image.jpg', + src: 'https://edge.sitecorecloud.io/media/image.jpg', }, }, }, @@ -90,8 +94,8 @@ describe('rewriteEdgeHostInResponse', () => { it('should rewrite arrays', () => { const response = { urls: [ - 'https://edge-platform.sitecorecloud.io/a.jpg', - 'https://edge-platform.sitecorecloud.io/b.jpg', + 'https://edge.sitecorecloud.io/a.jpg', + 'https://edge.sitecorecloud.io/b.jpg', ], }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); @@ -131,7 +135,7 @@ describe('rewriteEdgeHostInResponse', () => { it('should handle http protocol in edge URLs', () => { const response = { - url: 'http://edge-platform.sitecorecloud.io/media/image.jpg', + url: 'http://edge.sitecorecloud.io/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); @@ -139,7 +143,7 @@ describe('rewriteEdgeHostInResponse', () => { it('should handle mixed case (case insensitive)', () => { const response = { - url: 'https://EDGE-PLATFORM.SITECORECLOUD.IO/media/image.jpg', + url: 'https://EDGE.SITECORECLOUD.IO/media/image.jpg', }; const result = rewriteEdgeHostInResponse(response, CUSTOM_EDGE_URL); expect(result.url).to.equal('https://custom.example.com/media/image.jpg'); @@ -161,7 +165,7 @@ describe('rewriteEdgeHostInResponse', () => { fields: { image: { value: { - src: 'https://edge-platform.sitecorecloud.io/-/media/image.jpg', + src: 'https://edge.sitecorecloud.io/-/media/image.jpg', alt: 'Test image', }, }, @@ -172,7 +176,7 @@ describe('rewriteEdgeHostInResponse', () => { fields: { content: { value: - '

Image:

', + '

Image:

', }, }, }, @@ -203,25 +207,27 @@ describe('rewriteEdgeHostInResponse', () => { expect(result.a).to.equal('https://other-cdn.com/path/edge-platform/image.jpg'); expect(result.b).to.equal('https://my-edge-store.example.com/media/file.jpg'); - expect(result.c).to.equal('https://custom.example.com/real-edge/media.jpg'); + expect(result.c).to.equal( + 'https://edge-platform.sitecorecloud.io/real-edge/media.jpg' + ); }); }); describe('containsDefaultEdgeHost()', () => { - it('should return true for edge-platform.sitecorecloud.io', () => { + it('should return false for edge-platform.sitecorecloud.io (only default hostname matches)', () => { expect( containsDefaultEdgeHost('https://edge-platform.sitecorecloud.io/media/image.jpg') - ).to.be.true; + ).to.be.false; }); - it('should return true for edge.sitecorecloud.io', () => { + it('should return true for edge.sitecorecloud.io (default hostname)', () => { expect(containsDefaultEdgeHost('https://edge.sitecorecloud.io/media/image.jpg')).to.be.true; }); - it('should return true for edge-staging.sitecore-staging.cloud', () => { + it('should return false for edge-staging.sitecore-staging.cloud (only default hostname matches)', () => { expect( containsDefaultEdgeHost('https://edge-staging.sitecore-staging.cloud/tenant/media/a.jpg') - ).to.be.true; + ).to.be.false; }); it('should return false for custom hostname', () => { diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index 0616bd072a..577c05f881 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -1,40 +1,33 @@ +import { constants } from '@sitecore-content-sdk/core'; import { normalizeUrl } from '@sitecore-content-sdk/core/tools'; -/** - * Default Edge hostnames that may appear in layout/editing responses. - * Used when rewriting URLs to a custom hostname. Includes production and staging. - * @internal - */ -const DEFAULT_EDGE_HOSTNAMES = [ - 'edge-platform.sitecorecloud.io', - 'edge.sitecorecloud.io', - 'edge-staging.sitecore-staging.cloud', - 'edge-platform-staging.sitecore-staging.cloud', -] as const; +/** Default Edge hostname derived from the default Edge URL (edge.sitecorecloud.io). @internal */ +const DEFAULT_EDGE_HOSTNAME = new URL(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT).hostname; /** * Returns true if the given URL has a custom (non-default) Edge hostname. * @param {string} url - Full URL or hostname - * @returns {boolean} True if URL host is not a default Edge hostname + * @returns {boolean} True if URL host is not the default Edge hostname * @internal */ function isCustomEdgeUrl(url: string): boolean { try { const u = url.startsWith('http') ? new URL(url) : new URL(`https://${url}`); const host = u.hostname.toLowerCase(); - return !DEFAULT_EDGE_HOSTNAMES.some((h) => host === h); + return host !== DEFAULT_EDGE_HOSTNAME; } catch { return false; } } /** - * Regular expression patterns for matching Edge hostnames in URLs. + * Regular expression for matching the default Edge hostname in URLs. * Matches both http:// and https:// protocols. * @internal */ -const EDGE_HOST_PATTERNS = DEFAULT_EDGE_HOSTNAMES.map( - (hostname) => new RegExp(`https?://${escapeRegExp(hostname)}`, 'gi') +const EDGE_HOST_PATTERN = new RegExp( + `https?://${escapeRegExp(DEFAULT_EDGE_HOSTNAME)}`, + 'gi' ); /** @@ -55,7 +48,7 @@ function escapeRegExp(input: string): string { * * Use case: Experience Edge returns Layout Service output (layout, placeholders, component fields). * Field values can contain URLs with the Edge hostname—e.g. Image field `value.src` - * (`https://edge-platform.sitecorecloud.io/-/media/...`), Rich Text HTML (``), + * (`https://edge.sitecorecloud.io/-/media/...`), Rich Text HTML (``), * or link `href`. When using a custom hostname (e.g. CDN in front of Edge), these URLs * must be rewritten so layout API and media requests both go through the custom host. * @param {T} response - The response object to process (typically LayoutServiceData) @@ -123,15 +116,8 @@ function deepRewriteEdgeHost(value: T, customEdgeUrl: string): T { * @internal */ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { - let result = str; - - for (const pattern of EDGE_HOST_PATTERNS) { - // Reset lastIndex for global regex - pattern.lastIndex = 0; - result = result.replace(pattern, customEdgeUrl); - } - - return result; + EDGE_HOST_PATTERN.lastIndex = 0; + return str.replace(EDGE_HOST_PATTERN, customEdgeUrl); } /** @@ -180,11 +166,11 @@ export function applyMediaUrlRewrite(value: T, transform: (s: string) => stri } /** - * Checks if a string contains any default Edge Platform hostnames. + * Checks if a string contains the default Edge hostname (from the default Edge URL). * @param {string} str - The string to check - * @returns {boolean} True if the string contains a default Edge hostname + * @returns {boolean} True if the string contains the default Edge hostname * @public */ export function containsDefaultEdgeHost(str: string): boolean { - return DEFAULT_EDGE_HOSTNAMES.some((hostname) => str.includes(hostname)); + return str.includes(DEFAULT_EDGE_HOSTNAME); } diff --git a/packages/content/src/layout/themes.test.ts b/packages/content/src/layout/themes.test.ts index 5c8f541ef2..04d5a12a93 100644 --- a/packages/content/src/layout/themes.test.ts +++ b/packages/content/src/layout/themes.test.ts @@ -3,7 +3,7 @@ import { constants } from '@sitecore-content-sdk/core'; import { getDesignLibraryStylesheetLinks, getStylesheetUrl } from './themes'; import { ComponentRendering } from '.'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('themes', () => { const sitecoreEdgeContextId = 'test'; @@ -425,7 +425,7 @@ describe('themes', () => { describe('getStylesheetUrl', () => { it('should use prod edge url by default', () => { expect(getStylesheetUrl('foo', sitecoreEdgeContextId)).to.equal( - `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=${sitecoreEdgeContextId}` + `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/v1/files/components/styles/foo.css?sitecoreContextId=${sitecoreEdgeContextId}` ); }); diff --git a/packages/content/src/layout/themes.ts b/packages/content/src/layout/themes.ts index e2140c6cbc..81db7a78b0 100644 --- a/packages/content/src/layout/themes.ts +++ b/packages/content/src/layout/themes.ts @@ -20,7 +20,7 @@ const STYLES_LIBRARY_ID_REGEX = /-library--([^\s]+)/; export function getDesignLibraryStylesheetLinks( layoutData: LayoutServiceData, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ): HTMLLink[] { const ids = new Set(); @@ -37,7 +37,7 @@ export function getDesignLibraryStylesheetLinks( export const getStylesheetUrl = ( id: string, sitecoreEdgeContextId: string, - sitecoreEdgeUrl: string = constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT + sitecoreEdgeUrl: string = constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT ) => `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/content/src/tools/codegen/component-generation.test.ts b/packages/content/src/tools/codegen/component-generation.test.ts index f99988c751..4f159ea282 100644 --- a/packages/content/src/tools/codegen/component-generation.test.ts +++ b/packages/content/src/tools/codegen/component-generation.test.ts @@ -7,7 +7,7 @@ import { getComponentSpec, } from './component-generation'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('component-generation', () => { const token = '456'; @@ -21,7 +21,7 @@ describe('component-generation', () => { }); expect(url).to.equal( - `${SITECORE_EXPERIENCE_EDGE_URL_DEFAULT}/authoring/api/v1/components/generated/123?token=456&targetPath=.%2Fcomponents%2Fpromo-block%2FPromoBlock.variantA.ts` + `${SITECORE_EDGE_PLATFORM_URL_DEFAULT}/authoring/api/v1/components/generated/123?token=456&targetPath=.%2Fcomponents%2Fpromo-block%2FPromoBlock.variantA.ts` ); }); @@ -41,7 +41,7 @@ describe('component-generation', () => { describe('getComponentSpec', () => { const mockComponentSpecApi = ({ - edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EDGE_PLATFORM_URL_DEFAULT, componentId, targetPath, token, diff --git a/packages/content/src/tools/codegen/component-generation.ts b/packages/content/src/tools/codegen/component-generation.ts index 5deb964986..bcae335e2f 100644 --- a/packages/content/src/tools/codegen/component-generation.ts +++ b/packages/content/src/tools/codegen/component-generation.ts @@ -1,6 +1,6 @@ import { constants, debug } from '@sitecore-content-sdk/core'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; /** * The parameters for fetching the component spec. @@ -46,7 +46,7 @@ export interface ComponentSpec { */ export const getComponentSpecUrl = ({ componentId, - edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EDGE_PLATFORM_URL_DEFAULT, targetPath, token, }: GetComponentSpecParams) => { @@ -67,7 +67,7 @@ export const getComponentSpecUrl = ({ */ export const getComponentSpec = async ({ componentId, - edgeUrl = SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, + edgeUrl = SITECORE_EDGE_PLATFORM_URL_DEFAULT, targetPath, token, }: GetComponentSpecParams) => { diff --git a/packages/content/src/tools/codegen/extract-files.test.ts b/packages/content/src/tools/codegen/extract-files.test.ts index 7d78bb4367..a0dbd46624 100644 --- a/packages/content/src/tools/codegen/extract-files.test.ts +++ b/packages/content/src/tools/codegen/extract-files.test.ts @@ -11,7 +11,7 @@ import { extractFiles, ExtractFilesConfig } from './extract-files'; import nock from 'nock'; import { constants } from '@sitecore-content-sdk/core'; -const { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } = constants; +const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; describe('extract-files', () => { const RENDERINGHOST_NAME = 'testRenderingHost'; @@ -233,7 +233,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); @@ -265,7 +265,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); @@ -297,7 +297,7 @@ describe('extract-files', () => { const consoleLogStub = sandbox.stub(console, 'log'); - nock(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT) + nock(SITECORE_EDGE_PLATFORM_URL_DEFAULT) .post('/mesh/push/api/v1/contentsdk/code/extracted') .reply(200) .persist(); diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index 62cef68ab5..d744eb7338 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -38,6 +38,7 @@ export { ClientError } declare namespace constants { export { + SITECORE_EDGE_PLATFORM_URL_DEFAULT, SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, CLAIMS, DEFAULT_SITECORE_AUTH_DOMAIN, @@ -284,6 +285,9 @@ export function setCache(key: string, data: unknown): void; // @public export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = "SITECORE_EDGE_PLATFORM_HOSTNAME"; +// @internal +const SITECORE_EDGE_PLATFORM_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; + // @internal const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = "https://edge.sitecorecloud.io"; diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 162bca4a0f..07082d485b 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,5 +1,13 @@ /** - * Default Experience Edge URL (edge.sitecorecloud.io). Used when no custom hostname is configured. + * Default Edge Platform URL (edge-platform.sitecorecloud.io). Used for service endpoints + * (GraphQL, content API, forms, layout, static files) when no custom hostname is configured. + * @internal + */ +export const SITECORE_EDGE_PLATFORM_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io'; + +/** + * Default Experience Edge URL (edge.sitecorecloud.io). Used only when replacing media URLs + * in layout/editing responses (rewrite-edge-host). Do not use for service endpoints. * @internal */ export const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = 'https://edge.sitecorecloud.io'; diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index db84ca1881..e66c1029b4 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -7,7 +7,7 @@ import { getCustomEdgeUrl, SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, } from './resolve-edge-url'; -import { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } from '../constants'; +import { SITECORE_EDGE_PLATFORM_URL_DEFAULT } from '../constants'; describe('resolveEdgeUrl', () => { const originalEnv = { ...process.env }; @@ -65,25 +65,25 @@ describe('resolveEdgeUrl', () => { it('should return default when no env vars are set', () => { const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); it('should treat the string "undefined" as an unset hostname env var', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'undefined'; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); it('should treat the string "null" as an unset hostname env var', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'null'; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); it('should treat whitespace-only hostname env var as unset', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = ' '; const result = resolveEdgeUrl(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); it('should handle http protocol in hostname', () => { @@ -118,13 +118,13 @@ describe('resolveEdgeUrl', () => { describe('resolveEdgeUrlForStaticFiles()', () => { it('should return default Edge URL', () => { const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); it('should return default even when custom hostname is set', () => { process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; const result = resolveEdgeUrlForStaticFiles(); - expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); + expect(result).to.equal(SITECORE_EDGE_PLATFORM_URL_DEFAULT); }); }); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index 55bb3dec02..38d9d43c61 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -1,4 +1,4 @@ -import { SITECORE_EXPERIENCE_EDGE_URL_DEFAULT } from '../constants'; +import { SITECORE_EDGE_PLATFORM_URL_DEFAULT } from '../constants'; import { normalizeEnvValue } from './normalize-env-value'; import { normalizeUrl } from './normalize-url'; @@ -14,7 +14,7 @@ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTN * Priority order: * 1. Explicit `edgeUrl` parameter (if provided and not empty) * 2. `SITECORE_EDGE_PLATFORM_HOSTNAME` environment variable - * 3. Default Experience Edge URL (`https://edge.sitecorecloud.io`) + * 3. Default Edge Platform URL (`https://edge-platform.sitecorecloud.io`) * * The hostname env var can be provided as: * - Full URL: `https://my-custom-edge.example.com` @@ -27,7 +27,7 @@ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTN * @example * resolveEdgeUrl('https://custom.edge.com') // => 'https://custom.edge.com' * @example - * resolveEdgeUrl() // => 'https://edge.sitecorecloud.io' + * resolveEdgeUrl() // => 'https://edge-platform.sitecorecloud.io' */ export function resolveEdgeUrl(edgeUrl?: string): string { // Use explicit edgeUrl if provided and not empty @@ -42,18 +42,18 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return normalizeHostnameToUrl(hostnameEnvVar); } - return SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; + return SITECORE_EDGE_PLATFORM_URL_DEFAULT; } /** * Resolves the Edge URL for static files (e.g. stylesheets) by ignoring the custom hostname. * Use this when the custom host does not serve static file paths (e.g. /v1/files/...). - * Returns the default Experience Edge URL. - * @returns {string} The Experience Edge base URL for static files (no trailing slash) + * Returns the default Edge Platform URL. + * @returns {string} The Edge Platform base URL for static files (no trailing slash) * @public */ export function resolveEdgeUrlForStaticFiles(): string { - return SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; + return SITECORE_EDGE_PLATFORM_URL_DEFAULT; } /** diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index 6d085749ed..4e6ddeb183 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -20,7 +20,6 @@ SITECORE_EDGE_CONTEXT_ID= NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Sitecore Edge Platform hostname (hostname or full URL). -# Next.js: use NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME for client; SITECORE_EDGE_PLATFORM_HOSTNAME for server-only. NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= # An optional Sitecore Personalize scope identifier. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index 6d085749ed..4e6ddeb183 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -20,7 +20,6 @@ SITECORE_EDGE_CONTEXT_ID= NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Sitecore Edge Platform hostname (hostname or full URL). -# Next.js: use NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME for client; SITECORE_EDGE_PLATFORM_HOSTNAME for server-only. NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= # An optional Sitecore Personalize scope identifier. diff --git a/packages/nextjs/src/config/define-config.test.ts b/packages/nextjs/src/config/define-config.test.ts index ae27046fc9..288b55c930 100644 --- a/packages/nextjs/src/config/define-config.test.ts +++ b/packages/nextjs/src/config/define-config.test.ts @@ -143,7 +143,7 @@ describe('defineConfig', () => { it('should default to Edge Platform URL', () => { defineConfigModule.defineConfig(defaultConfig()); const resultConfig = defineConfigCoreStub.getCalls()[0].args[0]; - expect(resultConfig.api?.edge?.edgeUrl).to.equal('https://edge.sitecorecloud.io'); + expect(resultConfig.api?.edge?.edgeUrl).to.equal('https://edge-platform.sitecorecloud.io'); }); it('should use the value from the config', () => { diff --git a/packages/search/src/search-service.test.ts b/packages/search/src/search-service.test.ts index e160589e85..97eea4b43d 100644 --- a/packages/search/src/search-service.test.ts +++ b/packages/search/src/search-service.test.ts @@ -13,7 +13,7 @@ describe('SearchService', () => { }); it('should send a request with the keyphrase', async () => { - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -50,7 +50,7 @@ describe('SearchService', () => { }); it('should send a request with empty keyphrase', async () => { - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -128,7 +128,7 @@ describe('SearchService', () => { it('should send a request with custom limit', async () => { const limit = 20; - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -168,7 +168,7 @@ describe('SearchService', () => { it('should sent a request with custom offset', async () => { const offset = 50; - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -208,7 +208,7 @@ describe('SearchService', () => { it('should send a request with custom sort', async () => { const sort: SortSetting = { name: 'event', order: 'asc' }; - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -251,7 +251,7 @@ describe('SearchService', () => { { name: 'title', order: 'desc' }, ]; - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -289,7 +289,7 @@ describe('SearchService', () => { }); it('should return a default response when no results are found', async () => { - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, @@ -323,7 +323,7 @@ describe('SearchService', () => { }); it('should throw an error if the request fails', async () => { - nock(constants.SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, { + nock(constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT, { reqheaders: { 'x-sitecore-contextid': contextId, }, diff --git a/packages/search/src/search-service.ts b/packages/search/src/search-service.ts index 34ffebf3e2..3b28bf1658 100644 --- a/packages/search/src/search-service.ts +++ b/packages/search/src/search-service.ts @@ -19,7 +19,7 @@ export type SortSetting = { export interface SearchServiceConfig { /** * XM Cloud endpoint that the app will communicate and retrieve data from. - * @default https://edge.sitecorecloud.io + * @default https://edge-platform.sitecorecloud.io */ edgeUrl?: string; /** From f1c4c54c52584ffacb12e07b00ab63a7071b92b0 Mon Sep 17 00:00:00 2001 From: MenKNas Date: Wed, 18 Feb 2026 12:28:54 +0200 Subject: [PATCH 24/25] Address PR comments 5 --- .../src/client/sitecore-client.test.ts | 21 +++----- .../content/src/client/sitecore-client.ts | 18 +++---- .../content/src/layout/rewrite-edge-host.ts | 23 ++++---- packages/core/api/content-sdk-core.api.md | 12 ++--- packages/core/src/tools/index.ts | 4 +- .../core/src/tools/resolve-edge-url.test.ts | 39 ++++++++------ packages/core/src/tools/resolve-edge-url.ts | 52 +++++++++++-------- .../nextjs-app-router/.env.remote.example | 3 ++ .../src/templates/nextjs/.env.remote.example | 3 ++ packages/nextjs/src/config/define-config.ts | 13 ++--- .../react/src/components/SitecoreProvider.tsx | 18 +------ 11 files changed, 97 insertions(+), 109 deletions(-) diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index 5ba9e1a5c8..73678a9966 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -5,6 +5,7 @@ import sinonChai from 'sinon-chai'; import sinon from 'sinon'; import { DocumentNode } from 'graphql'; import { DefaultRetryStrategy, NativeDataFetcher, constants } from '@sitecore-content-sdk/core'; +import { SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV } from '@sitecore-content-sdk/core/tools'; const { SITECORE_EDGE_PLATFORM_URL_DEFAULT } = constants; import { ErrorPage, SitecoreClient } from './sitecore-client'; @@ -527,24 +528,19 @@ describe('SitecoreClient', () => { }, }; layoutServiceStub.fetchLayoutData.returns(rawLayout); + process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; const clientWithRewrite = new SitecoreClient({ ...defaultInitOptions, - api: { - ...defaultInitOptions.api, - edge: { - ...defaultInitOptions.api.edge, - edgeUrl: 'https://custom.example.com', - }, - }, rewriteMediaUrls: true, } as any); - (clientWithRewrite as any).layoutService = layoutServiceStub; + (clientWithRewrite as any).layoutService = layoutServiceStub; const result = await clientWithRewrite.getPage(path, { locale }); expect( (result?.layout.sitecore.route?.fields?.image?.value as { src: string }).src ).to.equal('https://custom.example.com/-/media/hero.jpg'); + delete process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV]; }); it('should pass fetchOptions to layoutService when calling getPage', async () => { @@ -1518,15 +1514,9 @@ describe('SitecoreClient', () => { }); it('should rewrite Edge hostnames in sitemap path and XML when custom hostname is configured', async () => { + process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV] = 'custom.example.com'; const clientWithCustomEdge = new SitecoreClient({ ...defaultInitOptions, - api: { - ...defaultInitOptions.api, - edge: { - ...defaultInitOptions.api.edge, - edgeUrl: 'https://custom.example.com', - }, - }, } as any); const edgeSitemapPath = 'https://edge.sitecorecloud.io/sitemap.xml'; @@ -1543,6 +1533,7 @@ describe('SitecoreClient', () => { expect(getGraphqlSitemapXMLServiceStub.calledWith(defaultReqConfig.siteName)).to.be.true; expect(dataFetcherStub.calledWith('https://custom.example.com/sitemap.xml')).to.be.true; expect(result).to.include('https://custom.example.com/a'); + delete process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV]; }); it('should fetch specific sitemap when ID is provided', async () => { diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index c29fb12809..64473b9c03 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -1,6 +1,5 @@ import { DocumentNode } from 'graphql'; import { - constants, GraphQLClient, GraphQLRequestClientFactory, FetchOptions, @@ -8,7 +7,10 @@ import { NativeDataFetcher, debug, } from '@sitecore-content-sdk/core'; -import { resolveEdgeUrlForStaticFiles } from '@sitecore-content-sdk/core/tools'; +import { + resolveEdgeUrlForStaticFiles, + resolveExperienceEdgeUrl, +} from '@sitecore-content-sdk/core/tools'; import { DictionaryPhrases, DictionaryService } from '../i18n'; import { getDesignLibraryStylesheetLinks, @@ -634,15 +636,14 @@ export class SitecoreClient implements BaseSitecoreClient { // regular sitemap if (sitemapPath) { try { - const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT; - const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath, edgeUrl); + const experienceEdgeUrl = resolveExperienceEdgeUrl(); + const rewrittenSitemapPath = rewriteEdgeHostInResponse(sitemapPath, experienceEdgeUrl); const fetcher = new NativeDataFetcher(); const xmlResponse = await fetcher.fetch(rewrittenSitemapPath); if (!xmlResponse.data) { throw new Error('REDIRECT_404'); } - return rewriteEdgeHostInResponse(xmlResponse.data, edgeUrl); + return rewriteEdgeHostInResponse(xmlResponse.data, experienceEdgeUrl); // eslint-disable-next-line no-unused-vars } catch (error) { throw new Error('REDIRECT_404'); @@ -727,10 +728,9 @@ export class SitecoreClient implements BaseSitecoreClient { if (!opt) { return layout; } - const edgeUrl = - this.initOptions.api.edge.edgeUrl ?? constants.SITECORE_EDGE_PLATFORM_URL_DEFAULT; + const experienceEdgeUrl = resolveExperienceEdgeUrl(); const transformer = - opt === true ? getDefaultMediaUrlTransformer(edgeUrl) : opt; + opt === true ? getDefaultMediaUrlTransformer(experienceEdgeUrl) : opt; return applyMediaUrlRewrite(layout, transformer); } diff --git a/packages/content/src/layout/rewrite-edge-host.ts b/packages/content/src/layout/rewrite-edge-host.ts index 577c05f881..4a78524b68 100644 --- a/packages/content/src/layout/rewrite-edge-host.ts +++ b/packages/content/src/layout/rewrite-edge-host.ts @@ -41,10 +41,10 @@ function escapeRegExp(input: string): string { } /** - * Rewrites Edge Platform hostnames in a response object to use the custom hostname. + * Rewrites Experience Edge hostnames in a response object to use the custom hostname. * This function performs a deep traversal of the object and replaces any string values - * containing the default Edge hostnames with the custom hostname. - * Caller must pass the resolved Edge URL from config (no env resolution here). + * containing the default Experience Edge hostname with the custom hostname. + * Caller should pass the Experience Edge URL (e.g. from resolveExperienceEdgeUrl()). * * Use case: Experience Edge returns Layout Service output (layout, placeholders, component fields). * Field values can contain URLs with the Edge hostname—e.g. Image field `value.src` @@ -52,12 +52,12 @@ function escapeRegExp(input: string): string { * or link `href`. When using a custom hostname (e.g. CDN in front of Edge), these URLs * must be rewritten so layout API and media requests both go through the custom host. * @param {T} response - The response object to process (typically LayoutServiceData) - * @param {string} edgeUrl - Edge URL from config (resolved at config level). - * @returns {T} The response object with Edge hostnames rewritten (same reference if no custom hostname) + * @param {string} edgeUrl - Experience Edge URL to rewrite to (e.g. from resolveExperienceEdgeUrl). + * @returns {T} The response object with Experience Edge hostnames rewritten (same reference if no custom hostname) * @public * @example * const layout = await layoutService.fetchLayoutData(path, options); - * const rewritten = rewriteEdgeHostInResponse(layout, config.api.edge.edgeUrl); + * const rewritten = rewriteEdgeHostInResponse(layout, resolveExperienceEdgeUrl()); */ export function rewriteEdgeHostInResponse(response: T, edgeUrl: string): T { const customEdgeUrl = normalizeUrl(edgeUrl); @@ -109,10 +109,10 @@ function deepRewriteEdgeHost(value: T, customEdgeUrl: string): T { } /** - * Replaces Edge Platform hostnames in a string with the custom hostname. + * Replaces Experience Edge hostnames in a string with the custom hostname. * @param {string} str - The string to process - * @param {string} customEdgeUrl - The custom Edge URL to replace with - * @returns {string} The string with Edge hostnames replaced + * @param {string} customEdgeUrl - The custom Experience Edge URL to replace with + * @returns {string} The string with Experience Edge hostnames replaced * @internal */ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { @@ -121,9 +121,8 @@ function rewriteEdgeHostInString(str: string, customEdgeUrl: string): string { } /** - * Returns the default media URL transformer: rewrites Edge hostnames when custom hostname is configured. - * Caller must pass the resolved Edge URL from config. - * @param {string} edgeUrl - Edge URL from config (resolved at config level). + * Returns the default media URL transformer: rewrites Experience Edge hostnames when custom hostname is configured. + * @param {string} edgeUrl - Experience Edge URL to rewrite to (e.g. from resolveExperienceEdgeUrl()). * @returns {(value: string) => string} Transformer function; returns string unchanged when no custom hostname * @internal */ diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index d744eb7338..a389430c76 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -137,9 +137,6 @@ export function getCache(key: string): T | undefined; // @internal export function getCacheAndClean(key: string): T | undefined; -// @public -export function getCustomEdgeUrl(): string | undefined; - // @public export const getEnforcedCorsHeaders: ({ requestMethod, headers, presetCorsHeader, allowedOrigins, }: { requestMethod: string | undefined; @@ -194,9 +191,6 @@ export type GraphQLRequestClientFactoryConfig = { // @internal export function hasCache(key: string): boolean; -// @public -export function hasCustomEdgeHostname(): boolean; - // @public export const isRegexOrUrl: (input: string) => "regex" | "url"; @@ -270,6 +264,9 @@ export function resolveEdgeUrl(edgeUrl?: string): string; // @public export function resolveEdgeUrlForStaticFiles(): string; +// @public +export function resolveExperienceEdgeUrl(): string; + // @public export function resolveUrl(urlBase: string, params?: ParsedUrlQueryInput): string; @@ -288,6 +285,9 @@ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = "SITECORE_EDGE_PLATFORM_HOSTN // @internal const SITECORE_EDGE_PLATFORM_URL_DEFAULT = "https://edge-platform.sitecorecloud.io"; +// @public +export const SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV = "SITECORE_EXPERIENCE_EDGE_HOSTNAME"; + // @internal const SITECORE_EXPERIENCE_EDGE_URL_DEFAULT = "https://edge.sitecorecloud.io"; diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index b9ab52c943..b65e9e2ec8 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -9,9 +9,9 @@ export { normalizeUrl } from './normalize-url'; export { resolveEdgeUrl, resolveEdgeUrlForStaticFiles, - hasCustomEdgeHostname, - getCustomEdgeUrl, + resolveExperienceEdgeUrl, SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, + SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV, } from './resolve-edge-url'; export { resolveUrl, diff --git a/packages/core/src/tools/resolve-edge-url.test.ts b/packages/core/src/tools/resolve-edge-url.test.ts index e66c1029b4..55a4adcf75 100644 --- a/packages/core/src/tools/resolve-edge-url.test.ts +++ b/packages/core/src/tools/resolve-edge-url.test.ts @@ -3,17 +3,21 @@ import { expect } from 'chai'; import { resolveEdgeUrl, resolveEdgeUrlForStaticFiles, - hasCustomEdgeHostname, - getCustomEdgeUrl, + resolveExperienceEdgeUrl, SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, + SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV, } from './resolve-edge-url'; -import { SITECORE_EDGE_PLATFORM_URL_DEFAULT } from '../constants'; +import { + SITECORE_EDGE_PLATFORM_URL_DEFAULT, + SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, +} from '../constants'; describe('resolveEdgeUrl', () => { const originalEnv = { ...process.env }; beforeEach(() => { delete process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV]; + delete process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV]; }); afterEach(() => { @@ -93,25 +97,28 @@ describe('resolveEdgeUrl', () => { }); }); - describe('hasCustomEdgeHostname()', () => { - it('should return false when no hostname env vars are set', () => { - expect(hasCustomEdgeHostname()).to.be.false; + describe('resolveExperienceEdgeUrl()', () => { + it('should return default Experience Edge URL when env is not set', () => { + const result = resolveExperienceEdgeUrl(); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); - it('should return true when SITECORE_EDGE_PLATFORM_HOSTNAME is set', () => { - process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; - expect(hasCustomEdgeHostname()).to.be.true; + it('should return custom URL when SITECORE_EXPERIENCE_EDGE_HOSTNAME is set (hostname)', () => { + process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV] = 'media.example.com'; + const result = resolveExperienceEdgeUrl(); + expect(result).to.equal('https://media.example.com'); }); - }); - describe('getCustomEdgeUrl()', () => { - it('should return undefined when no custom hostname is configured', () => { - expect(getCustomEdgeUrl()).to.be.undefined; + it('should return custom URL when SITECORE_EXPERIENCE_EDGE_HOSTNAME is set (full URL)', () => { + process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV] = 'https://media.example.com'; + const result = resolveExperienceEdgeUrl(); + expect(result).to.equal('https://media.example.com'); }); - it('should return resolved URL when custom hostname is configured', () => { - process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'custom.example.com'; - expect(getCustomEdgeUrl()).to.equal('https://custom.example.com'); + it('should not be affected by SITECORE_EDGE_PLATFORM_HOSTNAME', () => { + process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] = 'platform.example.com'; + const result = resolveExperienceEdgeUrl(); + expect(result).to.equal(SITECORE_EXPERIENCE_EDGE_URL_DEFAULT); }); }); diff --git a/packages/core/src/tools/resolve-edge-url.ts b/packages/core/src/tools/resolve-edge-url.ts index 38d9d43c61..93e0934eb9 100644 --- a/packages/core/src/tools/resolve-edge-url.ts +++ b/packages/core/src/tools/resolve-edge-url.ts @@ -1,13 +1,24 @@ -import { SITECORE_EDGE_PLATFORM_URL_DEFAULT } from '../constants'; +import { + SITECORE_EDGE_PLATFORM_URL_DEFAULT, + SITECORE_EXPERIENCE_EDGE_URL_DEFAULT, +} from '../constants'; import { normalizeEnvValue } from './normalize-env-value'; import { normalizeUrl } from './normalize-url'; /** * Environment variable name for the custom Edge Platform hostname (framework-agnostic). + * Used for service endpoints (GraphQL, APIs, forms). Server-side only. * @public */ export const SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'SITECORE_EDGE_PLATFORM_HOSTNAME'; +/** + * Environment variable name for the custom Experience Edge hostname. + * Used only when rewriting media URLs in layout/editing (rewrite-edge-host). Server-side only. + * @public + */ +export const SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV = 'SITECORE_EXPERIENCE_EDGE_HOSTNAME'; + /** * Resolves the Sitecore Edge URL based on configuration and environment. * @@ -45,6 +56,24 @@ export function resolveEdgeUrl(edgeUrl?: string): string { return SITECORE_EDGE_PLATFORM_URL_DEFAULT; } +/** + * Resolves the Experience Edge URL for media URL rewriting. + * Use only when rewriting media URLs in layout/editing (e.g. rewriteEdgeHostInResponse). + * Priority: SITECORE_EXPERIENCE_EDGE_HOSTNAME env, then default (edge.sitecorecloud.io). + * Server-side only. + * @returns {string} The Experience Edge base URL (no trailing slash) + * @public + */ +export function resolveExperienceEdgeUrl(): string { + const hostnameEnvVar = normalizeEnvValue( + process.env[SITECORE_EXPERIENCE_EDGE_HOSTNAME_ENV] + ); + if (hostnameEnvVar) { + return normalizeHostnameToUrl(hostnameEnvVar); + } + return SITECORE_EXPERIENCE_EDGE_URL_DEFAULT; +} + /** * Resolves the Edge URL for static files (e.g. stylesheets) by ignoring the custom hostname. * Use this when the custom host does not serve static file paths (e.g. /v1/files/...). @@ -74,24 +103,3 @@ function normalizeHostnameToUrl(hostnameOrUrl: string): string { return normalizeUrl(`https://${trimmed}`); } -/** - * Checks if a custom Edge hostname is configured via environment variables. - * @returns {boolean} True if a custom hostname is configured - * @public - */ -export function hasCustomEdgeHostname(): boolean { - return !!normalizeEnvValue(process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV]); -} - -/** - * Gets the custom Edge hostname if configured, otherwise returns undefined. - * @returns {string | undefined} The custom Edge URL if configured, undefined otherwise - * @public - */ -export function getCustomEdgeUrl(): string | undefined { - if (!hasCustomEdgeHostname()) { - return undefined; - } - - return resolveEdgeUrl(); -} diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example index 4e6ddeb183..646f8c0b71 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/.env.remote.example @@ -22,6 +22,9 @@ NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Sitecore Edge Platform hostname (hostname or full URL). NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= +# Optional: custom Experience Edge hostname for media URL rewriting (e.g. staging). +SITECORE_EXPERIENCE_EDGE_HOSTNAME= + # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. # This should match the PAGES_PERSONALIZE_SCOPE environment variable for your connected XM Cloud Environment. diff --git a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example index 4e6ddeb183..646f8c0b71 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example +++ b/packages/create-content-sdk-app/src/templates/nextjs/.env.remote.example @@ -22,6 +22,9 @@ NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID= # Optional: custom Sitecore Edge Platform hostname (hostname or full URL). NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME= +# Optional: custom Experience Edge hostname for media URL rewriting (e.g. staging). +SITECORE_EXPERIENCE_EDGE_HOSTNAME= + # An optional Sitecore Personalize scope identifier. # This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant. # This should match the PAGES_PERSONALIZE_SCOPE environment variable for your connected XM Cloud Environment. diff --git a/packages/nextjs/src/config/define-config.ts b/packages/nextjs/src/config/define-config.ts index a350183043..e585174846 100644 --- a/packages/nextjs/src/config/define-config.ts +++ b/packages/nextjs/src/config/define-config.ts @@ -3,15 +3,9 @@ import { defineConfig as defineConfigCore, SitecoreConfigInput as SitecoreConfigInputCore, } from '@sitecore-content-sdk/content/config'; -import { - resolveEdgeUrl, - SITECORE_EDGE_PLATFORM_HOSTNAME_ENV, -} from '@sitecore-content-sdk/core/tools'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; -/** - * Next.js environment variable for Edge Platform hostname (client-exposed). - * Used so the hostname is available in the browser; falls back to server-only env in getNextFallbackConfig. - */ +/** Next.js env var for Edge hostname; exposed to the browser so client code can use it. */ const NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME_ENV = 'NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME'; /** @@ -31,8 +25,7 @@ export const getNextFallbackConfig = (config?: SitecoreConfigInput): SitecoreCon config?.api?.edge?.clientContextId || process.env.NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID, edgeUrl: resolveEdgeUrl( config?.api?.edge?.edgeUrl ?? - process.env[NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] ?? - process.env[SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] + process.env[NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME_ENV] ), }, local: { diff --git a/packages/react/src/components/SitecoreProvider.tsx b/packages/react/src/components/SitecoreProvider.tsx index 6d2fd5d153..cb29bb12aa 100644 --- a/packages/react/src/components/SitecoreProvider.tsx +++ b/packages/react/src/components/SitecoreProvider.tsx @@ -3,7 +3,6 @@ import React from 'react'; import fastDeepEqual from 'fast-deep-equal/es6/react'; import { Page } from '@sitecore-content-sdk/content/client'; import { SitecoreConfig } from '@sitecore-content-sdk/content/config'; -import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; import { ComponentMap } from './sharedTypes'; import { ImportMapImport } from './DesignLibrary/models'; @@ -76,25 +75,10 @@ export class SitecoreProvider extends React.Component< constructor(props: SitecoreProviderProps) { super(props); - // If any Edge ID is present but no edgeUrl, resolve using custom hostname or default - let api = props.api; - if ( - (props.api?.edge?.contextId || props.api?.edge?.clientContextId) && - !props.api?.edge?.edgeUrl - ) { - api = { - ...props.api, - edge: { - ...props.api.edge, - edgeUrl: resolveEdgeUrl(), - }, - }; - } - this.state = { page: props.page, setPage: this.setPage, - api, + api: props.api, }; } From 62ed9fb923430379df258a288382125375e10a7c Mon Sep 17 00:00:00 2001 From: MenKNas Date: Wed, 18 Feb 2026 12:40:42 +0200 Subject: [PATCH 25/25] Fix API extractor issue --- packages/content/api/content-sdk-content.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index b887c0be7a..f424858fe2 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -1353,7 +1353,7 @@ export type WriteImportMapArgsInternal = WriteImportMapArgs & { // Warnings were encountered during analysis: // -// src/client/sitecore-client.ts:63:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts +// src/client/sitecore-client.ts:65:3 - (ae-forgotten-export) The symbol "PageModeName" needs to be exported by the entry point api-surface.d.ts // src/editing/codegen/preview.ts:108:5 - (ae-forgotten-export) The symbol "ComponentImport_2" needs to be exported by the entry point api-surface.d.ts // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "ComponentMapTemplate" which is marked as @internal // src/tools/generate-map.ts:24:3 - (ae-incompatible-release-tags) The symbol "mapTemplate" is marked as @public, but its signature references "EnhancedComponentMapTemplate" which is marked as @internal