@sitecore-content-sdk/nextjs 2.0.2 → 2.1.0-canary.10
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.
- package/LICENSE.txt +202 -202
- package/dist/cjs/components/Link.js +6 -1
- package/dist/cjs/initialization/proxy/analytics-adapter.js +6 -3
- package/dist/cjs/proxy/bot-tracking-proxy.js +127 -0
- package/dist/cjs/proxy/index.js +3 -1
- package/dist/cjs/proxy/personalize-proxy.js +10 -3
- package/dist/cjs/proxy/proxy.js +32 -2
- package/dist/cjs/proxy/redirects-proxy.js +49 -11
- package/dist/esm/components/Link.js +6 -1
- package/dist/esm/initialization/proxy/analytics-adapter.js +5 -2
- package/dist/esm/proxy/bot-tracking-proxy.js +120 -0
- package/dist/esm/proxy/index.js +1 -0
- package/dist/esm/proxy/personalize-proxy.js +10 -3
- package/dist/esm/proxy/proxy.js +32 -2
- package/dist/esm/proxy/redirects-proxy.js +49 -11
- package/package.json +13 -12
- package/types/components/Link.d.ts.map +1 -1
- package/types/initialization/proxy/analytics-adapter.d.ts.map +1 -1
- package/types/proxy/bot-tracking-proxy.d.ts +33 -0
- package/types/proxy/bot-tracking-proxy.d.ts.map +1 -0
- package/types/proxy/index.d.ts +1 -0
- package/types/proxy/index.d.ts.map +1 -1
- package/types/proxy/personalize-proxy.d.ts +5 -0
- package/types/proxy/personalize-proxy.d.ts.map +1 -1
- package/types/proxy/proxy.d.ts +1 -1
- package/types/proxy/proxy.d.ts.map +1 -1
- package/types/proxy/redirects-proxy.d.ts +20 -1
- package/types/proxy/redirects-proxy.d.ts.map +1 -1
|
@@ -5,13 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PersonalizeProxy = void 0;
|
|
7
7
|
const personalize_1 = require("@sitecore-content-sdk/content/personalize");
|
|
8
|
-
const
|
|
8
|
+
const internal_1 = require("@sitecore-content-sdk/analytics-core/internal");
|
|
9
9
|
const core_1 = require("@sitecore-content-sdk/core");
|
|
10
10
|
const personalize_2 = require("@sitecore-content-sdk/personalize");
|
|
11
|
-
const debug_1 = __importDefault(require("../debug"));
|
|
12
11
|
const analytics_core_1 = require("@sitecore-content-sdk/analytics-core");
|
|
13
|
-
const analytics_adapter_1 = require("../initialization/proxy/analytics-adapter");
|
|
14
12
|
const personalize_3 = require("@sitecore-content-sdk/personalize");
|
|
13
|
+
const analytics_adapter_1 = require("../initialization/proxy/analytics-adapter");
|
|
14
|
+
const proxy_1 = require("./proxy");
|
|
15
|
+
const debug_1 = __importDefault(require("../debug"));
|
|
15
16
|
const personalize_adapter_1 = require("../initialization/proxy/personalize-adapter");
|
|
16
17
|
/**
|
|
17
18
|
* Proxy / handler to support Sitecore Personalize
|
|
@@ -26,11 +27,13 @@ class PersonalizeProxy extends proxy_1.ProxyBase {
|
|
|
26
27
|
super(config);
|
|
27
28
|
this.config = config;
|
|
28
29
|
this.handle = async (req, res) => {
|
|
30
|
+
var _a, _b;
|
|
29
31
|
if (!this.config.enabled) {
|
|
30
32
|
debug_1.default.personalize('skipped (personalize proxy is disabled globally)');
|
|
31
33
|
return res;
|
|
32
34
|
}
|
|
33
35
|
try {
|
|
36
|
+
const skipForBot = (_a = this.config.skipForBot) !== null && _a !== void 0 ? _a : true;
|
|
34
37
|
const pathname = req.nextUrl.pathname;
|
|
35
38
|
const language = this.getLanguage(req, res);
|
|
36
39
|
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
@@ -52,6 +55,10 @@ class PersonalizeProxy extends proxy_1.ProxyBase {
|
|
|
52
55
|
debug_1.default.personalize('skipped (%s)', res.redirected ? 'redirected' : 'preview');
|
|
53
56
|
return res;
|
|
54
57
|
}
|
|
58
|
+
if (skipForBot && ((_b = req.cookies.get(internal_1.BOT_DETECTION_COOKIE)) === null || _b === void 0 ? void 0 : _b.value)) {
|
|
59
|
+
debug_1.default.personalize('skipped (bot request)');
|
|
60
|
+
return res;
|
|
61
|
+
}
|
|
55
62
|
const site = this.getSite(req, res);
|
|
56
63
|
// Get personalization info from Experience Edge
|
|
57
64
|
// personalizeService is guaranteed to be non-null here because disabled() check passed
|
package/dist/cjs/proxy/proxy.js
CHANGED
|
@@ -17,6 +17,37 @@ exports.LOCALE_HEADER_NAME = 'x-sc-locale';
|
|
|
17
17
|
class ProxyHandler {
|
|
18
18
|
}
|
|
19
19
|
exports.ProxyHandler = ProxyHandler;
|
|
20
|
+
/**
|
|
21
|
+
* Hostname from a `Host` or `x-forwarded-host` value, without port.
|
|
22
|
+
* - `[::1]:3000` → `::1`
|
|
23
|
+
* - `127.0.0.1:3000` → `127.0.0.1`
|
|
24
|
+
* - `example.com:443` → `example.com`
|
|
25
|
+
* - `::1` → `::1` (does not treat `:1` as a port)
|
|
26
|
+
* @param {string} host - Raw header value
|
|
27
|
+
*/
|
|
28
|
+
function getHostnameFromHostHeader(host) {
|
|
29
|
+
const trimmed = host.trim();
|
|
30
|
+
// Bracketed IPv6: "[...]:port" or "[...]"
|
|
31
|
+
if (trimmed.startsWith('[')) {
|
|
32
|
+
const end = trimmed.indexOf(']');
|
|
33
|
+
if (end !== -1) {
|
|
34
|
+
return trimmed.slice(1, end).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Unbracketed IPv6 (e.g. ::1, 2001:db8::1) — never strip on last ":digits"
|
|
38
|
+
if (trimmed.includes('::')) {
|
|
39
|
+
return trimmed.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
// IPv4 or DNS name with ":port" (port = decimal digits only)
|
|
42
|
+
const lastColon = trimmed.lastIndexOf(':');
|
|
43
|
+
if (lastColon > 0) {
|
|
44
|
+
const after = trimmed.slice(lastColon + 1);
|
|
45
|
+
if (/^\d+$/.test(after)) {
|
|
46
|
+
return trimmed.slice(0, lastColon).toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return trimmed.toLowerCase();
|
|
50
|
+
}
|
|
20
51
|
/**
|
|
21
52
|
* Base proxy class with common methods
|
|
22
53
|
* @public
|
|
@@ -113,8 +144,7 @@ class ProxyBase extends ProxyHandler {
|
|
|
113
144
|
* @param {NextRequest} req request
|
|
114
145
|
*/
|
|
115
146
|
getHostHeader(req) {
|
|
116
|
-
|
|
117
|
-
return req.headers.get('x-forwarded-host') || ((_a = req.headers.get('host')) === null || _a === void 0 ? void 0 : _a.split(':')[0]);
|
|
147
|
+
return getHostnameFromHostHeader(req.headers.get('x-forwarded-host') || req.headers.get('host') || '');
|
|
118
148
|
}
|
|
119
149
|
/**
|
|
120
150
|
* Get site information. If site name is stored in cookie, use it, otherwise resolve by hostname
|
|
@@ -65,7 +65,7 @@ class RedirectsProxy extends proxy_1.ProxyBase {
|
|
|
65
65
|
}
|
|
66
66
|
site = this.getSite(req, res);
|
|
67
67
|
// Find the redirect from result of RedirectService
|
|
68
|
-
const existsRedirect = await this.getExistsRedirect(req, site.name);
|
|
68
|
+
const existsRedirect = await this.getExistsRedirect(req, site.name, language);
|
|
69
69
|
if (!existsRedirect) {
|
|
70
70
|
debug_1.default.redirects('skipped (redirect does not exist)');
|
|
71
71
|
return res;
|
|
@@ -202,19 +202,38 @@ class RedirectsProxy extends proxy_1.ProxyBase {
|
|
|
202
202
|
* @returns Promise<RedirectInfo | undefined>
|
|
203
203
|
* @private
|
|
204
204
|
*/
|
|
205
|
-
async getExistsRedirect(req, siteName) {
|
|
205
|
+
async getExistsRedirect(req, siteName, requestLocale) {
|
|
206
206
|
if (!this.redirectsService) {
|
|
207
207
|
return undefined;
|
|
208
208
|
}
|
|
209
209
|
const { pathname: incomingURL, search: incomingQS = '' } = this.normalizeUrl(req.nextUrl.clone());
|
|
210
|
-
const locale = this.getLanguage(req);
|
|
211
210
|
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
212
211
|
const redirects = await this.redirectsService.fetchRedirects(siteName);
|
|
213
|
-
|
|
212
|
+
// using locale of current request (from URL, headers or otherwise), used to match versioned redirect rules
|
|
213
|
+
const matchedLocaleRedirect = this.matchRedirectItemRedirect(redirects, requestLocale, normalizedPath);
|
|
214
|
+
if (matchedLocaleRedirect) {
|
|
215
|
+
return matchedLocaleRedirect;
|
|
216
|
+
}
|
|
217
|
+
// locale of the url - if present in the url, used for redirect map matching
|
|
218
|
+
const urlLocale = req.nextUrl.locale;
|
|
219
|
+
return this.matchFromRedirectMapRedirect(redirects, urlLocale, incomingURL, incomingQS);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Matches redirect-map rules without a `locale` field against the incoming URL (static or regex patterns).
|
|
223
|
+
* @param {RedirectResult[]} redirects All redirects from the service (non-locale entries are filtered inside).
|
|
224
|
+
* @param {string} urlLocale Locale segment from the request URL (`nextUrl.locale`).
|
|
225
|
+
* @param {string} incomingURL Original pathname used for regex tests.
|
|
226
|
+
* @param {string} incomingQS Query string including leading `?` if present.
|
|
227
|
+
* @returns {RedirectResult | undefined} First matching redirect or undefined.
|
|
228
|
+
* @private
|
|
229
|
+
*/
|
|
230
|
+
matchFromRedirectMapRedirect(redirects, urlLocale, incomingURL, incomingQS) {
|
|
231
|
+
const nonLocaleRedirects = redirects.filter((redirect) => !redirect.locale);
|
|
214
232
|
let matchedQueryString;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
234
|
+
const localePath = `/${urlLocale.toLowerCase()}${normalizedPath}`;
|
|
235
|
+
return nonLocaleRedirects.length
|
|
236
|
+
? nonLocaleRedirects.find((redirect) => {
|
|
218
237
|
// process static URL (non-regex) rules
|
|
219
238
|
if ((0, tools_1.isRegexOrUrl)(redirect.pattern) === 'url') {
|
|
220
239
|
const urlArray = redirect.pattern.endsWith('/')
|
|
@@ -236,14 +255,14 @@ class RedirectsProxy extends proxy_1.ProxyBase {
|
|
|
236
255
|
// process regex rules
|
|
237
256
|
// Modify the redirect pattern to ignore the language prefix in the path
|
|
238
257
|
// And escapes non-special "?" characters in a string or regex.
|
|
239
|
-
redirect.pattern = (0, tools_1.escapeNonSpecialQuestionMarks)('^' + redirect.pattern.replace(new RegExp(`^[^]?/${
|
|
258
|
+
redirect.pattern = (0, tools_1.escapeNonSpecialQuestionMarks)('^' + redirect.pattern.replace(new RegExp(`^[^]?/${urlLocale}/`, 'gi'), '') // ensure function thinks input is regex
|
|
240
259
|
);
|
|
241
260
|
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
|
|
242
261
|
redirect.pattern = `/^\/${redirect.pattern
|
|
243
262
|
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
|
|
244
263
|
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
|
|
245
264
|
.replace(/^\^|\$$/g, '') // Further cleans up anchors
|
|
246
|
-
.replace(/\$\/
|
|
265
|
+
.replace(/\$\/g$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
|
|
247
266
|
// Redirect pattern matches the full incoming URL with query string present
|
|
248
267
|
matchedQueryString = [
|
|
249
268
|
(0, regex_parser_1.default)(redirect.pattern).test(`/${localePath}${incomingQS}`),
|
|
@@ -253,9 +272,28 @@ class RedirectsProxy extends proxy_1.ProxyBase {
|
|
|
253
272
|
: undefined;
|
|
254
273
|
// Save the matched query string (if found) into the redirect object
|
|
255
274
|
redirect.matchedQueryString = matchedQueryString || '';
|
|
256
|
-
return (!!((0, regex_parser_1.default)(redirect.pattern).test(`/${
|
|
275
|
+
return (!!((0, regex_parser_1.default)(redirect.pattern).test(`/${urlLocale}${incomingURL}`) ||
|
|
257
276
|
(0, regex_parser_1.default)(redirect.pattern).test(incomingURL) ||
|
|
258
|
-
matchedQueryString) &&
|
|
277
|
+
matchedQueryString) &&
|
|
278
|
+
(redirect.locale ? redirect.locale.toLowerCase() === urlLocale.toLowerCase() : true));
|
|
279
|
+
})
|
|
280
|
+
: undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Processes redirect rules from redirect items (language-versioned)
|
|
284
|
+
* @param {RedirectResult[]} redirects redirect entries from Edge
|
|
285
|
+
* @param {string} locale current request locale
|
|
286
|
+
* @param {string} currentPath current request path
|
|
287
|
+
* @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
matchRedirectItemRedirect(redirects, locale, currentPath) {
|
|
291
|
+
const nonLocalePath = currentPath.replace(new RegExp(`^\/?${locale}\/`, 'i'), '/');
|
|
292
|
+
return redirects.length
|
|
293
|
+
? redirects.find((redirect) => {
|
|
294
|
+
const patternPath = redirect.pattern.replace(/\/*$/g, '').toLowerCase();
|
|
295
|
+
// locale rules are easy and nice
|
|
296
|
+
return redirect.locale === locale && patternPath === nonLocalePath;
|
|
259
297
|
})
|
|
260
298
|
: undefined;
|
|
261
299
|
}
|
|
@@ -13,6 +13,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
13
13
|
import React, { forwardRef } from 'react';
|
|
14
14
|
import NextLink from 'next/link';
|
|
15
15
|
import { Link as ReactLink, } from '@sitecore-content-sdk/react';
|
|
16
|
+
import { useRouter as usePageRouter } from 'next/compat/router';
|
|
16
17
|
/**
|
|
17
18
|
* The list of NextLink props to be supported by the Link component.
|
|
18
19
|
*/
|
|
@@ -34,6 +35,7 @@ const FILE_EXTENSION_MATCHER = /^\/.*\.\w+$/;
|
|
|
34
35
|
* @public
|
|
35
36
|
*/
|
|
36
37
|
export const Link = forwardRef((props, ref) => {
|
|
38
|
+
const pageRouter = usePageRouter();
|
|
37
39
|
const { field, editable = true, children, internalLinkMatcher = /^\//g, showLinkTextWithChildrenPresent } = props, rest = __rest(props, ["field", "editable", "children", "internalLinkMatcher", "showLinkTextWithChildrenPresent"]);
|
|
38
40
|
if (!field || (!field.value && !field.href && !field.metadata)) {
|
|
39
41
|
return null;
|
|
@@ -48,10 +50,13 @@ export const Link = forwardRef((props, ref) => {
|
|
|
48
50
|
const isFileUrl = FILE_EXTENSION_MATCHER.test(href);
|
|
49
51
|
// determine if a link is a route or not. File extensions are not routes and should not be pre-fetched.
|
|
50
52
|
if (isMatching && !isFileUrl) {
|
|
51
|
-
return (React.createElement(NextLink, Object.assign({ href: { pathname: href, query: querystring, hash: anchor }, key: "link", title: value.title, target: value.target, className: value.class }, rest, {
|
|
53
|
+
return (React.createElement(NextLink, Object.assign({ href: { pathname: href, query: querystring, hash: anchor }, key: "link", title: value.title, target: value.target, className: value.class }, rest, {
|
|
54
|
+
// locale prop is supported only in Pages Router
|
|
55
|
+
locale: pageRouter ? false : undefined, ref: ref }, (process.env.TEST
|
|
52
56
|
? {
|
|
53
57
|
'data-nextjs-link': true,
|
|
54
58
|
'data-nextjs-prefetch': props.prefetch,
|
|
59
|
+
'data-nextjs-locale': pageRouter ? false : undefined,
|
|
55
60
|
}
|
|
56
61
|
: {})),
|
|
57
62
|
text,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { COOKIE_NAME_PREFIX, fetchClientIdFromEdgeProxy, getDefaultCookieAttributes, } from '@sitecore-content-sdk/analytics-core/internal';
|
|
2
|
-
import { getAnalyticsPlugin, } from '@sitecore-content-sdk/analytics-core/internal';
|
|
1
|
+
import { COOKIE_NAME_PREFIX, fetchClientIdFromEdgeProxy, getBotCookieServerSide, getDefaultCookieAttributes, getAnalyticsPlugin, isBot, } from '@sitecore-content-sdk/analytics-core/internal';
|
|
3
2
|
import { getCoreContext } from '@sitecore-content-sdk/core';
|
|
4
3
|
/**
|
|
5
4
|
* Creates a proxy-based analytics adapter that reads and writes the visitor ID
|
|
@@ -13,6 +12,10 @@ import { getCoreContext } from '@sitecore-content-sdk/core';
|
|
|
13
12
|
export function analyticsProxyAdapter(request, response) {
|
|
14
13
|
return {
|
|
15
14
|
type: 'proxy',
|
|
15
|
+
isBot: () => {
|
|
16
|
+
const botCookie = getBotCookieServerSide(request.cookies.toString());
|
|
17
|
+
return !!botCookie || isBot(request.headers.get('user-agent'));
|
|
18
|
+
},
|
|
16
19
|
getClientId: () => {
|
|
17
20
|
return getClientId(request);
|
|
18
21
|
},
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { initContentSdk } from '@sitecore-content-sdk/core';
|
|
2
|
+
import { analyticsPlugin } from '@sitecore-content-sdk/analytics-core';
|
|
3
|
+
import { eventsPlugin, botPageView } from '@sitecore-content-sdk/events';
|
|
4
|
+
import { isBot, BOT_DETECTION_COOKIE } from '@sitecore-content-sdk/analytics-core/internal';
|
|
5
|
+
import { ProxyBase } from './proxy';
|
|
6
|
+
import debug from '../debug';
|
|
7
|
+
import { analyticsProxyAdapter } from '../initialization/proxy/analytics-adapter';
|
|
8
|
+
/**
|
|
9
|
+
* Next.js proxy that runs bot detection once per request and sets the bot cookie.
|
|
10
|
+
* Run first in the proxy chain to ensure that the bot cookie is set before other proxies run.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export class BotTrackingProxy extends ProxyBase {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super(config);
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.handle = async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const isDisabled = (this.config.skip && this.config.skip(req, res)) || false;
|
|
20
|
+
debug.common('bot tracking proxy start: %o', {
|
|
21
|
+
pathname: req.nextUrl.pathname,
|
|
22
|
+
headers: this.extractDebugHeaders(req.headers),
|
|
23
|
+
});
|
|
24
|
+
if (isDisabled) {
|
|
25
|
+
debug.common('bot tracking proxy skipped (disabled)');
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
if (this.isPreview(req)) {
|
|
29
|
+
debug.common('bot tracking proxy skipped (preview)');
|
|
30
|
+
return res;
|
|
31
|
+
}
|
|
32
|
+
if (this.shouldSkipForLocalEnvironment(req)) {
|
|
33
|
+
debug.common('bot tracking proxy skipped (local environment)');
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
const userAgent = req.headers.get('user-agent');
|
|
37
|
+
if (!userAgent) {
|
|
38
|
+
debug.common('bot tracking proxy skipped (no user-agent)');
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
41
|
+
if (!isBot(userAgent)) {
|
|
42
|
+
debug.common('bot tracking proxy skipped (not a bot)');
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
if (this.isPrefetch(req)) {
|
|
46
|
+
debug.common('bot tracking proxy skipped (prefetch)');
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
const site = this.getSite(req, res);
|
|
50
|
+
const language = this.getLanguage(req, res);
|
|
51
|
+
const botTracking = async () => {
|
|
52
|
+
await initContentSdk({
|
|
53
|
+
config: {
|
|
54
|
+
contextId: this.config.contextId,
|
|
55
|
+
edgeUrl: this.config.edgeUrl,
|
|
56
|
+
siteName: site.name,
|
|
57
|
+
},
|
|
58
|
+
plugins: [
|
|
59
|
+
analyticsPlugin({
|
|
60
|
+
options: {
|
|
61
|
+
enableCookie: false,
|
|
62
|
+
},
|
|
63
|
+
adapter: analyticsProxyAdapter(req, res),
|
|
64
|
+
}),
|
|
65
|
+
eventsPlugin(),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
await botPageView({
|
|
69
|
+
page: req.nextUrl.pathname,
|
|
70
|
+
language: language,
|
|
71
|
+
userAgent,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
res.cookies.set(BOT_DETECTION_COOKIE, '1', {
|
|
75
|
+
secure: true,
|
|
76
|
+
sameSite: 'lax',
|
|
77
|
+
path: '/',
|
|
78
|
+
});
|
|
79
|
+
debug.common('bot tracking proxy (visitor is a bot)');
|
|
80
|
+
if (this.config.fetchEvent) {
|
|
81
|
+
this.config.fetchEvent.waitUntil(botTracking());
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await botTracking();
|
|
85
|
+
}
|
|
86
|
+
debug.common('bot tracking proxy end: %o', {
|
|
87
|
+
pathname: req.nextUrl.pathname,
|
|
88
|
+
cookies: res.cookies,
|
|
89
|
+
});
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
debug.common('bot tracking proxy error: %o', error);
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* @param {NextRequest} req - Incoming request
|
|
100
|
+
* @returns True when bot tracking should be skipped for a local / dev environment.
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
shouldSkipForLocalEnvironment(req) {
|
|
104
|
+
// Allow bot tracking in local environment for development purposes
|
|
105
|
+
if (process.env.SITECORE_ENABLE_BOT_TRACKING === 'true') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (process.env.NODE_ENV === 'development') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const hostName = (this.getHostHeader(req) ||
|
|
112
|
+
req.nextUrl.hostname ||
|
|
113
|
+
this.defaultHostname ||
|
|
114
|
+
'').toLowerCase();
|
|
115
|
+
return (hostName === 'localhost' ||
|
|
116
|
+
hostName === '127.0.0.1' ||
|
|
117
|
+
hostName === '::1' ||
|
|
118
|
+
hostName === '[::1]');
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/esm/proxy/index.js
CHANGED
|
@@ -5,5 +5,6 @@ export { MultisiteProxy } from './multisite-proxy';
|
|
|
5
5
|
export { AppRouterMultisiteProxy } from './app-router-multisite-proxy';
|
|
6
6
|
export { LocaleProxy } from './locale-proxy';
|
|
7
7
|
export { PersonalizeService, } from '@sitecore-content-sdk/content/personalize';
|
|
8
|
+
export { BotTrackingProxy } from './bot-tracking-proxy';
|
|
8
9
|
export { RedirectsService, REDIRECT_TYPE_301, REDIRECT_TYPE_302, REDIRECT_TYPE_SERVER_TRANSFER, } from '@sitecore-content-sdk/content/site';
|
|
9
10
|
export { default as debug } from '../debug';
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { PersonalizeService, getPersonalizedRewrite, CdpHelper, DEFAULT_VARIANT, } from '@sitecore-content-sdk/content/personalize';
|
|
2
|
-
import {
|
|
2
|
+
import { BOT_DETECTION_COOKIE } from '@sitecore-content-sdk/analytics-core/internal';
|
|
3
3
|
import { initContentSdk } from '@sitecore-content-sdk/core';
|
|
4
4
|
import { personalize } from '@sitecore-content-sdk/personalize';
|
|
5
|
-
import debug from '../debug';
|
|
6
5
|
import { analyticsPlugin } from '@sitecore-content-sdk/analytics-core';
|
|
7
|
-
import { analyticsProxyAdapter } from '../initialization/proxy/analytics-adapter';
|
|
8
6
|
import { personalizeServerPlugin } from '@sitecore-content-sdk/personalize';
|
|
7
|
+
import { analyticsProxyAdapter } from '../initialization/proxy/analytics-adapter';
|
|
8
|
+
import { ProxyBase, REWRITE_HEADER_NAME } from './proxy';
|
|
9
|
+
import debug from '../debug';
|
|
9
10
|
import { personalizeProxyAdapter } from '../initialization/proxy/personalize-adapter';
|
|
10
11
|
/**
|
|
11
12
|
* Proxy / handler to support Sitecore Personalize
|
|
@@ -20,11 +21,13 @@ export class PersonalizeProxy extends ProxyBase {
|
|
|
20
21
|
super(config);
|
|
21
22
|
this.config = config;
|
|
22
23
|
this.handle = async (req, res) => {
|
|
24
|
+
var _a, _b;
|
|
23
25
|
if (!this.config.enabled) {
|
|
24
26
|
debug.personalize('skipped (personalize proxy is disabled globally)');
|
|
25
27
|
return res;
|
|
26
28
|
}
|
|
27
29
|
try {
|
|
30
|
+
const skipForBot = (_a = this.config.skipForBot) !== null && _a !== void 0 ? _a : true;
|
|
28
31
|
const pathname = req.nextUrl.pathname;
|
|
29
32
|
const language = this.getLanguage(req, res);
|
|
30
33
|
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
@@ -46,6 +49,10 @@ export class PersonalizeProxy extends ProxyBase {
|
|
|
46
49
|
debug.personalize('skipped (%s)', res.redirected ? 'redirected' : 'preview');
|
|
47
50
|
return res;
|
|
48
51
|
}
|
|
52
|
+
if (skipForBot && ((_b = req.cookies.get(BOT_DETECTION_COOKIE)) === null || _b === void 0 ? void 0 : _b.value)) {
|
|
53
|
+
debug.personalize('skipped (bot request)');
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
49
56
|
const site = this.getSite(req, res);
|
|
50
57
|
// Get personalization info from Experience Edge
|
|
51
58
|
// personalizeService is guaranteed to be non-null here because disabled() check passed
|
package/dist/esm/proxy/proxy.js
CHANGED
|
@@ -10,6 +10,37 @@ export const LOCALE_HEADER_NAME = 'x-sc-locale';
|
|
|
10
10
|
*/
|
|
11
11
|
export class ProxyHandler {
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Hostname from a `Host` or `x-forwarded-host` value, without port.
|
|
15
|
+
* - `[::1]:3000` → `::1`
|
|
16
|
+
* - `127.0.0.1:3000` → `127.0.0.1`
|
|
17
|
+
* - `example.com:443` → `example.com`
|
|
18
|
+
* - `::1` → `::1` (does not treat `:1` as a port)
|
|
19
|
+
* @param {string} host - Raw header value
|
|
20
|
+
*/
|
|
21
|
+
function getHostnameFromHostHeader(host) {
|
|
22
|
+
const trimmed = host.trim();
|
|
23
|
+
// Bracketed IPv6: "[...]:port" or "[...]"
|
|
24
|
+
if (trimmed.startsWith('[')) {
|
|
25
|
+
const end = trimmed.indexOf(']');
|
|
26
|
+
if (end !== -1) {
|
|
27
|
+
return trimmed.slice(1, end).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Unbracketed IPv6 (e.g. ::1, 2001:db8::1) — never strip on last ":digits"
|
|
31
|
+
if (trimmed.includes('::')) {
|
|
32
|
+
return trimmed.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
// IPv4 or DNS name with ":port" (port = decimal digits only)
|
|
35
|
+
const lastColon = trimmed.lastIndexOf(':');
|
|
36
|
+
if (lastColon > 0) {
|
|
37
|
+
const after = trimmed.slice(lastColon + 1);
|
|
38
|
+
if (/^\d+$/.test(after)) {
|
|
39
|
+
return trimmed.slice(0, lastColon).toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return trimmed.toLowerCase();
|
|
43
|
+
}
|
|
13
44
|
/**
|
|
14
45
|
* Base proxy class with common methods
|
|
15
46
|
* @public
|
|
@@ -106,8 +137,7 @@ export class ProxyBase extends ProxyHandler {
|
|
|
106
137
|
* @param {NextRequest} req request
|
|
107
138
|
*/
|
|
108
139
|
getHostHeader(req) {
|
|
109
|
-
|
|
110
|
-
return req.headers.get('x-forwarded-host') || ((_a = req.headers.get('host')) === null || _a === void 0 ? void 0 : _a.split(':')[0]);
|
|
140
|
+
return getHostnameFromHostHeader(req.headers.get('x-forwarded-host') || req.headers.get('host') || '');
|
|
111
141
|
}
|
|
112
142
|
/**
|
|
113
143
|
* Get site information. If site name is stored in cookie, use it, otherwise resolve by hostname
|
|
@@ -59,7 +59,7 @@ export class RedirectsProxy extends ProxyBase {
|
|
|
59
59
|
}
|
|
60
60
|
site = this.getSite(req, res);
|
|
61
61
|
// Find the redirect from result of RedirectService
|
|
62
|
-
const existsRedirect = await this.getExistsRedirect(req, site.name);
|
|
62
|
+
const existsRedirect = await this.getExistsRedirect(req, site.name, language);
|
|
63
63
|
if (!existsRedirect) {
|
|
64
64
|
debug.redirects('skipped (redirect does not exist)');
|
|
65
65
|
return res;
|
|
@@ -196,19 +196,38 @@ export class RedirectsProxy extends ProxyBase {
|
|
|
196
196
|
* @returns Promise<RedirectInfo | undefined>
|
|
197
197
|
* @private
|
|
198
198
|
*/
|
|
199
|
-
async getExistsRedirect(req, siteName) {
|
|
199
|
+
async getExistsRedirect(req, siteName, requestLocale) {
|
|
200
200
|
if (!this.redirectsService) {
|
|
201
201
|
return undefined;
|
|
202
202
|
}
|
|
203
203
|
const { pathname: incomingURL, search: incomingQS = '' } = this.normalizeUrl(req.nextUrl.clone());
|
|
204
|
-
const locale = this.getLanguage(req);
|
|
205
204
|
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
206
205
|
const redirects = await this.redirectsService.fetchRedirects(siteName);
|
|
207
|
-
|
|
206
|
+
// using locale of current request (from URL, headers or otherwise), used to match versioned redirect rules
|
|
207
|
+
const matchedLocaleRedirect = this.matchRedirectItemRedirect(redirects, requestLocale, normalizedPath);
|
|
208
|
+
if (matchedLocaleRedirect) {
|
|
209
|
+
return matchedLocaleRedirect;
|
|
210
|
+
}
|
|
211
|
+
// locale of the url - if present in the url, used for redirect map matching
|
|
212
|
+
const urlLocale = req.nextUrl.locale;
|
|
213
|
+
return this.matchFromRedirectMapRedirect(redirects, urlLocale, incomingURL, incomingQS);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Matches redirect-map rules without a `locale` field against the incoming URL (static or regex patterns).
|
|
217
|
+
* @param {RedirectResult[]} redirects All redirects from the service (non-locale entries are filtered inside).
|
|
218
|
+
* @param {string} urlLocale Locale segment from the request URL (`nextUrl.locale`).
|
|
219
|
+
* @param {string} incomingURL Original pathname used for regex tests.
|
|
220
|
+
* @param {string} incomingQS Query string including leading `?` if present.
|
|
221
|
+
* @returns {RedirectResult | undefined} First matching redirect or undefined.
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
matchFromRedirectMapRedirect(redirects, urlLocale, incomingURL, incomingQS) {
|
|
225
|
+
const nonLocaleRedirects = redirects.filter((redirect) => !redirect.locale);
|
|
208
226
|
let matchedQueryString;
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
227
|
+
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
228
|
+
const localePath = `/${urlLocale.toLowerCase()}${normalizedPath}`;
|
|
229
|
+
return nonLocaleRedirects.length
|
|
230
|
+
? nonLocaleRedirects.find((redirect) => {
|
|
212
231
|
// process static URL (non-regex) rules
|
|
213
232
|
if (isRegexOrUrl(redirect.pattern) === 'url') {
|
|
214
233
|
const urlArray = redirect.pattern.endsWith('/')
|
|
@@ -230,14 +249,14 @@ export class RedirectsProxy extends ProxyBase {
|
|
|
230
249
|
// process regex rules
|
|
231
250
|
// Modify the redirect pattern to ignore the language prefix in the path
|
|
232
251
|
// And escapes non-special "?" characters in a string or regex.
|
|
233
|
-
redirect.pattern = escapeNonSpecialQuestionMarks('^' + redirect.pattern.replace(new RegExp(`^[^]?/${
|
|
252
|
+
redirect.pattern = escapeNonSpecialQuestionMarks('^' + redirect.pattern.replace(new RegExp(`^[^]?/${urlLocale}/`, 'gi'), '') // ensure function thinks input is regex
|
|
234
253
|
);
|
|
235
254
|
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
|
|
236
255
|
redirect.pattern = `/^\/${redirect.pattern
|
|
237
256
|
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
|
|
238
257
|
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
|
|
239
258
|
.replace(/^\^|\$$/g, '') // Further cleans up anchors
|
|
240
|
-
.replace(/\$\/
|
|
259
|
+
.replace(/\$\/g$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
|
|
241
260
|
// Redirect pattern matches the full incoming URL with query string present
|
|
242
261
|
matchedQueryString = [
|
|
243
262
|
regexParser(redirect.pattern).test(`/${localePath}${incomingQS}`),
|
|
@@ -247,9 +266,28 @@ export class RedirectsProxy extends ProxyBase {
|
|
|
247
266
|
: undefined;
|
|
248
267
|
// Save the matched query string (if found) into the redirect object
|
|
249
268
|
redirect.matchedQueryString = matchedQueryString || '';
|
|
250
|
-
return (!!(regexParser(redirect.pattern).test(`/${
|
|
269
|
+
return (!!(regexParser(redirect.pattern).test(`/${urlLocale}${incomingURL}`) ||
|
|
251
270
|
regexParser(redirect.pattern).test(incomingURL) ||
|
|
252
|
-
matchedQueryString) &&
|
|
271
|
+
matchedQueryString) &&
|
|
272
|
+
(redirect.locale ? redirect.locale.toLowerCase() === urlLocale.toLowerCase() : true));
|
|
273
|
+
})
|
|
274
|
+
: undefined;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Processes redirect rules from redirect items (language-versioned)
|
|
278
|
+
* @param {RedirectResult[]} redirects redirect entries from Edge
|
|
279
|
+
* @param {string} locale current request locale
|
|
280
|
+
* @param {string} currentPath current request path
|
|
281
|
+
* @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
matchRedirectItemRedirect(redirects, locale, currentPath) {
|
|
285
|
+
const nonLocalePath = currentPath.replace(new RegExp(`^\/?${locale}\/`, 'i'), '/');
|
|
286
|
+
return redirects.length
|
|
287
|
+
? redirects.find((redirect) => {
|
|
288
|
+
const patternPath = redirect.pattern.replace(/\/*$/g, '').toLowerCase();
|
|
289
|
+
// locale rules are easy and nice
|
|
290
|
+
return redirect.locale === locale && patternPath === nonLocalePath;
|
|
253
291
|
})
|
|
254
292
|
: undefined;
|
|
255
293
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitecore-content-sdk/nextjs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.1.0-canary.10",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"module": "dist/esm/index.js",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"url": "https://github.com/sitecore/content-sdk/issues"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@sitecore-content-sdk/analytics-core": "
|
|
36
|
-
"@sitecore-content-sdk/personalize": "
|
|
35
|
+
"@sitecore-content-sdk/analytics-core": "2.1.0-canary.10",
|
|
36
|
+
"@sitecore-content-sdk/personalize": "2.1.0-canary.10",
|
|
37
37
|
"@stylistic/eslint-plugin": "^5.2.2",
|
|
38
38
|
"@testing-library/dom": "^10.4.0",
|
|
39
39
|
"@testing-library/react": "^16.3.0",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"glob": "^11.0.2",
|
|
64
64
|
"jsdom": "^26.1.0",
|
|
65
65
|
"mocha": "^11.2.2",
|
|
66
|
-
"next": "^16.
|
|
66
|
+
"next": "^16.2.0",
|
|
67
67
|
"nock": "14.0.0-beta.7",
|
|
68
68
|
"nyc": "^17.1.0",
|
|
69
69
|
"prettier": "^2.8.0",
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"typescript": "~5.8.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@sitecore-content-sdk/analytics-core": "^2.0.
|
|
80
|
-
"@sitecore-content-sdk/events": "^2.0.
|
|
81
|
-
"@sitecore-content-sdk/personalize": "^2.0.
|
|
82
|
-
"next": "^16.
|
|
79
|
+
"@sitecore-content-sdk/analytics-core": "^2.1.0-canary.3",
|
|
80
|
+
"@sitecore-content-sdk/events": "^2.1.0-canary.3",
|
|
81
|
+
"@sitecore-content-sdk/personalize": "^2.1.0-canary.3",
|
|
82
|
+
"next": "^16.2.0",
|
|
83
83
|
"react": "^19.2.1",
|
|
84
84
|
"react-dom": "^19.2.1",
|
|
85
85
|
"typescript": "^5.4.0"
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"@babel/parser": "^7.27.2",
|
|
94
|
-
"@sitecore-content-sdk/content": "
|
|
95
|
-
"@sitecore-content-sdk/core": "
|
|
96
|
-
"@sitecore-content-sdk/
|
|
94
|
+
"@sitecore-content-sdk/content": "2.1.0-canary.10",
|
|
95
|
+
"@sitecore-content-sdk/core": "2.1.0-canary.10",
|
|
96
|
+
"@sitecore-content-sdk/events": "2.1.0-canary.10",
|
|
97
|
+
"@sitecore-content-sdk/react": "2.1.0-canary.10",
|
|
97
98
|
"recast": "^0.23.11",
|
|
98
99
|
"regex-parser": "^2.3.1",
|
|
99
100
|
"sync-disk-cache": "^2.1.0"
|
|
@@ -177,7 +178,7 @@
|
|
|
177
178
|
},
|
|
178
179
|
"description": "",
|
|
179
180
|
"types": "types/index.d.ts",
|
|
180
|
-
"gitHead": "
|
|
181
|
+
"gitHead": "f4f6bb8a4ac09e5d721e8a4a361c8ad980997e6c",
|
|
181
182
|
"files": [
|
|
182
183
|
"dist",
|
|
183
184
|
"types",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../src/components/Link.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAC/C,OAAiB,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAIL,SAAS,IAAI,cAAc,EAC5B,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../src/components/Link.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAC/C,OAAiB,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAIL,SAAS,IAAI,cAAc,EAC5B,MAAM,6BAA6B,CAAC;AAGrC;;GAEG;AACH,QAAA,MAAM,sBAAsB,uFAQlB,CAAC;AAEX;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG;IACvC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAOjE;;;GAGG;AACH,eAAO,MAAM,IAAI,kGAqEhB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics-adapter.d.ts","sourceRoot":"","sources":["../../../src/initialization/proxy/analytics-adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analytics-adapter.d.ts","sourceRoot":"","sources":["../../../src/initialization/proxy/analytics-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,gBAAgB,EAEjB,MAAM,+CAA+C,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD;;;GAGG;AACH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,GACrB,qBAAqB,CA+DvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,WAAW,KAAG,MAAM,GAAG,IAI3D,CAAC"}
|