@sitecore-content-sdk/content 2.2.1 → 2.2.2-canary.20260727010537

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,18 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
17
  exports.SiteResolver = exports.getHostnameFromHostHeader = exports.SITE_KEY = exports.SITE_PREFIX = exports.normalizeSiteRewrite = exports.getSiteRewriteData = exports.getSiteRewrite = exports.SitePathService = exports.SiteInfoService = exports.ErrorPagesService = exports.SitemapXmlService = exports.RedirectsService = exports.REDIRECT_TYPE_SERVER_TRANSFER = exports.REDIRECT_TYPE_302 = exports.REDIRECT_TYPE_301 = exports.RobotsService = void 0;
4
18
  var robots_service_1 = require("./robots-service");
@@ -23,5 +37,6 @@ Object.defineProperty(exports, "normalizeSiteRewrite", { enumerable: true, get:
23
37
  Object.defineProperty(exports, "SITE_PREFIX", { enumerable: true, get: function () { return utils_1.SITE_PREFIX; } });
24
38
  Object.defineProperty(exports, "SITE_KEY", { enumerable: true, get: function () { return utils_1.SITE_KEY; } });
25
39
  Object.defineProperty(exports, "getHostnameFromHostHeader", { enumerable: true, get: function () { return utils_1.getHostnameFromHostHeader; } });
40
+ __exportStar(require("./redirect-utils"), exports);
26
41
  var site_resolver_1 = require("./site-resolver");
27
42
  Object.defineProperty(exports, "SiteResolver", { enumerable: true, get: function () { return site_resolver_1.SiteResolver; } });
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processRelativeUrlTarget = exports.processAbsoluteUrlTarget = exports.resolveRedirectTarget = exports.isAbsoluteTarget = exports.safeCompileRedirectPattern = exports.matchRedirectItemRedirect = exports.matchFromRedirectMapRedirect = exports.breakDownPath = void 0;
4
+ const tools_1 = require("@sitecore-content-sdk/core/tools");
5
+ const REGEXP_CONTEXT_SITE_LANG = /\$siteLang/i;
6
+ const REGEXP_ABSOLUTE_URL = /^(?:[a-z]+:)?\/\//i;
7
+ /**
8
+ * Splits a URL path into its (optional) leading locale segment, the remaining
9
+ * locale-less path, and the query string. Locales are compared case-insensitively.
10
+ * A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
11
+ * so slash-sensitive regex redirect rules can still be matched downstream.
12
+ * @param {string[]} configuredLocales configured site locales
13
+ * @param {string} urlPath path (optionally including a `?query`)
14
+ * @returns {ProcessedPath} broken-down path parts
15
+ * @internal
16
+ */
17
+ const breakDownPath = (configuredLocales, urlPath) => {
18
+ const urlArray = urlPath.split('?');
19
+ const urlQs = urlArray[1];
20
+ // collapse duplicate slashes so degenerate inputs (`//`, `///`) reduce to `/`,
21
+ // while a single meaningful trailing slash is preserved
22
+ const nonQsPath = urlArray[0].replace(/\/{2,}/g, '/');
23
+ const pathArray = nonQsPath.startsWith('/')
24
+ ? nonQsPath.slice(1).split('/')
25
+ : nonQsPath.split('/');
26
+ const [maybeLocale, maybeNonLocale] = [pathArray[0], pathArray.slice(1).join('/')];
27
+ const lowerCaseMaybeLocale = maybeLocale.toLowerCase();
28
+ // locales must be compared case insensitively, variations are abundant and typos can be there
29
+ return configuredLocales.find((configuredLocale) => configuredLocale.toLowerCase() === lowerCaseMaybeLocale)
30
+ ? {
31
+ nonLocalePath: `/${maybeNonLocale}`,
32
+ queryString: urlQs,
33
+ locale: maybeLocale,
34
+ }
35
+ : {
36
+ nonLocalePath: nonQsPath.startsWith('/') ? nonQsPath : `/${nonQsPath}`,
37
+ queryString: urlQs,
38
+ };
39
+ };
40
+ exports.breakDownPath = breakDownPath;
41
+ /**
42
+ * Matches redirect-map rules (rules without a `locale` field) against the incoming request.
43
+ * The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
44
+ * guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
45
+ * avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
46
+ * @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
47
+ * @param {string} requestLocale locale of the current request
48
+ * @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
49
+ * @returns {RedirectResult | undefined} First matching redirect or undefined.
50
+ * @internal
51
+ */
52
+ const matchFromRedirectMapRedirect = (redirects, requestLocale, incomingPathData) => {
53
+ const redirectMapRedirects = redirects.filter((redirect) => !redirect.locale);
54
+ // start from the locale-less path and rebuild the locale-prefixed variant from it
55
+ // (keep root as `/` so anchored `^/...` patterns still match). Trailing slashes are preserved.
56
+ const nonLocalePath = incomingPathData.nonLocalePath.toLowerCase() || '/';
57
+ const localePath = `/${requestLocale.toLowerCase()}${nonLocalePath}`;
58
+ const incomingQS = incomingPathData.queryString || '';
59
+ // regex rules are matched against the raw candidates (trailing slash preserved so slash-sensitive
60
+ // patterns still match); static rules ignore trailing slashes, so they use trimmed candidates
61
+ const pathCandidates = [nonLocalePath, localePath];
62
+ const staticCandidates = pathCandidates.map((path) => path.replace(/\/+$/g, '') || '/');
63
+ return redirectMapRedirects.find((redirect) => {
64
+ // process static URL (non-regex) rules
65
+ if ((0, tools_1.isRegexOrUrl)(redirect.pattern) === 'url') {
66
+ const [rawPatternPath, patternQS] = (redirect.pattern.endsWith('/') ? redirect.pattern.slice(0, -1) : redirect.pattern).split('?');
67
+ // candidates are already lowercased; lowercase the pattern path to keep locale (and route)
68
+ // comparison case-insensitive
69
+ const patternPath = rawPatternPath.toLowerCase();
70
+ return (staticCandidates.includes(patternPath) &&
71
+ (!patternQS ||
72
+ (0, tools_1.areURLSearchParamsEqual)(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
73
+ }
74
+ // process regex rules
75
+ const regex = (0, exports.safeCompileRedirectPattern)(redirect.pattern);
76
+ if (!regex) {
77
+ return false;
78
+ }
79
+ const testRegex = (value) => {
80
+ regex.lastIndex = 0;
81
+ return regex.test(value);
82
+ };
83
+ // remove duplicates
84
+ const regexCandidates = [...pathCandidates, ...staticCandidates].filter((candidate, index, array) => array.indexOf(candidate) === index);
85
+ const matchedPath = regexCandidates.find((candidate) => testRegex(candidate));
86
+ const matchedPathWithQuery = incomingQS
87
+ ? regexCandidates.find((candidate) => testRegex(`${candidate}?${incomingQS}`))
88
+ : undefined;
89
+ // Save the matched path/query (if found) into the redirect object
90
+ redirect.matchedQueryString = matchedPathWithQuery ? `?${incomingQS}` : '';
91
+ redirect.matchedPath = matchedPath || matchedPathWithQuery || '';
92
+ return !!(matchedPath || matchedPathWithQuery);
93
+ });
94
+ };
95
+ exports.matchFromRedirectMapRedirect = matchFromRedirectMapRedirect;
96
+ /**
97
+ * Processes redirect rules from redirect items (language-versioned)
98
+ * @param {RedirectResult[]} redirects redirect entries from Edge
99
+ * @param {string} locale current request locale
100
+ * @param {string} nonLocalePath current request path with locale prefix stripped
101
+ * @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
102
+ * @internal
103
+ */
104
+ const matchRedirectItemRedirect = (redirects, locale, nonLocalePath) => {
105
+ return redirects.find((redirect) => {
106
+ const patternPath = redirect.pattern.replace(/\/+$/g, '').toLowerCase();
107
+ // locale rules are easy and nice
108
+ return redirect.locale === locale && patternPath === nonLocalePath;
109
+ });
110
+ };
111
+ exports.matchRedirectItemRedirect = matchRedirectItemRedirect;
112
+ /**
113
+ * Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
114
+ * so one bad entry does not fail the entire redirect chain.
115
+ * Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
116
+ * @param {string} pattern redirect pattern from redirect map
117
+ * @returns {RegExp | null} normalized regex instance, or null when invalid
118
+ * @internal
119
+ */
120
+ const safeCompileRedirectPattern = (pattern) => {
121
+ try {
122
+ const normalizedPattern = (0, tools_1.escapeNonSpecialQuestionMarks)(pattern);
123
+ const literalMatch = normalizedPattern.match(/^\/(.+)\/([a-z]*)$/i);
124
+ if (literalMatch) {
125
+ const [, source, flags] = literalMatch;
126
+ const safeFlags = flags || 'i';
127
+ return new RegExp(source, safeFlags);
128
+ }
129
+ return new RegExp(normalizedPattern, 'i');
130
+ }
131
+ catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ console.warn(`[RedirectsProxy] Invalid redirect regex; skipping rule. pattern=${pattern} (${message})`);
134
+ return null;
135
+ }
136
+ };
137
+ exports.safeCompileRedirectPattern = safeCompileRedirectPattern;
138
+ /**
139
+ * Detects absolute (external or protocol-relative) target URLs.
140
+ * @param {string} target redirect target
141
+ * @returns {boolean} true when the target is an absolute URL
142
+ * @internal
143
+ */
144
+ const isAbsoluteTarget = (target) => REGEXP_ABSOLUTE_URL.test(target);
145
+ exports.isAbsoluteTarget = isAbsoluteTarget;
146
+ /**
147
+ * Resolves the redirect target string: replaces the `$siteLang` token and applies
148
+ * regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
149
+ * @param {RedirectResult} existsRedirect matched redirect
150
+ * @param {string} siteLanguage site language used for the `$siteLang` token
151
+ * @param {string} requestPath incoming request path, used when the rule stored no matched path
152
+ * @returns {string} resolved target
153
+ * @internal
154
+ */
155
+ const resolveRedirectTarget = (existsRedirect, siteLanguage, requestPath) => {
156
+ let target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, siteLanguage);
157
+ // Apply regex replacements to the target URL if the pattern is a regex
158
+ if ((0, tools_1.isRegexOrUrl)(existsRedirect.pattern) === 'regex') {
159
+ const sourcePath = existsRedirect.matchedPath || requestPath;
160
+ const pathForCaptureMatch = sourcePath.replace(/\/+$/g, '') || '/';
161
+ const redirectRegex = (0, exports.safeCompileRedirectPattern)(existsRedirect.pattern);
162
+ const matched = redirectRegex ? pathForCaptureMatch.match(redirectRegex) : null;
163
+ if (matched) {
164
+ target = target.replace(/\$(\d+)/g, (_, index) => matched[parseInt(index, 10)] || '');
165
+ }
166
+ }
167
+ return target;
168
+ };
169
+ exports.resolveRedirectTarget = resolveRedirectTarget;
170
+ /**
171
+ * Resolves the absolute redirect target (external redirects), optionally preseriving query string
172
+ * @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
173
+ * @param {RedirectResult} existsRedirect matched redirect to resolve final result from
174
+ * @returns {string} URL to redirect to
175
+ * @internal
176
+ */
177
+ const processAbsoluteUrlTarget = (incomingPathData, existsRedirect) => {
178
+ if (!incomingPathData.queryString || !existsRedirect.isQueryStringPreserved) {
179
+ return existsRedirect.target;
180
+ }
181
+ const [targetMainUrl, targetQS] = existsRedirect.target.split('?');
182
+ const mergedQueryString = (0, tools_1.mergeURLSearchParams)(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(targetQS || ''));
183
+ return `${targetMainUrl}?${mergedQueryString}`;
184
+ };
185
+ exports.processAbsoluteUrlTarget = processAbsoluteUrlTarget;
186
+ /**
187
+ * Resolves the locale-less path and target locale for a relative redirect URL.
188
+ * The framework layer is responsible for placing the locale (pathname vs. locale property).
189
+ * @param {ProcessedPath} incomingPathData broken-down incoming request path
190
+ * @param {RedirectResult} existsRedirect matched redirect
191
+ * @param {string[]} configuredLocales configured site locales
192
+ * @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
193
+ * @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
194
+ * @internal
195
+ */
196
+ const processRelativeUrlTarget = (incomingPathData, existsRedirect, configuredLocales, reqLocale) => {
197
+ const { locale, nonLocalePath, queryString } = (0, exports.breakDownPath)(configuredLocales, existsRedirect.target);
198
+ // preserve the request locale when the target has no locale prefix and the rule opts in
199
+ const targetLocale = locale || (existsRedirect.isLanguagePreserved ? reqLocale : '');
200
+ const targetQS = incomingPathData.queryString && existsRedirect.isQueryStringPreserved
201
+ ? (0, tools_1.mergeURLSearchParams)(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(queryString || ''))
202
+ : queryString || '';
203
+ return {
204
+ targetLocale,
205
+ targetPath: `${nonLocalePath}${targetQS ? '?' + targetQS : ''}`,
206
+ };
207
+ };
208
+ exports.processRelativeUrlTarget = processRelativeUrlTarget;
@@ -5,4 +5,5 @@ export { ErrorPagesService } from './error-pages-service';
5
5
  export { SiteInfoService } from './siteinfo-service';
6
6
  export { SitePathService } from './sitepath-service';
7
7
  export { getSiteRewrite, getSiteRewriteData, normalizeSiteRewrite, SITE_PREFIX, SITE_KEY, getHostnameFromHostHeader, } from './utils';
8
+ export * from './redirect-utils';
8
9
  export { SiteResolver } from './site-resolver';
@@ -0,0 +1,197 @@
1
+ import { areURLSearchParamsEqual, escapeNonSpecialQuestionMarks, isRegexOrUrl, mergeURLSearchParams, } from '@sitecore-content-sdk/core/tools';
2
+ const REGEXP_CONTEXT_SITE_LANG = /\$siteLang/i;
3
+ const REGEXP_ABSOLUTE_URL = /^(?:[a-z]+:)?\/\//i;
4
+ /**
5
+ * Splits a URL path into its (optional) leading locale segment, the remaining
6
+ * locale-less path, and the query string. Locales are compared case-insensitively.
7
+ * A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
8
+ * so slash-sensitive regex redirect rules can still be matched downstream.
9
+ * @param {string[]} configuredLocales configured site locales
10
+ * @param {string} urlPath path (optionally including a `?query`)
11
+ * @returns {ProcessedPath} broken-down path parts
12
+ * @internal
13
+ */
14
+ export const breakDownPath = (configuredLocales, urlPath) => {
15
+ const urlArray = urlPath.split('?');
16
+ const urlQs = urlArray[1];
17
+ // collapse duplicate slashes so degenerate inputs (`//`, `///`) reduce to `/`,
18
+ // while a single meaningful trailing slash is preserved
19
+ const nonQsPath = urlArray[0].replace(/\/{2,}/g, '/');
20
+ const pathArray = nonQsPath.startsWith('/')
21
+ ? nonQsPath.slice(1).split('/')
22
+ : nonQsPath.split('/');
23
+ const [maybeLocale, maybeNonLocale] = [pathArray[0], pathArray.slice(1).join('/')];
24
+ const lowerCaseMaybeLocale = maybeLocale.toLowerCase();
25
+ // locales must be compared case insensitively, variations are abundant and typos can be there
26
+ return configuredLocales.find((configuredLocale) => configuredLocale.toLowerCase() === lowerCaseMaybeLocale)
27
+ ? {
28
+ nonLocalePath: `/${maybeNonLocale}`,
29
+ queryString: urlQs,
30
+ locale: maybeLocale,
31
+ }
32
+ : {
33
+ nonLocalePath: nonQsPath.startsWith('/') ? nonQsPath : `/${nonQsPath}`,
34
+ queryString: urlQs,
35
+ };
36
+ };
37
+ /**
38
+ * Matches redirect-map rules (rules without a `locale` field) against the incoming request.
39
+ * The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
40
+ * guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
41
+ * avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
42
+ * @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
43
+ * @param {string} requestLocale locale of the current request
44
+ * @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
45
+ * @returns {RedirectResult | undefined} First matching redirect or undefined.
46
+ * @internal
47
+ */
48
+ export const matchFromRedirectMapRedirect = (redirects, requestLocale, incomingPathData) => {
49
+ const redirectMapRedirects = redirects.filter((redirect) => !redirect.locale);
50
+ // start from the locale-less path and rebuild the locale-prefixed variant from it
51
+ // (keep root as `/` so anchored `^/...` patterns still match). Trailing slashes are preserved.
52
+ const nonLocalePath = incomingPathData.nonLocalePath.toLowerCase() || '/';
53
+ const localePath = `/${requestLocale.toLowerCase()}${nonLocalePath}`;
54
+ const incomingQS = incomingPathData.queryString || '';
55
+ // regex rules are matched against the raw candidates (trailing slash preserved so slash-sensitive
56
+ // patterns still match); static rules ignore trailing slashes, so they use trimmed candidates
57
+ const pathCandidates = [nonLocalePath, localePath];
58
+ const staticCandidates = pathCandidates.map((path) => path.replace(/\/+$/g, '') || '/');
59
+ return redirectMapRedirects.find((redirect) => {
60
+ // process static URL (non-regex) rules
61
+ if (isRegexOrUrl(redirect.pattern) === 'url') {
62
+ const [rawPatternPath, patternQS] = (redirect.pattern.endsWith('/') ? redirect.pattern.slice(0, -1) : redirect.pattern).split('?');
63
+ // candidates are already lowercased; lowercase the pattern path to keep locale (and route)
64
+ // comparison case-insensitive
65
+ const patternPath = rawPatternPath.toLowerCase();
66
+ return (staticCandidates.includes(patternPath) &&
67
+ (!patternQS ||
68
+ areURLSearchParamsEqual(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
69
+ }
70
+ // process regex rules
71
+ const regex = safeCompileRedirectPattern(redirect.pattern);
72
+ if (!regex) {
73
+ return false;
74
+ }
75
+ const testRegex = (value) => {
76
+ regex.lastIndex = 0;
77
+ return regex.test(value);
78
+ };
79
+ // remove duplicates
80
+ const regexCandidates = [...pathCandidates, ...staticCandidates].filter((candidate, index, array) => array.indexOf(candidate) === index);
81
+ const matchedPath = regexCandidates.find((candidate) => testRegex(candidate));
82
+ const matchedPathWithQuery = incomingQS
83
+ ? regexCandidates.find((candidate) => testRegex(`${candidate}?${incomingQS}`))
84
+ : undefined;
85
+ // Save the matched path/query (if found) into the redirect object
86
+ redirect.matchedQueryString = matchedPathWithQuery ? `?${incomingQS}` : '';
87
+ redirect.matchedPath = matchedPath || matchedPathWithQuery || '';
88
+ return !!(matchedPath || matchedPathWithQuery);
89
+ });
90
+ };
91
+ /**
92
+ * Processes redirect rules from redirect items (language-versioned)
93
+ * @param {RedirectResult[]} redirects redirect entries from Edge
94
+ * @param {string} locale current request locale
95
+ * @param {string} nonLocalePath current request path with locale prefix stripped
96
+ * @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
97
+ * @internal
98
+ */
99
+ export const matchRedirectItemRedirect = (redirects, locale, nonLocalePath) => {
100
+ return redirects.find((redirect) => {
101
+ const patternPath = redirect.pattern.replace(/\/+$/g, '').toLowerCase();
102
+ // locale rules are easy and nice
103
+ return redirect.locale === locale && patternPath === nonLocalePath;
104
+ });
105
+ };
106
+ /**
107
+ * Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
108
+ * so one bad entry does not fail the entire redirect chain.
109
+ * Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
110
+ * @param {string} pattern redirect pattern from redirect map
111
+ * @returns {RegExp | null} normalized regex instance, or null when invalid
112
+ * @internal
113
+ */
114
+ export const safeCompileRedirectPattern = (pattern) => {
115
+ try {
116
+ const normalizedPattern = escapeNonSpecialQuestionMarks(pattern);
117
+ const literalMatch = normalizedPattern.match(/^\/(.+)\/([a-z]*)$/i);
118
+ if (literalMatch) {
119
+ const [, source, flags] = literalMatch;
120
+ const safeFlags = flags || 'i';
121
+ return new RegExp(source, safeFlags);
122
+ }
123
+ return new RegExp(normalizedPattern, 'i');
124
+ }
125
+ catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ console.warn(`[RedirectsProxy] Invalid redirect regex; skipping rule. pattern=${pattern} (${message})`);
128
+ return null;
129
+ }
130
+ };
131
+ /**
132
+ * Detects absolute (external or protocol-relative) target URLs.
133
+ * @param {string} target redirect target
134
+ * @returns {boolean} true when the target is an absolute URL
135
+ * @internal
136
+ */
137
+ export const isAbsoluteTarget = (target) => REGEXP_ABSOLUTE_URL.test(target);
138
+ /**
139
+ * Resolves the redirect target string: replaces the `$siteLang` token and applies
140
+ * regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
141
+ * @param {RedirectResult} existsRedirect matched redirect
142
+ * @param {string} siteLanguage site language used for the `$siteLang` token
143
+ * @param {string} requestPath incoming request path, used when the rule stored no matched path
144
+ * @returns {string} resolved target
145
+ * @internal
146
+ */
147
+ export const resolveRedirectTarget = (existsRedirect, siteLanguage, requestPath) => {
148
+ let target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, siteLanguage);
149
+ // Apply regex replacements to the target URL if the pattern is a regex
150
+ if (isRegexOrUrl(existsRedirect.pattern) === 'regex') {
151
+ const sourcePath = existsRedirect.matchedPath || requestPath;
152
+ const pathForCaptureMatch = sourcePath.replace(/\/+$/g, '') || '/';
153
+ const redirectRegex = safeCompileRedirectPattern(existsRedirect.pattern);
154
+ const matched = redirectRegex ? pathForCaptureMatch.match(redirectRegex) : null;
155
+ if (matched) {
156
+ target = target.replace(/\$(\d+)/g, (_, index) => matched[parseInt(index, 10)] || '');
157
+ }
158
+ }
159
+ return target;
160
+ };
161
+ /**
162
+ * Resolves the absolute redirect target (external redirects), optionally preseriving query string
163
+ * @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
164
+ * @param {RedirectResult} existsRedirect matched redirect to resolve final result from
165
+ * @returns {string} URL to redirect to
166
+ * @internal
167
+ */
168
+ export const processAbsoluteUrlTarget = (incomingPathData, existsRedirect) => {
169
+ if (!incomingPathData.queryString || !existsRedirect.isQueryStringPreserved) {
170
+ return existsRedirect.target;
171
+ }
172
+ const [targetMainUrl, targetQS] = existsRedirect.target.split('?');
173
+ const mergedQueryString = mergeURLSearchParams(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(targetQS || ''));
174
+ return `${targetMainUrl}?${mergedQueryString}`;
175
+ };
176
+ /**
177
+ * Resolves the locale-less path and target locale for a relative redirect URL.
178
+ * The framework layer is responsible for placing the locale (pathname vs. locale property).
179
+ * @param {ProcessedPath} incomingPathData broken-down incoming request path
180
+ * @param {RedirectResult} existsRedirect matched redirect
181
+ * @param {string[]} configuredLocales configured site locales
182
+ * @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
183
+ * @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
184
+ * @internal
185
+ */
186
+ export const processRelativeUrlTarget = (incomingPathData, existsRedirect, configuredLocales, reqLocale) => {
187
+ const { locale, nonLocalePath, queryString } = breakDownPath(configuredLocales, existsRedirect.target);
188
+ // preserve the request locale when the target has no locale prefix and the rule opts in
189
+ const targetLocale = locale || (existsRedirect.isLanguagePreserved ? reqLocale : '');
190
+ const targetQS = incomingPathData.queryString && existsRedirect.isQueryStringPreserved
191
+ ? mergeURLSearchParams(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(queryString || ''))
192
+ : queryString || '';
193
+ return {
194
+ targetLocale,
195
+ targetPath: `${nonLocalePath}${targetQS ? '?' + targetQS : ''}`,
196
+ };
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sitecore-content-sdk/content",
3
- "version": "2.2.1",
3
+ "version": "2.2.2-canary.20260727010537",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "sideEffects": false,
@@ -6,5 +6,6 @@ export { SiteInfoService, SiteInfoServiceConfig } from './siteinfo-service';
6
6
  export { SitePathService, SitePathServiceConfig } from './sitepath-service';
7
7
  export { SiteInfo } from './models';
8
8
  export { getSiteRewrite, getSiteRewriteData, normalizeSiteRewrite, SiteRewriteData, SITE_PREFIX, SITE_KEY, getHostnameFromHostHeader, } from './utils';
9
+ export * from './redirect-utils';
9
10
  export { SiteResolver } from './site-resolver';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAE/F,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,QAAQ,EACR,yBAAyB,GAC1B,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAE/F,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,QAAQ,EACR,yBAAyB,GAC1B,MAAM,SAAS,CAAC;AACjB,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { RedirectInfo } from '../site';
2
+ /**
3
+ * Expanded redirect info to contain matchers for regexp match
4
+ * @internal
5
+ */
6
+ export type RedirectResult = RedirectInfo & {
7
+ matchedQueryString?: string;
8
+ matchedPath?: string;
9
+ };
10
+ /**
11
+ * Broken down path with all parts needed to make a match for a redirect rule
12
+ * @internal
13
+ */
14
+ export type ProcessedPath = {
15
+ nonLocalePath: string;
16
+ locale?: string;
17
+ queryString?: string;
18
+ };
19
+ /**
20
+ * Splits a URL path into its (optional) leading locale segment, the remaining
21
+ * locale-less path, and the query string. Locales are compared case-insensitively.
22
+ * A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
23
+ * so slash-sensitive regex redirect rules can still be matched downstream.
24
+ * @param {string[]} configuredLocales configured site locales
25
+ * @param {string} urlPath path (optionally including a `?query`)
26
+ * @returns {ProcessedPath} broken-down path parts
27
+ * @internal
28
+ */
29
+ export declare const breakDownPath: (configuredLocales: string[], urlPath: string) => ProcessedPath;
30
+ /**
31
+ * Matches redirect-map rules (rules without a `locale` field) against the incoming request.
32
+ * The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
33
+ * guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
34
+ * avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
35
+ * @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
36
+ * @param {string} requestLocale locale of the current request
37
+ * @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
38
+ * @returns {RedirectResult | undefined} First matching redirect or undefined.
39
+ * @internal
40
+ */
41
+ export declare const matchFromRedirectMapRedirect: (redirects: RedirectResult[], requestLocale: string, incomingPathData: ProcessedPath) => RedirectResult | undefined;
42
+ /**
43
+ * Processes redirect rules from redirect items (language-versioned)
44
+ * @param {RedirectResult[]} redirects redirect entries from Edge
45
+ * @param {string} locale current request locale
46
+ * @param {string} nonLocalePath current request path with locale prefix stripped
47
+ * @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
48
+ * @internal
49
+ */
50
+ export declare const matchRedirectItemRedirect: (redirects: RedirectResult[], locale: string, nonLocalePath: string) => RedirectResult | undefined;
51
+ /**
52
+ * Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
53
+ * so one bad entry does not fail the entire redirect chain.
54
+ * Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
55
+ * @param {string} pattern redirect pattern from redirect map
56
+ * @returns {RegExp | null} normalized regex instance, or null when invalid
57
+ * @internal
58
+ */
59
+ export declare const safeCompileRedirectPattern: (pattern: string) => RegExp | null;
60
+ /**
61
+ * Detects absolute (external or protocol-relative) target URLs.
62
+ * @param {string} target redirect target
63
+ * @returns {boolean} true when the target is an absolute URL
64
+ * @internal
65
+ */
66
+ export declare const isAbsoluteTarget: (target: string) => boolean;
67
+ /**
68
+ * Resolves the redirect target string: replaces the `$siteLang` token and applies
69
+ * regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
70
+ * @param {RedirectResult} existsRedirect matched redirect
71
+ * @param {string} siteLanguage site language used for the `$siteLang` token
72
+ * @param {string} requestPath incoming request path, used when the rule stored no matched path
73
+ * @returns {string} resolved target
74
+ * @internal
75
+ */
76
+ export declare const resolveRedirectTarget: (existsRedirect: RedirectResult, siteLanguage: string, requestPath: string) => string;
77
+ /**
78
+ * Resolves the absolute redirect target (external redirects), optionally preseriving query string
79
+ * @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
80
+ * @param {RedirectResult} existsRedirect matched redirect to resolve final result from
81
+ * @returns {string} URL to redirect to
82
+ * @internal
83
+ */
84
+ export declare const processAbsoluteUrlTarget: (incomingPathData: ProcessedPath, existsRedirect: RedirectResult) => string;
85
+ /**
86
+ * Resolves the locale-less path and target locale for a relative redirect URL.
87
+ * The framework layer is responsible for placing the locale (pathname vs. locale property).
88
+ * @param {ProcessedPath} incomingPathData broken-down incoming request path
89
+ * @param {RedirectResult} existsRedirect matched redirect
90
+ * @param {string[]} configuredLocales configured site locales
91
+ * @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
92
+ * @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
93
+ * @internal
94
+ */
95
+ export declare const processRelativeUrlTarget: (incomingPathData: ProcessedPath, existsRedirect: RedirectResult, configuredLocales: string[], reqLocale: string) => {
96
+ targetLocale: string;
97
+ targetPath: string;
98
+ };
99
+ //# sourceMappingURL=redirect-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-utils.d.ts","sourceRoot":"","sources":["../../src/site/redirect-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAWvC;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,mBAAmB,MAAM,EAAE,EAAE,SAAS,MAAM,KAAG,aAwB5E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACvC,WAAW,cAAc,EAAE,EAC3B,eAAe,MAAM,EACrB,kBAAkB,aAAa,KAC9B,cAAc,GAAG,SAqDnB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GACpC,WAAW,cAAc,EAAE,EAC3B,QAAQ,MAAM,EACd,eAAe,MAAM,KACpB,cAAc,GAAG,SAMnB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAiBrE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,KAAG,OAA2C,CAAC;AAE9F;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,gBAAgB,cAAc,EAC9B,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GACnC,kBAAkB,aAAa,EAC/B,gBAAgB,cAAc,KAC7B,MAUF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACnC,kBAAkB,aAAa,EAC/B,gBAAgB,cAAc,EAC9B,mBAAmB,MAAM,EAAE,EAC3B,WAAW,MAAM,KAChB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAoB5C,CAAC"}