@sitecore-jss/sitecore-jss-nextjs 22.2.0-canary.8 → 22.2.0-canary.80

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.
@@ -42,6 +42,10 @@ const react_1 = __importStar(require("react"));
42
42
  const prop_types_1 = __importDefault(require("prop-types"));
43
43
  const link_1 = __importDefault(require("next/link"));
44
44
  const sitecore_jss_react_1 = require("@sitecore-jss/sitecore-jss-react");
45
+ /**
46
+ * Matches relative URLs that end with a file extension.
47
+ */
48
+ const FILE_EXTENSION_MATCHER = /^\/.*\.\w+$/;
45
49
  exports.Link = (0, react_1.forwardRef)((props, ref) => {
46
50
  const { field, editable = true, children, internalLinkMatcher = /^\//g, showLinkTextWithChildrenPresent } = props, htmlLinkProps = __rest(props, ["field", "editable", "children", "internalLinkMatcher", "showLinkTextWithChildrenPresent"]);
47
51
  if (!field ||
@@ -59,8 +63,10 @@ exports.Link = (0, react_1.forwardRef)((props, ref) => {
59
63
  const isEditing = editable && (field.editable || field.metadata);
60
64
  if (href && !isEditing) {
61
65
  const text = showLinkTextWithChildrenPresent || !children ? value.text || value.href : null;
62
- // determine if a link is a route or not.
63
- if (internalLinkMatcher.test(href)) {
66
+ const isMatching = internalLinkMatcher.test(href);
67
+ const isFileUrl = FILE_EXTENSION_MATCHER.test(href);
68
+ // determine if a link is a route or not. File extensions are not routes and should not be pre-fetched.
69
+ if (isMatching && !isFileUrl) {
64
70
  return (react_1.default.createElement(link_1.default, Object.assign({ href: { pathname: href, query: querystring, hash: anchor }, key: "link", locale: false, title: value.title, target: value.target, className: value.class }, htmlLinkProps, { ref: ref }),
65
71
  text,
66
72
  children));
@@ -24,7 +24,9 @@ const sitecore_jss_react_2 = require("@sitecore-jss/sitecore-jss-react");
24
24
  const sitecore_jss_react_3 = require("@sitecore-jss/sitecore-jss-react");
25
25
  const layout_1 = require("@sitecore-jss/sitecore-jss/layout");
26
26
  exports.NextImage = (0, sitecore_jss_react_1.withFieldMetadata)((0, sitecore_jss_react_2.withEmptyFieldEditingComponent)((_a) => {
27
+ var _b;
27
28
  var { editable = true, imageParams, field, mediaUrlPrefix, fill, priority } = _a, otherProps = __rest(_a, ["editable", "imageParams", "field", "mediaUrlPrefix", "fill", "priority"]);
29
+ const sitecoreContext = react_1.default.useContext(sitecore_jss_react_1.SitecoreContextReactContext);
28
30
  // next handles src and we use a custom loader,
29
31
  // throw error if these are present
30
32
  if (otherProps.src) {
@@ -46,8 +48,11 @@ exports.NextImage = (0, sitecore_jss_react_1.withFieldMetadata)((0, sitecore_jss
46
48
  if (!img) {
47
49
  return null;
48
50
  }
51
+ // disable image optimization for Edit and Preview, but preserve original value if true
52
+ const unoptimized = otherProps.unoptimized ||
53
+ ((_b = sitecoreContext.context) === null || _b === void 0 ? void 0 : _b.pageState) !== layout_1.LayoutServicePageState.Normal;
49
54
  const attrs = Object.assign(Object.assign(Object.assign({}, img), otherProps), { fill,
50
- priority, src: media_1.mediaApi.updateImageUrl(img.src, imageParams, mediaUrlPrefix) });
55
+ priority, src: media_1.mediaApi.updateImageUrl(img.src, imageParams, mediaUrlPrefix), unoptimized });
51
56
  const imageProps = Object.assign(Object.assign({}, attrs), {
52
57
  // force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
53
58
  // this is required for Sitecore media API resizing to work properly
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EDITING_ALLOWED_ORIGINS = exports.EDITING_PASS_THROUGH_HEADERS = exports.QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = exports.QUERY_PARAM_VERCEL_PROTECTION_BYPASS = exports.QUERY_PARAM_EDITING_SECRET = void 0;
4
- exports.QUERY_PARAM_EDITING_SECRET = 'secret';
3
+ exports.EDITING_PASS_THROUGH_HEADERS = exports.QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = exports.QUERY_PARAM_VERCEL_PROTECTION_BYPASS = void 0;
5
4
  exports.QUERY_PARAM_VERCEL_PROTECTION_BYPASS = 'x-vercel-protection-bypass';
6
5
  exports.QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = 'x-vercel-set-bypass-cookie';
7
6
  /**
@@ -9,7 +8,3 @@ exports.QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = 'x-vercel-set-bypass-cookie';
9
8
  * Note these are in lowercase format to match expected `IncomingHttpHeaders`.
10
9
  */
11
10
  exports.EDITING_PASS_THROUGH_HEADERS = ['authorization', 'cookie'];
12
- /**
13
- * Default allowed origins for editing requests. This is used to enforce CORS, CSP headers.
14
- */
15
- exports.EDITING_ALLOWED_ORIGINS = ['https://pages*.cloud', 'https://pages.sitecorecloud.io'];
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.EditingConfigMiddleware = void 0;
13
- const constants_1 = require("./constants");
13
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
14
14
  const utils_1 = require("../utils/utils");
15
15
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
16
16
  const layout_1 = require("@sitecore-jss/sitecore-jss/layout");
@@ -26,8 +26,8 @@ class EditingConfigMiddleware {
26
26
  constructor(config) {
27
27
  this.config = config;
28
28
  this.handler = (_req, res) => __awaiter(this, void 0, void 0, function* () {
29
- const secret = _req.query[constants_1.QUERY_PARAM_EDITING_SECRET];
30
- if (!(0, utils_2.enforceCors)(_req, res, constants_1.EDITING_ALLOWED_ORIGINS)) {
29
+ const secret = _req.query[editing_1.QUERY_PARAM_EDITING_SECRET];
30
+ if (!(0, utils_2.enforceCors)(_req, res, editing_1.EDITING_ALLOWED_ORIGINS)) {
31
31
  sitecore_jss_1.debug.editing('invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable');
32
32
  return res.status(401).json({ message: 'Invalid origin' });
33
33
  }
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.EditingDataMiddleware = void 0;
13
13
  const editing_data_cache_1 = require("./editing-data-cache");
14
14
  const editing_data_1 = require("./editing-data");
15
- const constants_1 = require("./constants");
15
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
16
16
  const utils_1 = require("../utils/utils");
17
17
  const utils_2 = require("@sitecore-jss/sitecore-jss/utils");
18
18
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
@@ -28,9 +28,9 @@ class EditingDataMiddleware {
28
28
  var _a, _b;
29
29
  this.handler = (req, res) => __awaiter(this, void 0, void 0, function* () {
30
30
  const { method, query, body } = req;
31
- const secret = query[constants_1.QUERY_PARAM_EDITING_SECRET];
31
+ const secret = query[editing_1.QUERY_PARAM_EDITING_SECRET];
32
32
  const key = query[this.queryParamKey];
33
- if (!(0, utils_2.enforceCors)(req, res, constants_1.EDITING_ALLOWED_ORIGINS)) {
33
+ if (!(0, utils_2.enforceCors)(req, res, editing_1.EDITING_ALLOWED_ORIGINS)) {
34
34
  sitecore_jss_1.debug.editing('invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable');
35
35
  return res.status(401).json({ message: 'Invalid origin' });
36
36
  }
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.editingDataService = exports.ServerlessEditingDataService = exports.BasicEditingDataService = exports.generateKey = void 0;
13
- const constants_1 = require("./constants");
13
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
14
14
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
15
15
  const editing_data_cache_1 = require("./editing-data-cache");
16
16
  const utils_1 = require("../utils/utils");
@@ -137,7 +137,7 @@ class ServerlessEditingDataService {
137
137
  // http://localhost:3000/api/editing/data/52961eea-bafd-5287-a532-a72e36bd8a36-qkb4e3fv5x?secret=1234secret
138
138
  const apiRoute = (_a = this.apiRoute) === null || _a === void 0 ? void 0 : _a.replace('[key]', key);
139
139
  const url = new URL(apiRoute, serverUrl);
140
- url.searchParams.append(constants_1.QUERY_PARAM_EDITING_SECRET, (0, utils_1.getJssEditingSecret)());
140
+ url.searchParams.append(editing_1.QUERY_PARAM_EDITING_SECRET, (0, utils_1.getJssEditingSecret)());
141
141
  if (params) {
142
142
  for (const key in params) {
143
143
  if ({}.hasOwnProperty.call(params, key)) {
@@ -13,8 +13,8 @@ exports.EditingRenderMiddleware = exports.MetadataHandler = exports.isEditingMet
13
13
  const constants_1 = require("next/constants");
14
14
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
15
15
  const layout_1 = require("@sitecore-jss/sitecore-jss/layout");
16
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
16
17
  const editing_data_service_1 = require("./editing-data-service");
17
- const constants_2 = require("./constants");
18
18
  const utils_1 = require("../utils/utils");
19
19
  const render_middleware_1 = require("./render-middleware");
20
20
  const utils_2 = require("@sitecore-jss/sitecore-jss/utils");
@@ -221,10 +221,10 @@ class MetadataHandler {
221
221
  version: query.sc_version,
222
222
  editMode: layout_1.EditMode.Metadata,
223
223
  pageState: query.mode,
224
+ layoutKind: query.sc_layoutKind,
224
225
  },
225
226
  // Cache the preview data for 3 seconds to ensure the page is rendered with the correct preview data not the cached one
226
227
  {
227
- path: query.route,
228
228
  maxAge: 3,
229
229
  });
230
230
  // Cookies with the SameSite=Lax policy set by Next.js setPreviewData function causes CORS issue
@@ -265,7 +265,7 @@ class MetadataHandler {
265
265
  * @returns Content-Security-Policy header value
266
266
  */
267
267
  getSCPHeader() {
268
- return `frame-ancestors 'self' ${[(0, utils_2.getAllowedOriginsFromEnv)(), ...constants_2.EDITING_ALLOWED_ORIGINS].join(' ')}`;
268
+ return `frame-ancestors 'self' ${[(0, utils_2.getAllowedOriginsFromEnv)(), ...editing_1.EDITING_ALLOWED_ORIGINS].join(' ')}`;
269
269
  }
270
270
  }
271
271
  exports.MetadataHandler = MetadataHandler;
@@ -289,14 +289,14 @@ class EditingRenderMiddleware extends render_middleware_1.RenderMiddlewareBase {
289
289
  headers,
290
290
  body,
291
291
  });
292
- if (!(0, utils_2.enforceCors)(req, res, constants_2.EDITING_ALLOWED_ORIGINS)) {
292
+ if (!(0, utils_2.enforceCors)(req, res, editing_1.EDITING_ALLOWED_ORIGINS)) {
293
293
  sitecore_jss_1.debug.editing('invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable');
294
294
  return res.status(401).json({
295
295
  html: `<html><body>Requests from origin ${(_a = req.headers) === null || _a === void 0 ? void 0 : _a.origin} not allowed</body></html>`,
296
296
  });
297
297
  }
298
298
  // Validate secret
299
- const secret = (_b = query[constants_2.QUERY_PARAM_EDITING_SECRET]) !== null && _b !== void 0 ? _b : body === null || body === void 0 ? void 0 : body.jssEditingSecret;
299
+ const secret = (_b = query[editing_1.QUERY_PARAM_EDITING_SECRET]) !== null && _b !== void 0 ? _b : body === null || body === void 0 ? void 0 : body.jssEditingSecret;
300
300
  if (secret !== (0, utils_1.getJssEditingSecret)()) {
301
301
  sitecore_jss_1.debug.editing('invalid editing secret - sent "%s" expected "%s"', secret, (0, utils_1.getJssEditingSecret)());
302
302
  return res.status(401).json({
@@ -11,7 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FEAASRenderMiddleware = void 0;
13
13
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
14
- const constants_1 = require("./constants");
14
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
15
15
  const utils_1 = require("../utils/utils");
16
16
  const render_middleware_1 = require("./render-middleware");
17
17
  const utils_2 = require("@sitecore-jss/sitecore-jss/utils");
@@ -37,7 +37,7 @@ class FEAASRenderMiddleware extends render_middleware_1.RenderMiddlewareBase {
37
37
  query,
38
38
  headers,
39
39
  });
40
- if (!(0, utils_2.enforceCors)(req, res, constants_1.EDITING_ALLOWED_ORIGINS)) {
40
+ if (!(0, utils_2.enforceCors)(req, res, editing_1.EDITING_ALLOWED_ORIGINS)) {
41
41
  sitecore_jss_1.debug.editing('invalid origin host - set allowed origins in JSS_ALLOWED_ORIGINS environment variable');
42
42
  return res
43
43
  .status(401)
@@ -49,7 +49,7 @@ class FEAASRenderMiddleware extends render_middleware_1.RenderMiddlewareBase {
49
49
  return res.status(405).send(`<html><body>Invalid request method '${method}'</body></html>`);
50
50
  }
51
51
  // Validate secret
52
- const secret = query[constants_1.QUERY_PARAM_EDITING_SECRET];
52
+ const secret = query[editing_1.QUERY_PARAM_EDITING_SECRET];
53
53
  if (secret !== (0, utils_1.getJssEditingSecret)()) {
54
54
  sitecore_jss_1.debug.editing('invalid editing secret - sent "%s" expected "%s"', secret, (0, utils_1.getJssEditingSecret)());
55
55
  return res.status(401).send('<html><body>Missing or invalid secret</body></html>');
package/dist/cjs/index.js CHANGED
@@ -23,14 +23,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.FEaaSComponent = exports.EditFrame = exports.DateField = exports.Text = exports.Image = exports.Context = exports.ComponentBuilder = exports.BYOCWrapper = exports.FEaaSWrapper = exports.NextImage = exports.Placeholder = exports.RichText = exports.Link = exports.useComponentProps = exports.ComponentPropsContext = exports.ComponentPropsReactContext = exports.normalizeSiteRewrite = exports.getSiteRewriteData = exports.getSiteRewrite = exports.GraphQLSiteInfoService = exports.SiteResolver = exports.GraphQLRobotsService = exports.GraphQLErrorPagesService = exports.GraphQLSitemapXmlService = exports.MultisiteGraphQLSitemapService = exports.GraphQLSitemapService = exports.DisconnectedSitemapService = exports.ComponentPropsService = exports.CdpHelper = exports.normalizePersonalizedRewrite = exports.getGroomedVariantIds = exports.getPersonalizedRewriteData = exports.getPersonalizedRewrite = exports.personalizeLayout = exports.RestDictionaryService = exports.GraphQLDictionaryService = exports.trackingApi = exports.mediaApi = exports.EditMode = exports.getContentStylesheetLink = exports.getFieldValue = exports.getChildPlaceholder = exports.RestLayoutService = exports.GraphQLLayoutService = exports.LayoutServicePageState = exports.debug = exports.enableDebug = exports.NativeDataFetcher = exports.AxiosDataFetcher = exports.constants = void 0;
27
- exports.EditingScripts = exports.withEmptyFieldEditingComponent = exports.withFieldMetadata = exports.withDatasourceCheck = exports.withPlaceholder = exports.withEditorChromes = exports.useSitecoreContext = exports.withSitecoreContext = exports.SitecoreContextReactContext = exports.SitecoreContext = exports.VisitorIdentification = exports.DefaultEmptyFieldEditingComponentText = exports.DefaultEmptyFieldEditingComponentImage = exports.File = exports.getComponentLibraryStylesheetLinks = exports.BYOCComponent = exports.fetchFEaaSComponentServerProps = void 0;
26
+ exports.EditFrame = exports.DateField = exports.Text = exports.Image = exports.Context = exports.ComponentBuilder = exports.BYOCWrapper = exports.FEaaSWrapper = exports.NextImage = exports.Placeholder = exports.RichText = exports.Link = exports.useComponentProps = exports.ComponentPropsContext = exports.ComponentPropsReactContext = exports.normalizeSiteRewrite = exports.getSiteRewriteData = exports.getSiteRewrite = exports.GraphQLSiteInfoService = exports.SiteResolver = exports.GraphQLRobotsService = exports.GraphQLErrorPagesService = exports.GraphQLSitemapXmlService = exports.MultisiteGraphQLSitemapService = exports.GraphQLSitemapService = exports.DisconnectedSitemapService = exports.ComponentPropsService = exports.CdpHelper = exports.normalizePersonalizedRewrite = exports.getGroomedVariantIds = exports.getPersonalizedRewriteData = exports.getPersonalizedRewrite = exports.personalizeLayout = exports.RestDictionaryService = exports.GraphQLDictionaryService = exports.trackingApi = exports.mediaApi = exports.EditMode = exports.getContentStylesheetLink = exports.getFieldValue = exports.getChildPlaceholder = exports.RestLayoutService = exports.GraphQLLayoutService = exports.LayoutServicePageState = exports.MemoryCacheClient = exports.debug = exports.enableDebug = exports.NativeDataFetcher = exports.AxiosDataFetcher = exports.constants = void 0;
27
+ exports.EditingScripts = exports.withEmptyFieldEditingComponent = exports.withFieldMetadata = exports.withDatasourceCheck = exports.withPlaceholder = exports.withEditorChromes = exports.useSitecoreContext = exports.withSitecoreContext = exports.SitecoreContextReactContext = exports.SitecoreContext = exports.VisitorIdentification = exports.DefaultEmptyFieldEditingComponentText = exports.DefaultEmptyFieldEditingComponentImage = exports.File = exports.getComponentLibraryStylesheetLinks = exports.BYOCComponent = exports.fetchFEaaSComponentServerProps = exports.FEaaSComponent = void 0;
28
28
  var sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
29
29
  Object.defineProperty(exports, "constants", { enumerable: true, get: function () { return sitecore_jss_1.constants; } });
30
30
  Object.defineProperty(exports, "AxiosDataFetcher", { enumerable: true, get: function () { return sitecore_jss_1.AxiosDataFetcher; } });
31
31
  Object.defineProperty(exports, "NativeDataFetcher", { enumerable: true, get: function () { return sitecore_jss_1.NativeDataFetcher; } });
32
32
  Object.defineProperty(exports, "enableDebug", { enumerable: true, get: function () { return sitecore_jss_1.enableDebug; } });
33
33
  Object.defineProperty(exports, "debug", { enumerable: true, get: function () { return sitecore_jss_1.debug; } });
34
+ Object.defineProperty(exports, "MemoryCacheClient", { enumerable: true, get: function () { return sitecore_jss_1.MemoryCacheClient; } });
34
35
  var layout_1 = require("@sitecore-jss/sitecore-jss/layout");
35
36
  Object.defineProperty(exports, "LayoutServicePageState", { enumerable: true, get: function () { return layout_1.LayoutServicePageState; } });
36
37
  Object.defineProperty(exports, "GraphQLLayoutService", { enumerable: true, get: function () { return layout_1.GraphQLLayoutService; } });
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MultisiteMiddleware = exports.PersonalizeMiddleware = exports.RedirectsMiddleware = exports.debug = void 0;
3
+ exports.MultisiteMiddleware = exports.PersonalizeMiddleware = exports.RedirectsMiddleware = exports.MiddlewareBase = exports.debug = void 0;
4
4
  var sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
5
5
  Object.defineProperty(exports, "debug", { enumerable: true, get: function () { return sitecore_jss_1.debug; } });
6
+ var middleware_1 = require("./middleware");
7
+ Object.defineProperty(exports, "MiddlewareBase", { enumerable: true, get: function () { return middleware_1.MiddlewareBase; } });
6
8
  var redirects_middleware_1 = require("./redirects-middleware");
7
9
  Object.defineProperty(exports, "RedirectsMiddleware", { enumerable: true, get: function () { return redirects_middleware_1.RedirectsMiddleware; } });
8
10
  var personalize_middleware_1 = require("./personalize-middleware");
@@ -13,10 +13,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RedirectsMiddleware = void 0;
16
- const regex_parser_1 = __importDefault(require("regex-parser"));
17
- const server_1 = require("next/server");
18
- const site_1 = require("@sitecore-jss/sitecore-jss/site");
19
16
  const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
17
+ const site_1 = require("@sitecore-jss/sitecore-jss/site");
18
+ const utils_1 = require("@sitecore-jss/sitecore-jss/utils");
19
+ const server_1 = require("next/server");
20
+ const regex_parser_1 = __importDefault(require("regex-parser"));
20
21
  const middleware_1 = require("./middleware");
21
22
  const REGEXP_CONTEXT_SITE_LANG = new RegExp(/\$siteLang/, 'i');
22
23
  const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
@@ -43,6 +44,7 @@ class RedirectsMiddleware extends middleware_1.MiddlewareBase {
43
44
  hostname,
44
45
  });
45
46
  const createResponse = () => __awaiter(this, void 0, void 0, function* () {
47
+ var _a;
46
48
  if (this.config.disabled && this.config.disabled(req, res || server_1.NextResponse.next())) {
47
49
  sitecore_jss_1.debug.redirects('skipped (redirects middleware is disabled)');
48
50
  return res || server_1.NextResponse.next();
@@ -63,48 +65,50 @@ class RedirectsMiddleware extends middleware_1.MiddlewareBase {
63
65
  !(REGEXP_ABSOLUTE_URL.test(existsRedirect.target) &&
64
66
  existsRedirect.target.includes(hostname))) {
65
67
  existsRedirect.target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, site.language);
68
+ req.nextUrl.locale = site.language;
66
69
  }
67
- const url = req.nextUrl.clone();
70
+ const url = this.normalizeUrl(req.nextUrl.clone());
68
71
  if (REGEXP_ABSOLUTE_URL.test(existsRedirect.target)) {
69
72
  url.href = existsRedirect.target;
70
73
  }
71
74
  else {
72
- const source = `${url.pathname}${url.search}`;
73
- url.search = existsRedirect.isQueryStringPreserved ? url.search : '';
75
+ const source = `${url.pathname.replace(/\/*$/gi, '')}${existsRedirect.matchedQueryString}`;
74
76
  const urlFirstPart = existsRedirect.target.split('/')[1];
75
77
  if (this.locales.includes(urlFirstPart)) {
76
- url.locale = urlFirstPart;
78
+ req.nextUrl.locale = urlFirstPart;
77
79
  existsRedirect.target = existsRedirect.target.replace(`/${urlFirstPart}`, '');
78
80
  }
79
81
  const target = source
80
82
  .replace((0, regex_parser_1.default)(existsRedirect.pattern), existsRedirect.target)
81
83
  .replace(/^\/\//, '/')
82
84
  .split('?');
83
- url.pathname = target[0];
84
- if (target[1]) {
85
- const newParams = new URLSearchParams(target[1]);
86
- for (const [key, val] of newParams.entries()) {
87
- url.searchParams.append(key, val);
88
- }
85
+ if (url.search && existsRedirect.isQueryStringPreserved) {
86
+ const targetQueryString = (_a = target[1]) !== null && _a !== void 0 ? _a : '';
87
+ url.search = '?' + new URLSearchParams(`${url.search}&${targetQueryString}`).toString();
89
88
  }
89
+ else if (target[1]) {
90
+ url.search = '?' + target[1];
91
+ }
92
+ else {
93
+ url.search = '';
94
+ }
95
+ const prepareNewURL = new URL(`${target[0]}${url.search}`, url.origin);
96
+ url.href = prepareNewURL.href;
97
+ url.pathname = prepareNewURL.pathname;
98
+ url.search = prepareNewURL.search;
99
+ url.locale = req.nextUrl.locale;
90
100
  }
91
- const redirectUrl = decodeURIComponent(url.href);
92
101
  /** return Response redirect with http code of redirect type **/
93
102
  switch (existsRedirect.redirectType) {
94
- case site_1.REDIRECT_TYPE_301:
95
- return server_1.NextResponse.redirect(redirectUrl, {
96
- status: 301,
97
- statusText: 'Moved Permanently',
98
- headers: res === null || res === void 0 ? void 0 : res.headers,
99
- });
100
- case site_1.REDIRECT_TYPE_302:
101
- return server_1.NextResponse.redirect(redirectUrl, {
102
- status: 302,
103
- statusText: 'Found',
104
- headers: res === null || res === void 0 ? void 0 : res.headers,
105
- });
106
- case site_1.REDIRECT_TYPE_SERVER_TRANSFER:
107
- return server_1.NextResponse.rewrite(redirectUrl, res);
103
+ case site_1.REDIRECT_TYPE_301: {
104
+ return this.createRedirectResponse(url, res, 301, 'Moved Permanently');
105
+ }
106
+ case site_1.REDIRECT_TYPE_302: {
107
+ return this.createRedirectResponse(url, res, 302, 'Found');
108
+ }
109
+ case site_1.REDIRECT_TYPE_SERVER_TRANSFER: {
110
+ return this.rewrite(url.href, req, res || server_1.NextResponse.next());
111
+ }
108
112
  default:
109
113
  return res || server_1.NextResponse.next();
110
114
  }
@@ -149,29 +153,140 @@ class RedirectsMiddleware extends middleware_1.MiddlewareBase {
149
153
  getExistsRedirect(req, siteName) {
150
154
  return __awaiter(this, void 0, void 0, function* () {
151
155
  const redirects = yield this.redirectsService.fetchRedirects(siteName);
152
- const tragetURL = req.nextUrl.pathname;
153
- const targetQS = req.nextUrl.search || '';
156
+ const { pathname: targetURL, search: targetQS = '', locale } = this.normalizeUrl(req.nextUrl.clone());
154
157
  const language = this.getLanguage(req);
155
158
  const modifyRedirects = structuredClone(redirects);
156
159
  return modifyRedirects.length
157
160
  ? modifyRedirects.find((redirect) => {
161
+ // Modify the redirect pattern to ignore the language prefix in the path
158
162
  redirect.pattern = redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), '');
163
+ // Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
159
164
  redirect.pattern = `/^\/${redirect.pattern
160
- .replace(/^\/|\/$/g, '')
161
- .replace(/^\^\/|\/\$$/g, '')
162
- .replace(/^\^|\$$/g, '')
163
- .replace(/(?<!\\)\?/g, '\\?')
164
- .replace(/\$\/gi$/g, '')}[\/]?$/gi`;
165
- return (((0, regex_parser_1.default)(redirect.pattern).test(tragetURL) ||
166
- (0, regex_parser_1.default)(redirect.pattern).test(`${tragetURL}${targetQS}`) ||
167
- (0, regex_parser_1.default)(redirect.pattern).test(`/${req.nextUrl.locale}${tragetURL}`) ||
168
- (0, regex_parser_1.default)(redirect.pattern).test(`/${req.nextUrl.locale}${tragetURL}${targetQS}`)) &&
169
- (redirect.locale
170
- ? redirect.locale.toLowerCase() === req.nextUrl.locale.toLowerCase()
171
- : true));
165
+ .replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
166
+ .replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
167
+ .replace(/^\^|\$$/g, '') // Further cleans up anchors
168
+ .replace(/(?<!\\)\?/g, '\\?') // Escapes question marks in the pattern
169
+ .replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
170
+ /**
171
+ * This line checks whether the current URL query string (and all its possible permutations)
172
+ * matches the redirect pattern.
173
+ *
174
+ * Query parameters in URLs can come in different orders, but logically they represent the
175
+ * same information (e.g., "key1=value1&key2=value2" is the same as "key2=value2&key1=value1").
176
+ * To account for this, the method `isPermutedQueryMatch` generates all possible permutations
177
+ * of the query parameters and checks if any of those permutations match the regex pattern for the redirect.
178
+ *
179
+ * NOTE: This fix is specifically implemented for Netlify, where query parameters are sometimes
180
+ * automatically sorted, which can cause issues with matching redirects if the order of query
181
+ * parameters is important. By checking every possible permutation, we ensure that redirects
182
+ * work correctly on Netlify despite this behavior.
183
+ *
184
+ * It passes several pieces of information to the function:
185
+ * 1. `pathname`: The normalized URL path without query parameters (e.g., '/about').
186
+ * 2. `queryString`: The current query string from the URL, which will be permuted and matched (e.g., '?key1=value1&key2=value2').
187
+ * 3. `pattern`: The regex pattern for the redirect that we are trying to match against the URL (e.g., '/about?key1=value1').
188
+ * 4. `locale`: The locale part of the URL (if any), which helps support multilingual URLs.
189
+ *
190
+ * If one of the permutations of the query string matches the redirect pattern, the function
191
+ * returns the matched query string, which is stored in `matchedQueryString`. If no match is found,
192
+ * it returns `undefined`. The `matchedQueryString` is later used to indicate whether the query
193
+ * string contributed to a successful redirect match.
194
+ */
195
+ const matchedQueryString = this.isPermutedQueryMatch({
196
+ pathname: targetURL,
197
+ queryString: targetQS,
198
+ pattern: redirect.pattern,
199
+ locale,
200
+ });
201
+ // Save the matched query string (if found) into the redirect object
202
+ redirect.matchedQueryString = matchedQueryString || '';
203
+ // Return the redirect if the URL path or any query string permutation matches the pattern
204
+ return (((0, regex_parser_1.default)(redirect.pattern).test(targetURL) ||
205
+ (0, regex_parser_1.default)(redirect.pattern).test(`/${req.nextUrl.locale}${targetURL}`) ||
206
+ matchedQueryString) &&
207
+ (redirect.locale ? redirect.locale.toLowerCase() === locale.toLowerCase() : true));
172
208
  })
173
209
  : undefined;
174
210
  });
175
211
  }
212
+ /**
213
+ * When a user clicks on a link generated by the Link component from next/link,
214
+ * Next.js adds special parameters in the route called path.
215
+ * This method removes these special parameters.
216
+ * @param {NextURL} url
217
+ * @returns {string} normalize url
218
+ */
219
+ normalizeUrl(url) {
220
+ if (!url.search) {
221
+ return url;
222
+ }
223
+ /**
224
+ * Prepare special parameters for exclusion.
225
+ */
226
+ const splittedPathname = url.pathname
227
+ .split('/')
228
+ .filter((route) => route)
229
+ .map((route) => `path=${route}`);
230
+ /**
231
+ * Remove special parameters(Next.JS)
232
+ * Example: /about/contact/us
233
+ * When a user clicks on this link, Next.js should generate a link for the middleware, formatted like this:
234
+ * http://host/about/contact/us?path=about&path=contact&path=us
235
+ */
236
+ const newQueryString = url.search
237
+ .replace(/^\?/, '')
238
+ .split('&')
239
+ .filter((param) => {
240
+ if (!splittedPathname.includes(param)) {
241
+ return param;
242
+ }
243
+ return false;
244
+ })
245
+ .join('&');
246
+ const newUrl = new URL(`${url.pathname}?${newQueryString}`, url.origin);
247
+ url.search = newUrl.search;
248
+ url.pathname = newUrl.pathname;
249
+ url.href = newUrl.href;
250
+ return url;
251
+ }
252
+ /**
253
+ * Helper function to create a redirect response and remove the x-middleware-next header.
254
+ * @param {NextURL} url The URL to redirect to.
255
+ * @param {Response} res The response object.
256
+ * @param {number} status The HTTP status code of the redirect.
257
+ * @param {string} statusText The status text of the redirect.
258
+ * @returns {NextResponse<unknown>} The redirect response.
259
+ */
260
+ createRedirectResponse(url, res, status, statusText) {
261
+ const redirect = server_1.NextResponse.redirect(url, {
262
+ status,
263
+ statusText,
264
+ headers: res === null || res === void 0 ? void 0 : res.headers,
265
+ });
266
+ if (res === null || res === void 0 ? void 0 : res.headers) {
267
+ redirect.headers.delete('x-middleware-next');
268
+ redirect.headers.delete('x-middleware-rewrite');
269
+ }
270
+ return redirect;
271
+ }
272
+ /**
273
+ * Checks if the current URL query matches the provided pattern, considering all permutations of query parameters.
274
+ * It constructs all possible query parameter permutations and tests them against the pattern.
275
+ * @param {Object} params - The parameters for the URL match.
276
+ * @param {string} params.pathname - The current URL pathname.
277
+ * @param {string} params.queryString - The current URL query string.
278
+ * @param {string} params.pattern - The regex pattern to test the constructed URLs against.
279
+ * @param {string} [params.locale] - The locale prefix to include in the URL if present.
280
+ * @returns {string | undefined} - return query string if any of the query permutations match the provided pattern, undefined otherwise.
281
+ */
282
+ isPermutedQueryMatch({ pathname, queryString, pattern, locale, }) {
283
+ const paramsArray = Array.from(new URLSearchParams(queryString).entries());
284
+ const listOfPermuted = (0, utils_1.getPermutations)(paramsArray).map((permutation) => '?' + permutation.map(([key, value]) => `${key}=${value}`).join('&'));
285
+ const normalizedPath = pathname.replace(/\/*$/gi, '');
286
+ return listOfPermuted.find((query) => [
287
+ (0, regex_parser_1.default)(pattern).test(`${normalizedPath}${query}`),
288
+ (0, regex_parser_1.default)(pattern).test(`/${locale}${normalizedPath}${query}`),
289
+ ].some(Boolean));
290
+ }
176
291
  }
177
292
  exports.RedirectsMiddleware = RedirectsMiddleware;
@@ -13,6 +13,10 @@ import React, { forwardRef } from 'react';
13
13
  import PropTypes from 'prop-types';
14
14
  import NextLink from 'next/link';
15
15
  import { Link as ReactLink, LinkPropTypes, } from '@sitecore-jss/sitecore-jss-react';
16
+ /**
17
+ * Matches relative URLs that end with a file extension.
18
+ */
19
+ const FILE_EXTENSION_MATCHER = /^\/.*\.\w+$/;
16
20
  export const Link = forwardRef((props, ref) => {
17
21
  const { field, editable = true, children, internalLinkMatcher = /^\//g, showLinkTextWithChildrenPresent } = props, htmlLinkProps = __rest(props, ["field", "editable", "children", "internalLinkMatcher", "showLinkTextWithChildrenPresent"]);
18
22
  if (!field ||
@@ -30,8 +34,10 @@ export const Link = forwardRef((props, ref) => {
30
34
  const isEditing = editable && (field.editable || field.metadata);
31
35
  if (href && !isEditing) {
32
36
  const text = showLinkTextWithChildrenPresent || !children ? value.text || value.href : null;
33
- // determine if a link is a route or not.
34
- if (internalLinkMatcher.test(href)) {
37
+ const isMatching = internalLinkMatcher.test(href);
38
+ const isFileUrl = FILE_EXTENSION_MATCHER.test(href);
39
+ // determine if a link is a route or not. File extensions are not routes and should not be pre-fetched.
40
+ if (isMatching && !isFileUrl) {
35
41
  return (React.createElement(NextLink, Object.assign({ href: { pathname: href, query: querystring, hash: anchor }, key: "link", locale: false, title: value.title, target: value.target, className: value.class }, htmlLinkProps, { ref: ref }),
36
42
  text,
37
43
  children));
@@ -12,13 +12,15 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { mediaApi } from '@sitecore-jss/sitecore-jss/media';
13
13
  import PropTypes from 'prop-types';
14
14
  import React from 'react';
15
- import { getEEMarkup, withFieldMetadata, } from '@sitecore-jss/sitecore-jss-react';
15
+ import { getEEMarkup, withFieldMetadata, SitecoreContextReactContext, } from '@sitecore-jss/sitecore-jss-react';
16
16
  import Image from 'next/image';
17
17
  import { withEmptyFieldEditingComponent } from '@sitecore-jss/sitecore-jss-react';
18
18
  import { DefaultEmptyFieldEditingComponentImage } from '@sitecore-jss/sitecore-jss-react';
19
- import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout';
19
+ import { isFieldValueEmpty, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout';
20
20
  export const NextImage = withFieldMetadata(withEmptyFieldEditingComponent((_a) => {
21
+ var _b;
21
22
  var { editable = true, imageParams, field, mediaUrlPrefix, fill, priority } = _a, otherProps = __rest(_a, ["editable", "imageParams", "field", "mediaUrlPrefix", "fill", "priority"]);
23
+ const sitecoreContext = React.useContext(SitecoreContextReactContext);
22
24
  // next handles src and we use a custom loader,
23
25
  // throw error if these are present
24
26
  if (otherProps.src) {
@@ -40,8 +42,11 @@ export const NextImage = withFieldMetadata(withEmptyFieldEditingComponent((_a) =
40
42
  if (!img) {
41
43
  return null;
42
44
  }
45
+ // disable image optimization for Edit and Preview, but preserve original value if true
46
+ const unoptimized = otherProps.unoptimized ||
47
+ ((_b = sitecoreContext.context) === null || _b === void 0 ? void 0 : _b.pageState) !== LayoutServicePageState.Normal;
43
48
  const attrs = Object.assign(Object.assign(Object.assign({}, img), otherProps), { fill,
44
- priority, src: mediaApi.updateImageUrl(img.src, imageParams, mediaUrlPrefix) });
49
+ priority, src: mediaApi.updateImageUrl(img.src, imageParams, mediaUrlPrefix), unoptimized });
45
50
  const imageProps = Object.assign(Object.assign({}, attrs), {
46
51
  // force replace /media with /jssmedia in src since we _know_ we will be adding a 'mw' query string parameter
47
52
  // this is required for Sitecore media API resizing to work properly
@@ -1,4 +1,3 @@
1
- export const QUERY_PARAM_EDITING_SECRET = 'secret';
2
1
  export const QUERY_PARAM_VERCEL_PROTECTION_BYPASS = 'x-vercel-protection-bypass';
3
2
  export const QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = 'x-vercel-set-bypass-cookie';
4
3
  /**
@@ -6,7 +5,3 @@ export const QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = 'x-vercel-set-bypass-cookie'
6
5
  * Note these are in lowercase format to match expected `IncomingHttpHeaders`.
7
6
  */
8
7
  export const EDITING_PASS_THROUGH_HEADERS = ['authorization', 'cookie'];
9
- /**
10
- * Default allowed origins for editing requests. This is used to enforce CORS, CSP headers.
11
- */
12
- export const EDITING_ALLOWED_ORIGINS = ['https://pages*.cloud', 'https://pages.sitecorecloud.io'];
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants';
10
+ import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET, } from '@sitecore-jss/sitecore-jss/editing';
11
11
  import { getJssEditingSecret } from '../utils/utils';
12
12
  import { debug } from '@sitecore-jss/sitecore-jss';
13
13
  import { EditMode } from '@sitecore-jss/sitecore-jss/layout';
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { editingDataDiskCache } from './editing-data-cache';
11
11
  import { isEditingData } from './editing-data';
12
- import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants';
12
+ import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET, } from '@sitecore-jss/sitecore-jss/editing';
13
13
  import { getJssEditingSecret } from '../utils/utils';
14
14
  import { enforceCors } from '@sitecore-jss/sitecore-jss/utils';
15
15
  import { debug } from '@sitecore-jss/sitecore-jss';
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { QUERY_PARAM_EDITING_SECRET } from './constants';
10
+ import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-jss/sitecore-jss/editing';
11
11
  import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss';
12
12
  import { editingDataDiskCache } from './editing-data-cache';
13
13
  import { getJssEditingSecret } from '../utils/utils';
@@ -10,8 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { STATIC_PROPS_ID, SERVER_PROPS_ID } from 'next/constants';
11
11
  import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss';
12
12
  import { EditMode } from '@sitecore-jss/sitecore-jss/layout';
13
+ import { QUERY_PARAM_EDITING_SECRET, EDITING_ALLOWED_ORIGINS, } from '@sitecore-jss/sitecore-jss/editing';
13
14
  import { editingDataService } from './editing-data-service';
14
- import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants';
15
15
  import { getJssEditingSecret } from '../utils/utils';
16
16
  import { RenderMiddlewareBase } from './render-middleware';
17
17
  import { enforceCors, getAllowedOriginsFromEnv } from '@sitecore-jss/sitecore-jss/utils';
@@ -216,10 +216,10 @@ export class MetadataHandler {
216
216
  version: query.sc_version,
217
217
  editMode: EditMode.Metadata,
218
218
  pageState: query.mode,
219
+ layoutKind: query.sc_layoutKind,
219
220
  },
220
221
  // Cache the preview data for 3 seconds to ensure the page is rendered with the correct preview data not the cached one
221
222
  {
222
- path: query.route,
223
223
  maxAge: 3,
224
224
  });
225
225
  // Cookies with the SameSite=Lax policy set by Next.js setPreviewData function causes CORS issue
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { debug } from '@sitecore-jss/sitecore-jss';
11
- import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET } from './constants';
11
+ import { EDITING_ALLOWED_ORIGINS, QUERY_PARAM_EDITING_SECRET, } from '@sitecore-jss/sitecore-jss/editing';
12
12
  import { getJssEditingSecret } from '../utils/utils';
13
13
  import { RenderMiddlewareBase } from './render-middleware';
14
14
  import { enforceCors } from '@sitecore-jss/sitecore-jss/utils';
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { constants, AxiosDataFetcher, NativeDataFetcher, enableDebug, debug, } from '@sitecore-jss/sitecore-jss';
1
+ export { constants, AxiosDataFetcher, NativeDataFetcher, enableDebug, debug, MemoryCacheClient, } from '@sitecore-jss/sitecore-jss';
2
2
  export { LayoutServicePageState, GraphQLLayoutService, RestLayoutService, getChildPlaceholder, getFieldValue, getContentStylesheetLink, EditMode, } from '@sitecore-jss/sitecore-jss/layout';
3
3
  export { mediaApi } from '@sitecore-jss/sitecore-jss/media';
4
4
  export { trackingApi, } from '@sitecore-jss/sitecore-jss/tracking';
@@ -1,4 +1,5 @@
1
1
  export { debug } from '@sitecore-jss/sitecore-jss';
2
+ export { MiddlewareBase } from './middleware';
2
3
  export { RedirectsMiddleware } from './redirects-middleware';
3
4
  export { PersonalizeMiddleware } from './personalize-middleware';
4
5
  export { MultisiteMiddleware } from './multisite-middleware';
@@ -7,10 +7,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import regexParser from 'regex-parser';
11
- import { NextResponse } from 'next/server';
12
- import { GraphQLRedirectsService, REDIRECT_TYPE_301, REDIRECT_TYPE_302, REDIRECT_TYPE_SERVER_TRANSFER, } from '@sitecore-jss/sitecore-jss/site';
13
10
  import { debug } from '@sitecore-jss/sitecore-jss';
11
+ import { GraphQLRedirectsService, REDIRECT_TYPE_301, REDIRECT_TYPE_302, REDIRECT_TYPE_SERVER_TRANSFER, } from '@sitecore-jss/sitecore-jss/site';
12
+ import { getPermutations } from '@sitecore-jss/sitecore-jss/utils';
13
+ import { NextResponse } from 'next/server';
14
+ import regexParser from 'regex-parser';
14
15
  import { MiddlewareBase } from './middleware';
15
16
  const REGEXP_CONTEXT_SITE_LANG = new RegExp(/\$siteLang/, 'i');
16
17
  const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
@@ -37,6 +38,7 @@ export class RedirectsMiddleware extends MiddlewareBase {
37
38
  hostname,
38
39
  });
39
40
  const createResponse = () => __awaiter(this, void 0, void 0, function* () {
41
+ var _a;
40
42
  if (this.config.disabled && this.config.disabled(req, res || NextResponse.next())) {
41
43
  debug.redirects('skipped (redirects middleware is disabled)');
42
44
  return res || NextResponse.next();
@@ -57,48 +59,50 @@ export class RedirectsMiddleware extends MiddlewareBase {
57
59
  !(REGEXP_ABSOLUTE_URL.test(existsRedirect.target) &&
58
60
  existsRedirect.target.includes(hostname))) {
59
61
  existsRedirect.target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, site.language);
62
+ req.nextUrl.locale = site.language;
60
63
  }
61
- const url = req.nextUrl.clone();
64
+ const url = this.normalizeUrl(req.nextUrl.clone());
62
65
  if (REGEXP_ABSOLUTE_URL.test(existsRedirect.target)) {
63
66
  url.href = existsRedirect.target;
64
67
  }
65
68
  else {
66
- const source = `${url.pathname}${url.search}`;
67
- url.search = existsRedirect.isQueryStringPreserved ? url.search : '';
69
+ const source = `${url.pathname.replace(/\/*$/gi, '')}${existsRedirect.matchedQueryString}`;
68
70
  const urlFirstPart = existsRedirect.target.split('/')[1];
69
71
  if (this.locales.includes(urlFirstPart)) {
70
- url.locale = urlFirstPart;
72
+ req.nextUrl.locale = urlFirstPart;
71
73
  existsRedirect.target = existsRedirect.target.replace(`/${urlFirstPart}`, '');
72
74
  }
73
75
  const target = source
74
76
  .replace(regexParser(existsRedirect.pattern), existsRedirect.target)
75
77
  .replace(/^\/\//, '/')
76
78
  .split('?');
77
- url.pathname = target[0];
78
- if (target[1]) {
79
- const newParams = new URLSearchParams(target[1]);
80
- for (const [key, val] of newParams.entries()) {
81
- url.searchParams.append(key, val);
82
- }
79
+ if (url.search && existsRedirect.isQueryStringPreserved) {
80
+ const targetQueryString = (_a = target[1]) !== null && _a !== void 0 ? _a : '';
81
+ url.search = '?' + new URLSearchParams(`${url.search}&${targetQueryString}`).toString();
83
82
  }
83
+ else if (target[1]) {
84
+ url.search = '?' + target[1];
85
+ }
86
+ else {
87
+ url.search = '';
88
+ }
89
+ const prepareNewURL = new URL(`${target[0]}${url.search}`, url.origin);
90
+ url.href = prepareNewURL.href;
91
+ url.pathname = prepareNewURL.pathname;
92
+ url.search = prepareNewURL.search;
93
+ url.locale = req.nextUrl.locale;
84
94
  }
85
- const redirectUrl = decodeURIComponent(url.href);
86
95
  /** return Response redirect with http code of redirect type **/
87
96
  switch (existsRedirect.redirectType) {
88
- case REDIRECT_TYPE_301:
89
- return NextResponse.redirect(redirectUrl, {
90
- status: 301,
91
- statusText: 'Moved Permanently',
92
- headers: res === null || res === void 0 ? void 0 : res.headers,
93
- });
94
- case REDIRECT_TYPE_302:
95
- return NextResponse.redirect(redirectUrl, {
96
- status: 302,
97
- statusText: 'Found',
98
- headers: res === null || res === void 0 ? void 0 : res.headers,
99
- });
100
- case REDIRECT_TYPE_SERVER_TRANSFER:
101
- return NextResponse.rewrite(redirectUrl, res);
97
+ case REDIRECT_TYPE_301: {
98
+ return this.createRedirectResponse(url, res, 301, 'Moved Permanently');
99
+ }
100
+ case REDIRECT_TYPE_302: {
101
+ return this.createRedirectResponse(url, res, 302, 'Found');
102
+ }
103
+ case REDIRECT_TYPE_SERVER_TRANSFER: {
104
+ return this.rewrite(url.href, req, res || NextResponse.next());
105
+ }
102
106
  default:
103
107
  return res || NextResponse.next();
104
108
  }
@@ -143,28 +147,139 @@ export class RedirectsMiddleware extends MiddlewareBase {
143
147
  getExistsRedirect(req, siteName) {
144
148
  return __awaiter(this, void 0, void 0, function* () {
145
149
  const redirects = yield this.redirectsService.fetchRedirects(siteName);
146
- const tragetURL = req.nextUrl.pathname;
147
- const targetQS = req.nextUrl.search || '';
150
+ const { pathname: targetURL, search: targetQS = '', locale } = this.normalizeUrl(req.nextUrl.clone());
148
151
  const language = this.getLanguage(req);
149
152
  const modifyRedirects = structuredClone(redirects);
150
153
  return modifyRedirects.length
151
154
  ? modifyRedirects.find((redirect) => {
155
+ // Modify the redirect pattern to ignore the language prefix in the path
152
156
  redirect.pattern = redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), '');
157
+ // Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
153
158
  redirect.pattern = `/^\/${redirect.pattern
154
- .replace(/^\/|\/$/g, '')
155
- .replace(/^\^\/|\/\$$/g, '')
156
- .replace(/^\^|\$$/g, '')
157
- .replace(/(?<!\\)\?/g, '\\?')
158
- .replace(/\$\/gi$/g, '')}[\/]?$/gi`;
159
- return ((regexParser(redirect.pattern).test(tragetURL) ||
160
- regexParser(redirect.pattern).test(`${tragetURL}${targetQS}`) ||
161
- regexParser(redirect.pattern).test(`/${req.nextUrl.locale}${tragetURL}`) ||
162
- regexParser(redirect.pattern).test(`/${req.nextUrl.locale}${tragetURL}${targetQS}`)) &&
163
- (redirect.locale
164
- ? redirect.locale.toLowerCase() === req.nextUrl.locale.toLowerCase()
165
- : true));
159
+ .replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
160
+ .replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
161
+ .replace(/^\^|\$$/g, '') // Further cleans up anchors
162
+ .replace(/(?<!\\)\?/g, '\\?') // Escapes question marks in the pattern
163
+ .replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
164
+ /**
165
+ * This line checks whether the current URL query string (and all its possible permutations)
166
+ * matches the redirect pattern.
167
+ *
168
+ * Query parameters in URLs can come in different orders, but logically they represent the
169
+ * same information (e.g., "key1=value1&key2=value2" is the same as "key2=value2&key1=value1").
170
+ * To account for this, the method `isPermutedQueryMatch` generates all possible permutations
171
+ * of the query parameters and checks if any of those permutations match the regex pattern for the redirect.
172
+ *
173
+ * NOTE: This fix is specifically implemented for Netlify, where query parameters are sometimes
174
+ * automatically sorted, which can cause issues with matching redirects if the order of query
175
+ * parameters is important. By checking every possible permutation, we ensure that redirects
176
+ * work correctly on Netlify despite this behavior.
177
+ *
178
+ * It passes several pieces of information to the function:
179
+ * 1. `pathname`: The normalized URL path without query parameters (e.g., '/about').
180
+ * 2. `queryString`: The current query string from the URL, which will be permuted and matched (e.g., '?key1=value1&key2=value2').
181
+ * 3. `pattern`: The regex pattern for the redirect that we are trying to match against the URL (e.g., '/about?key1=value1').
182
+ * 4. `locale`: The locale part of the URL (if any), which helps support multilingual URLs.
183
+ *
184
+ * If one of the permutations of the query string matches the redirect pattern, the function
185
+ * returns the matched query string, which is stored in `matchedQueryString`. If no match is found,
186
+ * it returns `undefined`. The `matchedQueryString` is later used to indicate whether the query
187
+ * string contributed to a successful redirect match.
188
+ */
189
+ const matchedQueryString = this.isPermutedQueryMatch({
190
+ pathname: targetURL,
191
+ queryString: targetQS,
192
+ pattern: redirect.pattern,
193
+ locale,
194
+ });
195
+ // Save the matched query string (if found) into the redirect object
196
+ redirect.matchedQueryString = matchedQueryString || '';
197
+ // Return the redirect if the URL path or any query string permutation matches the pattern
198
+ return ((regexParser(redirect.pattern).test(targetURL) ||
199
+ regexParser(redirect.pattern).test(`/${req.nextUrl.locale}${targetURL}`) ||
200
+ matchedQueryString) &&
201
+ (redirect.locale ? redirect.locale.toLowerCase() === locale.toLowerCase() : true));
166
202
  })
167
203
  : undefined;
168
204
  });
169
205
  }
206
+ /**
207
+ * When a user clicks on a link generated by the Link component from next/link,
208
+ * Next.js adds special parameters in the route called path.
209
+ * This method removes these special parameters.
210
+ * @param {NextURL} url
211
+ * @returns {string} normalize url
212
+ */
213
+ normalizeUrl(url) {
214
+ if (!url.search) {
215
+ return url;
216
+ }
217
+ /**
218
+ * Prepare special parameters for exclusion.
219
+ */
220
+ const splittedPathname = url.pathname
221
+ .split('/')
222
+ .filter((route) => route)
223
+ .map((route) => `path=${route}`);
224
+ /**
225
+ * Remove special parameters(Next.JS)
226
+ * Example: /about/contact/us
227
+ * When a user clicks on this link, Next.js should generate a link for the middleware, formatted like this:
228
+ * http://host/about/contact/us?path=about&path=contact&path=us
229
+ */
230
+ const newQueryString = url.search
231
+ .replace(/^\?/, '')
232
+ .split('&')
233
+ .filter((param) => {
234
+ if (!splittedPathname.includes(param)) {
235
+ return param;
236
+ }
237
+ return false;
238
+ })
239
+ .join('&');
240
+ const newUrl = new URL(`${url.pathname}?${newQueryString}`, url.origin);
241
+ url.search = newUrl.search;
242
+ url.pathname = newUrl.pathname;
243
+ url.href = newUrl.href;
244
+ return url;
245
+ }
246
+ /**
247
+ * Helper function to create a redirect response and remove the x-middleware-next header.
248
+ * @param {NextURL} url The URL to redirect to.
249
+ * @param {Response} res The response object.
250
+ * @param {number} status The HTTP status code of the redirect.
251
+ * @param {string} statusText The status text of the redirect.
252
+ * @returns {NextResponse<unknown>} The redirect response.
253
+ */
254
+ createRedirectResponse(url, res, status, statusText) {
255
+ const redirect = NextResponse.redirect(url, {
256
+ status,
257
+ statusText,
258
+ headers: res === null || res === void 0 ? void 0 : res.headers,
259
+ });
260
+ if (res === null || res === void 0 ? void 0 : res.headers) {
261
+ redirect.headers.delete('x-middleware-next');
262
+ redirect.headers.delete('x-middleware-rewrite');
263
+ }
264
+ return redirect;
265
+ }
266
+ /**
267
+ * Checks if the current URL query matches the provided pattern, considering all permutations of query parameters.
268
+ * It constructs all possible query parameter permutations and tests them against the pattern.
269
+ * @param {Object} params - The parameters for the URL match.
270
+ * @param {string} params.pathname - The current URL pathname.
271
+ * @param {string} params.queryString - The current URL query string.
272
+ * @param {string} params.pattern - The regex pattern to test the constructed URLs against.
273
+ * @param {string} [params.locale] - The locale prefix to include in the URL if present.
274
+ * @returns {string | undefined} - return query string if any of the query permutations match the provided pattern, undefined otherwise.
275
+ */
276
+ isPermutedQueryMatch({ pathname, queryString, pattern, locale, }) {
277
+ const paramsArray = Array.from(new URLSearchParams(queryString).entries());
278
+ const listOfPermuted = getPermutations(paramsArray).map((permutation) => '?' + permutation.map(([key, value]) => `${key}=${value}`).join('&'));
279
+ const normalizedPath = pathname.replace(/\/*$/gi, '');
280
+ return listOfPermuted.find((query) => [
281
+ regexParser(pattern).test(`${normalizedPath}${query}`),
282
+ regexParser(pattern).test(`/${locale}${normalizedPath}${query}`),
283
+ ].some(Boolean));
284
+ }
170
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sitecore-jss/sitecore-jss-nextjs",
3
- "version": "22.2.0-canary.8",
3
+ "version": "22.2.0-canary.80",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "sideEffects": false,
@@ -54,7 +54,7 @@
54
54
  "eslint-plugin-react": "^7.32.1",
55
55
  "jsdom": "^21.1.0",
56
56
  "mocha": "^10.2.0",
57
- "next": "^14.1.0",
57
+ "next": "^14.2.7",
58
58
  "nock": "^13.3.0",
59
59
  "nyc": "^15.1.0",
60
60
  "react": "^18.2.0",
@@ -67,14 +67,14 @@
67
67
  "peerDependencies": {
68
68
  "@sitecore-cloudsdk/events": "^0.3.1",
69
69
  "@sitecore-cloudsdk/personalize": "^0.3.1",
70
- "next": "^14.1.0",
70
+ "next": "^14.2.7",
71
71
  "react": "^18.2.0",
72
72
  "react-dom": "^18.2.0"
73
73
  },
74
74
  "dependencies": {
75
- "@sitecore-jss/sitecore-jss": "^22.2.0-canary.8",
76
- "@sitecore-jss/sitecore-jss-dev-tools": "^22.2.0-canary.8",
77
- "@sitecore-jss/sitecore-jss-react": "^22.2.0-canary.8",
75
+ "@sitecore-jss/sitecore-jss": "^22.2.0-canary.80",
76
+ "@sitecore-jss/sitecore-jss-dev-tools": "^22.2.0-canary.80",
77
+ "@sitecore-jss/sitecore-jss-react": "^22.2.0-canary.80",
78
78
  "@vercel/kv": "^0.2.1",
79
79
  "prop-types": "^15.8.1",
80
80
  "regex-parser": "^2.2.11",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "description": "",
84
84
  "types": "types/index.d.ts",
85
- "gitHead": "755525262fcb998452876d2f28a57d130e29731c",
85
+ "gitHead": "0ecca7f10712e929cfe4375ccfbd479020cd30fc",
86
86
  "files": [
87
87
  "dist",
88
88
  "types",
@@ -1,4 +1,3 @@
1
- export declare const QUERY_PARAM_EDITING_SECRET = "secret";
2
1
  export declare const QUERY_PARAM_VERCEL_PROTECTION_BYPASS = "x-vercel-protection-bypass";
3
2
  export declare const QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = "x-vercel-set-bypass-cookie";
4
3
  /**
@@ -6,7 +5,3 @@ export declare const QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE = "x-vercel-set-bypass
6
5
  * Note these are in lowercase format to match expected `IncomingHttpHeaders`.
7
6
  */
8
7
  export declare const EDITING_PASS_THROUGH_HEADERS: string[];
9
- /**
10
- * Default allowed origins for editing requests. This is used to enforce CORS, CSP headers.
11
- */
12
- export declare const EDITING_ALLOWED_ORIGINS: string[];
@@ -1,6 +1,7 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
2
  import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss';
3
3
  import { EditMode, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout';
4
+ import { RenderMetadataQueryParams, LayoutKind } from '@sitecore-jss/sitecore-jss/editing';
4
5
  import { EditingDataService } from './editing-data-service';
5
6
  import { RenderMiddlewareBase } from './render-middleware';
6
7
  /**
@@ -95,25 +96,11 @@ export declare class ChromesHandler extends RenderMiddlewareBase {
95
96
  * Configuration for the Editing Metadata Handler.
96
97
  */
97
98
  export type EditingRenderMiddlewareMetadataConfig = Pick<EditingRenderMiddlewareConfig, 'resolvePageUrl'>;
98
- /**
99
- * Query parameters appended to the page route URL
100
- * Appended when XMCloud Pages preview (editing) Metadata Edit Mode is used
101
- */
102
- export type MetadataQueryParams = {
103
- secret: string;
104
- sc_lang: string;
105
- sc_itemid: string;
106
- sc_site: string;
107
- route: string;
108
- mode: Exclude<LayoutServicePageState, 'normal'>;
109
- sc_variant?: string;
110
- sc_version?: string;
111
- };
112
99
  /**
113
100
  * Next.js API request with Metadata query parameters.
114
101
  */
115
102
  type MetadataNextApiRequest = NextApiRequest & {
116
- query: MetadataQueryParams;
103
+ query: RenderMetadataQueryParams;
117
104
  };
118
105
  /**
119
106
  * Data for Next.js Preview (Editing) Metadata Edit Mode.
@@ -126,6 +113,7 @@ export type EditingMetadataPreviewData = {
126
113
  pageState: Exclude<LayoutServicePageState, 'Normal'>;
127
114
  variantIds: string[];
128
115
  version?: string;
116
+ layoutKind?: LayoutKind;
129
117
  };
130
118
  /**
131
119
  * Type guard for EditingMetadataPreviewData
package/types/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { constants, HttpDataFetcher, HttpResponse, AxiosResponse, AxiosDataFetcher, AxiosDataFetcherConfig, NativeDataFetcher, NativeDataFetcherConfig, HTMLLink, enableDebug, debug, } from '@sitecore-jss/sitecore-jss';
1
+ export { constants, HttpDataFetcher, HttpResponse, AxiosResponse, AxiosDataFetcher, AxiosDataFetcherConfig, NativeDataFetcher, NativeDataFetcherConfig, HTMLLink, enableDebug, debug, CacheClient, CacheOptions, MemoryCacheClient, } from '@sitecore-jss/sitecore-jss';
2
2
  export { LayoutService, LayoutServiceData, LayoutServicePageState, LayoutServiceContext, LayoutServiceContextData, GraphQLLayoutService, GraphQLLayoutServiceConfig, RestLayoutService, RestLayoutServiceConfig, PlaceholderData, PlaceholdersData, RouteData, Field, Item, HtmlElementRendering, getChildPlaceholder, getFieldValue, ComponentRendering, ComponentFields, ComponentParams, getContentStylesheetLink, EditMode, } from '@sitecore-jss/sitecore-jss/layout';
3
3
  export { mediaApi } from '@sitecore-jss/sitecore-jss/media';
4
4
  export { trackingApi, TrackingRequestOptions, CampaignInstance, GoalInstance, OutcomeInstance, EventInstance, PageViewInstance, } from '@sitecore-jss/sitecore-jss/tracking';
@@ -1,4 +1,5 @@
1
1
  export { debug } from '@sitecore-jss/sitecore-jss';
2
+ export { MiddlewareBase, MiddlewareBaseConfig } from './middleware';
2
3
  export { RedirectsMiddleware, RedirectsMiddlewareConfig } from './redirects-middleware';
3
4
  export { PersonalizeMiddleware, PersonalizeMiddlewareConfig } from './personalize-middleware';
4
5
  export { MultisiteMiddleware, MultisiteMiddlewareConfig } from './multisite-middleware';
@@ -1,5 +1,5 @@
1
- import { NextResponse, NextRequest } from 'next/server';
2
1
  import { GraphQLRedirectsServiceConfig } from '@sitecore-jss/sitecore-jss/site';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
3
  import { MiddlewareBase, MiddlewareBaseConfig } from './middleware';
4
4
  /**
5
5
  * extended RedirectsMiddlewareConfig config type for RedirectsMiddleware
@@ -37,4 +37,32 @@ export declare class RedirectsMiddleware extends MiddlewareBase {
37
37
  * @private
38
38
  */
39
39
  private getExistsRedirect;
40
+ /**
41
+ * When a user clicks on a link generated by the Link component from next/link,
42
+ * Next.js adds special parameters in the route called path.
43
+ * This method removes these special parameters.
44
+ * @param {NextURL} url
45
+ * @returns {string} normalize url
46
+ */
47
+ private normalizeUrl;
48
+ /**
49
+ * Helper function to create a redirect response and remove the x-middleware-next header.
50
+ * @param {NextURL} url The URL to redirect to.
51
+ * @param {Response} res The response object.
52
+ * @param {number} status The HTTP status code of the redirect.
53
+ * @param {string} statusText The status text of the redirect.
54
+ * @returns {NextResponse<unknown>} The redirect response.
55
+ */
56
+ private createRedirectResponse;
57
+ /**
58
+ * Checks if the current URL query matches the provided pattern, considering all permutations of query parameters.
59
+ * It constructs all possible query parameter permutations and tests them against the pattern.
60
+ * @param {Object} params - The parameters for the URL match.
61
+ * @param {string} params.pathname - The current URL pathname.
62
+ * @param {string} params.queryString - The current URL query string.
63
+ * @param {string} params.pattern - The regex pattern to test the constructed URLs against.
64
+ * @param {string} [params.locale] - The locale prefix to include in the URL if present.
65
+ * @returns {string | undefined} - return query string if any of the query permutations match the provided pattern, undefined otherwise.
66
+ */
67
+ private isPermutedQueryMatch;
40
68
  }