@sitecore-jss/sitecore-jss-nextjs 21.10.0 → 21.10.1
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/dist/cjs/middleware/middleware.js +28 -3
- package/dist/cjs/middleware/redirects-middleware.js +214 -92
- package/dist/esm/middleware/middleware.js +27 -2
- package/dist/esm/middleware/redirects-middleware.js +215 -93
- package/package.json +5 -5
- package/types/middleware/middleware.d.ts +9 -1
- package/types/middleware/redirects-middleware.d.ts +48 -8
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MiddlewareBase = void 0;
|
|
3
|
+
exports.MiddlewareBase = exports.REWRITE_HEADER_NAME = void 0;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
|
+
exports.REWRITE_HEADER_NAME = 'x-sc-rewrite';
|
|
5
6
|
class MiddlewareBase {
|
|
6
7
|
constructor(config) {
|
|
7
8
|
this.config = config;
|
|
@@ -36,6 +37,27 @@ class MiddlewareBase {
|
|
|
36
37
|
incomingHeaders.forEach((value, key) => (headers[key] = value));
|
|
37
38
|
return headers;
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Determines if the request is a Next.js (next/link) prefetch request
|
|
42
|
+
* @param {NextRequest} req request
|
|
43
|
+
* @returns {boolean} is prefetch
|
|
44
|
+
*/
|
|
45
|
+
isPrefetch(req) {
|
|
46
|
+
const isMobile = req.headers.get('sec-ch-ua-mobile') === '?1';
|
|
47
|
+
const userAgent = req.headers.get('user-agent') || '';
|
|
48
|
+
const isKnownPlatform = /iPhone|Mac|Linux|Windows|Android/i.test(userAgent);
|
|
49
|
+
const isKnownDevice = isMobile || isKnownPlatform;
|
|
50
|
+
const purpose = req.headers.get('purpose');
|
|
51
|
+
const nextRouterPrefetch = req.headers.get('Next-Router-Prefetch');
|
|
52
|
+
const middlewarePrefetch = req.headers.get('x-middleware-prefetch');
|
|
53
|
+
// Some real navigations on different devices may incorrectly include 'prefetch' headers.
|
|
54
|
+
// To avoid skipping personalization in such cases, we treat 'x-middleware-prefetch' as a more reliable signal of true prefetch behavior.
|
|
55
|
+
if (isKnownDevice && middlewarePrefetch === '1') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Otherwise, standard prefetch detection
|
|
59
|
+
return purpose === 'prefetch' || nextRouterPrefetch === '1' || middlewarePrefetch === '1';
|
|
60
|
+
}
|
|
39
61
|
/**
|
|
40
62
|
* Provides used language
|
|
41
63
|
* @param {NextRequest} req request
|
|
@@ -72,14 +94,17 @@ class MiddlewareBase {
|
|
|
72
94
|
* @param {string} rewritePath the destionation path
|
|
73
95
|
* @param {NextRequest} req the current request
|
|
74
96
|
* @param {NextResponse} res the current response
|
|
97
|
+
* @param {boolean} [skipHeader] don't write 'x-sc-rewrite' header
|
|
75
98
|
*/
|
|
76
|
-
rewrite(rewritePath, req, res) {
|
|
99
|
+
rewrite(rewritePath, req, res, skipHeader) {
|
|
77
100
|
// Note an absolute URL is required: https://nextjs.org/docs/messages/middleware-relative-urls
|
|
78
101
|
const rewriteUrl = req.nextUrl.clone();
|
|
79
102
|
rewriteUrl.pathname = rewritePath;
|
|
80
103
|
const response = server_1.NextResponse.rewrite(rewriteUrl, res);
|
|
81
104
|
// Share rewrite path with following executed middlewares
|
|
82
|
-
|
|
105
|
+
if (!skipHeader) {
|
|
106
|
+
response.headers.set(exports.REWRITE_HEADER_NAME, rewritePath);
|
|
107
|
+
}
|
|
83
108
|
return response;
|
|
84
109
|
}
|
|
85
110
|
}
|
|
@@ -17,6 +17,7 @@ const regex_parser_1 = __importDefault(require("regex-parser"));
|
|
|
17
17
|
const server_1 = require("next/server");
|
|
18
18
|
const site_1 = require("@sitecore-jss/sitecore-jss/site");
|
|
19
19
|
const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
|
|
20
|
+
const utils_1 = require("@sitecore-jss/sitecore-jss/utils");
|
|
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');
|
|
@@ -25,13 +26,100 @@ const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
|
25
26
|
* compares with current url and redirects to target url
|
|
26
27
|
*/
|
|
27
28
|
class RedirectsMiddleware extends middleware_1.MiddlewareBase {
|
|
28
|
-
/**
|
|
29
|
-
* @param {RedirectsMiddlewareConfig} [config] redirects middleware config
|
|
30
|
-
*/
|
|
31
29
|
constructor(config) {
|
|
32
30
|
super(config);
|
|
33
31
|
this.config = config;
|
|
34
|
-
|
|
32
|
+
// NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
|
|
33
|
+
// (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
|
|
34
|
+
this.redirectsService = new site_1.GraphQLRedirectsService(Object.assign(Object.assign({}, config), { fetch: fetch }));
|
|
35
|
+
this.locales = config.locales;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gets the Next.js middleware handler with error handling
|
|
39
|
+
* @returns route handler
|
|
40
|
+
*/
|
|
41
|
+
getHandler() {
|
|
42
|
+
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
try {
|
|
44
|
+
return this.processRedirectRequest(req, res);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.log('Redirect middleware failed:');
|
|
48
|
+
console.log(error);
|
|
49
|
+
return res || server_1.NextResponse.next();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Method returns RedirectInfo when matches
|
|
55
|
+
* @param {NextRequest} req request
|
|
56
|
+
* @param {string} siteName site name
|
|
57
|
+
* @returns Promise<RedirectInfo | undefined> The redirect info or undefined if no redirect is found
|
|
58
|
+
* @protected
|
|
59
|
+
*/
|
|
60
|
+
getExistsRedirect(req, siteName) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const { pathname: incomingURL, search: incomingQS = '' } = this.normalizeUrl(req.nextUrl.clone());
|
|
63
|
+
const locale = this.getLanguage(req);
|
|
64
|
+
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
65
|
+
const redirects = yield this.getRedirects(siteName);
|
|
66
|
+
const language = this.getLanguage(req);
|
|
67
|
+
const modifyRedirects = structuredClone(redirects);
|
|
68
|
+
let matchedQueryString;
|
|
69
|
+
const localePath = `/${locale.toLowerCase()}${normalizedPath}`;
|
|
70
|
+
return modifyRedirects.length
|
|
71
|
+
? modifyRedirects.find((redirect) => {
|
|
72
|
+
// process static URL (non-regex) rules
|
|
73
|
+
if ((0, utils_1.isRegexOrUrl)(redirect.pattern) === 'url') {
|
|
74
|
+
const urlArray = redirect.pattern.endsWith('/')
|
|
75
|
+
? redirect.pattern.slice(0, -1).split('?')
|
|
76
|
+
: redirect.pattern.split('?');
|
|
77
|
+
const patternQS = urlArray[1];
|
|
78
|
+
let patternPath = urlArray[0].toLowerCase();
|
|
79
|
+
// nextjs routes are case-sensitive, but locales should be compared case-insensitively
|
|
80
|
+
const patternParts = patternPath.split('/');
|
|
81
|
+
const maybeLocale = patternParts[1].toLowerCase();
|
|
82
|
+
// case insensitive lookup of locales
|
|
83
|
+
if (new RegExp(this.locales.join('|'), 'i').test(maybeLocale)) {
|
|
84
|
+
patternPath = patternPath.replace(`/${patternParts[1]}`, `/${maybeLocale}`);
|
|
85
|
+
}
|
|
86
|
+
return ((patternPath === localePath || patternPath === normalizedPath) &&
|
|
87
|
+
(!patternQS ||
|
|
88
|
+
(0, utils_1.areURLSearchParamsEqual)(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
|
|
89
|
+
}
|
|
90
|
+
// process regex rules
|
|
91
|
+
// Modify the redirect pattern to ignore the language prefix in the path
|
|
92
|
+
// And escapes non-special "?" characters in a string or regex.
|
|
93
|
+
redirect.pattern = (0, utils_1.escapeNonSpecialQuestionMarks)(redirect.pattern.replace(new RegExp(`^[^]?/${language}/`, 'gi'), ''));
|
|
94
|
+
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
|
|
95
|
+
redirect.pattern = `/^\/${redirect.pattern
|
|
96
|
+
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
|
|
97
|
+
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
|
|
98
|
+
.replace(/^\^|\$$/g, '') // Further cleans up anchors
|
|
99
|
+
.replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
|
|
100
|
+
// Redirect pattern matches the full incoming URL with query string present
|
|
101
|
+
matchedQueryString = [
|
|
102
|
+
(0, regex_parser_1.default)(redirect.pattern).test(`/${localePath}${incomingQS}`),
|
|
103
|
+
(0, regex_parser_1.default)(redirect.pattern).test(`${normalizedPath}${incomingQS}`),
|
|
104
|
+
].some(Boolean)
|
|
105
|
+
? incomingQS
|
|
106
|
+
: undefined;
|
|
107
|
+
// Save the matched query string (if found) into the redirect object
|
|
108
|
+
redirect.matchedQueryString = matchedQueryString || '';
|
|
109
|
+
return (!!((0, regex_parser_1.default)(redirect.pattern).test(`/${req.nextUrl.locale}${incomingURL}`) ||
|
|
110
|
+
(0, regex_parser_1.default)(redirect.pattern).test(incomingURL) ||
|
|
111
|
+
matchedQueryString) && (redirect.locale ? redirect.locale.toLowerCase() === locale.toLowerCase() : true));
|
|
112
|
+
})
|
|
113
|
+
: undefined;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* @param {NextRequest} req request
|
|
118
|
+
* @param {Response} res response
|
|
119
|
+
* @returns {Promise<NextResponse>} The redirect response.
|
|
120
|
+
*/
|
|
121
|
+
processRedirectRequest(req, res) {
|
|
122
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
123
|
const pathname = req.nextUrl.pathname;
|
|
36
124
|
const language = this.getLanguage(req);
|
|
37
125
|
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
@@ -43,70 +131,69 @@ class RedirectsMiddleware extends middleware_1.MiddlewareBase {
|
|
|
43
131
|
hostname,
|
|
44
132
|
});
|
|
45
133
|
const createResponse = () => __awaiter(this, void 0, void 0, function* () {
|
|
46
|
-
|
|
134
|
+
var _a;
|
|
135
|
+
const response = res || server_1.NextResponse.next();
|
|
136
|
+
if (this.config.disabled && this.config.disabled(req, response)) {
|
|
47
137
|
sitecore_jss_1.debug.redirects('skipped (redirects middleware is disabled)');
|
|
48
|
-
return
|
|
138
|
+
return response;
|
|
49
139
|
}
|
|
50
140
|
if (this.isPreview(req) || this.excludeRoute(pathname)) {
|
|
51
141
|
sitecore_jss_1.debug.redirects('skipped (%s)', this.isPreview(req) ? 'preview' : 'route excluded');
|
|
52
|
-
return
|
|
142
|
+
return response;
|
|
53
143
|
}
|
|
54
|
-
|
|
144
|
+
// Skip prefetch requests from Next.js, which are not original client requests
|
|
145
|
+
// as they load unnecessary requests that burden the redirects middleware with meaningless traffic
|
|
146
|
+
if (this.isPrefetch(req)) {
|
|
147
|
+
sitecore_jss_1.debug.redirects('skipped (prefetch)');
|
|
148
|
+
response.headers.set('x-middleware-cache', 'no-cache');
|
|
149
|
+
response.headers.set('Cache-Control', 'no-store, must-revalidate');
|
|
150
|
+
return response;
|
|
151
|
+
}
|
|
152
|
+
site = this.getSite(req, response);
|
|
55
153
|
// Find the redirect from result of RedirectService
|
|
56
154
|
const existsRedirect = yield this.getExistsRedirect(req, site.name);
|
|
57
155
|
if (!existsRedirect) {
|
|
58
156
|
sitecore_jss_1.debug.redirects('skipped (redirect does not exist)');
|
|
59
|
-
return
|
|
157
|
+
return response;
|
|
60
158
|
}
|
|
159
|
+
sitecore_jss_1.debug.redirects('Matched redirect rule: %o', { existsRedirect });
|
|
61
160
|
// Find context site language and replace token
|
|
62
161
|
if (REGEXP_CONTEXT_SITE_LANG.test(existsRedirect.target) &&
|
|
63
162
|
!(REGEXP_ABSOLUTE_URL.test(existsRedirect.target) &&
|
|
64
163
|
existsRedirect.target.includes(hostname))) {
|
|
65
164
|
existsRedirect.target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, site.language);
|
|
165
|
+
req.nextUrl.locale = site.language;
|
|
66
166
|
}
|
|
67
|
-
const url = req.nextUrl.clone();
|
|
167
|
+
const url = this.normalizeUrl(req.nextUrl.clone());
|
|
68
168
|
if (REGEXP_ABSOLUTE_URL.test(existsRedirect.target)) {
|
|
69
|
-
|
|
169
|
+
return this.dispatchRedirect(existsRedirect.target, existsRedirect.redirectType, req, response, true);
|
|
70
170
|
}
|
|
71
171
|
else {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const urlFirstPart =
|
|
172
|
+
const isUrl = (0, utils_1.isRegexOrUrl)(existsRedirect.pattern) === 'url';
|
|
173
|
+
const targetParts = existsRedirect.target.split('/');
|
|
174
|
+
const urlFirstPart = targetParts[1];
|
|
75
175
|
if (this.locales.includes(urlFirstPart)) {
|
|
76
|
-
|
|
176
|
+
req.nextUrl.locale = urlFirstPart;
|
|
77
177
|
existsRedirect.target = existsRedirect.target.replace(`/${urlFirstPart}`, '');
|
|
78
178
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
.replace(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
108
|
-
default:
|
|
109
|
-
return res || server_1.NextResponse.next();
|
|
179
|
+
const targetSegments = isUrl
|
|
180
|
+
? existsRedirect.target.split('?')
|
|
181
|
+
: url.pathname.replace(/\/*$/gi, '') + existsRedirect.matchedQueryString;
|
|
182
|
+
const [targetPath, targetQueryString] = isUrl
|
|
183
|
+
? targetSegments
|
|
184
|
+
: targetSegments
|
|
185
|
+
.replace((0, regex_parser_1.default)(existsRedirect.pattern), existsRedirect.target)
|
|
186
|
+
.replace(/^\/\//, '/')
|
|
187
|
+
.split('?');
|
|
188
|
+
const mergedQueryString = existsRedirect.isQueryStringPreserved
|
|
189
|
+
? (0, utils_1.mergeURLSearchParams)(new URLSearchParams((_a = url.search) !== null && _a !== void 0 ? _a : ''), new URLSearchParams(targetQueryString || ''))
|
|
190
|
+
: targetQueryString || '';
|
|
191
|
+
const prepareNewURL = new URL(`${targetPath}${mergedQueryString ? `?${mergedQueryString}` : ''}`, url.origin);
|
|
192
|
+
url.href = prepareNewURL.href;
|
|
193
|
+
url.pathname = prepareNewURL.pathname;
|
|
194
|
+
url.search = prepareNewURL.search;
|
|
195
|
+
url.locale = req.nextUrl.locale;
|
|
196
|
+
return this.dispatchRedirect(url, existsRedirect.redirectType, req, response, false);
|
|
110
197
|
}
|
|
111
198
|
});
|
|
112
199
|
const response = yield createResponse();
|
|
@@ -118,60 +205,95 @@ class RedirectsMiddleware extends middleware_1.MiddlewareBase {
|
|
|
118
205
|
});
|
|
119
206
|
return response;
|
|
120
207
|
});
|
|
121
|
-
// NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
|
|
122
|
-
// (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
|
|
123
|
-
this.redirectsService = new site_1.GraphQLRedirectsService(Object.assign(Object.assign({}, config), { fetch: fetch }));
|
|
124
|
-
this.locales = config.locales;
|
|
125
208
|
}
|
|
126
209
|
/**
|
|
127
|
-
*
|
|
128
|
-
* @
|
|
210
|
+
* Fetches all redirects for a given site from the Sitecore instance
|
|
211
|
+
* @param {string} siteName - The name of the site to fetch redirects for
|
|
212
|
+
* @returns {Promise<RedirectInfo[]>} A promise that resolves to an array of redirect information
|
|
213
|
+
* @protected
|
|
129
214
|
*/
|
|
130
|
-
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
return yield this.handler(req, res);
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
console.log('Redirect middleware failed:');
|
|
137
|
-
console.log(error);
|
|
138
|
-
return res || server_1.NextResponse.next();
|
|
139
|
-
}
|
|
215
|
+
getRedirects(siteName) {
|
|
216
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
+
return this.redirectsService.fetchRedirects(siteName);
|
|
140
218
|
});
|
|
141
219
|
}
|
|
142
220
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* @
|
|
147
|
-
* @
|
|
221
|
+
* When a user clicks on a link generated by the Link component from next/link,
|
|
222
|
+
* Next.js adds special parameters in the route called path.
|
|
223
|
+
* This method removes these special parameters.
|
|
224
|
+
* @param {NextURL} url
|
|
225
|
+
* @returns {string} normalize url
|
|
148
226
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
227
|
+
normalizeUrl(url) {
|
|
228
|
+
if (!url.search)
|
|
229
|
+
return url;
|
|
230
|
+
/**
|
|
231
|
+
* Prepare special parameters for exclusion.
|
|
232
|
+
*/
|
|
233
|
+
const splittedPathname = url.pathname
|
|
234
|
+
.split('/')
|
|
235
|
+
.filter((route) => route)
|
|
236
|
+
.map((route) => `path=${route}`);
|
|
237
|
+
/**
|
|
238
|
+
* Remove special parameters(Next.JS)
|
|
239
|
+
* Example: /about/contact/us
|
|
240
|
+
* When a user clicks on this link, Next.js should generate a link for the middleware, formatted like this:
|
|
241
|
+
* http://host/about/contact/us?path=about&path=contact&path=us
|
|
242
|
+
*/
|
|
243
|
+
const newQueryString = url.search
|
|
244
|
+
.replace(/^\?/, '')
|
|
245
|
+
.split('&')
|
|
246
|
+
.filter((param) => !splittedPathname.includes(param))
|
|
247
|
+
.join('&');
|
|
248
|
+
const newUrl = new URL(`${url.pathname.toLowerCase()}?${newQueryString}`, url.origin);
|
|
249
|
+
url.search = newUrl.search;
|
|
250
|
+
url.pathname = newUrl.pathname.toLowerCase();
|
|
251
|
+
url.href = newUrl.href;
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Dispatch a redirect or rewrite based on type.
|
|
256
|
+
* @param {NextURL | string} target Final target to redirect/rewrite to (NextURL or string for externals).
|
|
257
|
+
* @param {string} type One of `REDIRECT_TYPE_301`, `REDIRECT_TYPE_302`, or `REDIRECT_TYPE_SERVER_TRANSFER`.
|
|
258
|
+
* @param {NextRequest} req Incoming request.
|
|
259
|
+
* @param {NextResponse} res Current response (used for header cleanup/carry-over).
|
|
260
|
+
* @param {boolean} isExternal Set to `true` when target is an external absolute URL.
|
|
261
|
+
* @returns A NextResponse.
|
|
262
|
+
*/
|
|
263
|
+
dispatchRedirect(target, type, req, res, isExternal = false) {
|
|
264
|
+
switch (type) {
|
|
265
|
+
case site_1.REDIRECT_TYPE_301:
|
|
266
|
+
return this.createRedirectResponse(target, res, 301, 'Moved Permanently');
|
|
267
|
+
case site_1.REDIRECT_TYPE_302:
|
|
268
|
+
return this.createRedirectResponse(target, res, 302, 'Found');
|
|
269
|
+
case site_1.REDIRECT_TYPE_SERVER_TRANSFER:
|
|
270
|
+
// rewrite expects a string; unwrap NextURL if needed
|
|
271
|
+
return this.rewrite(typeof target === 'string' ? target : target.href, req, res, isExternal);
|
|
272
|
+
default:
|
|
273
|
+
// Unknown type: return the input response unchanged
|
|
274
|
+
return res;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Helper function to create a redirect response and remove the x-middleware-next header.
|
|
279
|
+
* @param {NextURL} url The URL to redirect to.
|
|
280
|
+
* @param {Response} res The response object.
|
|
281
|
+
* @param {number} status The HTTP status code of the redirect.
|
|
282
|
+
* @param {string} statusText The status text of the redirect.
|
|
283
|
+
* @returns {NextResponse<unknown>} The redirect response.
|
|
284
|
+
*/
|
|
285
|
+
createRedirectResponse(url, res, status, statusText) {
|
|
286
|
+
const redirect = server_1.NextResponse.redirect(url, {
|
|
287
|
+
status,
|
|
288
|
+
statusText,
|
|
289
|
+
headers: res === null || res === void 0 ? void 0 : res.headers,
|
|
174
290
|
});
|
|
291
|
+
if (res === null || res === void 0 ? void 0 : res.headers) {
|
|
292
|
+
redirect.headers.delete('x-middleware-next');
|
|
293
|
+
redirect.headers.delete('x-middleware-rewrite');
|
|
294
|
+
redirect.headers.delete(middleware_1.REWRITE_HEADER_NAME);
|
|
295
|
+
}
|
|
296
|
+
return redirect;
|
|
175
297
|
}
|
|
176
298
|
}
|
|
177
299
|
exports.RedirectsMiddleware = RedirectsMiddleware;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
export const REWRITE_HEADER_NAME = 'x-sc-rewrite';
|
|
2
3
|
export class MiddlewareBase {
|
|
3
4
|
constructor(config) {
|
|
4
5
|
this.config = config;
|
|
@@ -33,6 +34,27 @@ export class MiddlewareBase {
|
|
|
33
34
|
incomingHeaders.forEach((value, key) => (headers[key] = value));
|
|
34
35
|
return headers;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Determines if the request is a Next.js (next/link) prefetch request
|
|
39
|
+
* @param {NextRequest} req request
|
|
40
|
+
* @returns {boolean} is prefetch
|
|
41
|
+
*/
|
|
42
|
+
isPrefetch(req) {
|
|
43
|
+
const isMobile = req.headers.get('sec-ch-ua-mobile') === '?1';
|
|
44
|
+
const userAgent = req.headers.get('user-agent') || '';
|
|
45
|
+
const isKnownPlatform = /iPhone|Mac|Linux|Windows|Android/i.test(userAgent);
|
|
46
|
+
const isKnownDevice = isMobile || isKnownPlatform;
|
|
47
|
+
const purpose = req.headers.get('purpose');
|
|
48
|
+
const nextRouterPrefetch = req.headers.get('Next-Router-Prefetch');
|
|
49
|
+
const middlewarePrefetch = req.headers.get('x-middleware-prefetch');
|
|
50
|
+
// Some real navigations on different devices may incorrectly include 'prefetch' headers.
|
|
51
|
+
// To avoid skipping personalization in such cases, we treat 'x-middleware-prefetch' as a more reliable signal of true prefetch behavior.
|
|
52
|
+
if (isKnownDevice && middlewarePrefetch === '1') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Otherwise, standard prefetch detection
|
|
56
|
+
return purpose === 'prefetch' || nextRouterPrefetch === '1' || middlewarePrefetch === '1';
|
|
57
|
+
}
|
|
36
58
|
/**
|
|
37
59
|
* Provides used language
|
|
38
60
|
* @param {NextRequest} req request
|
|
@@ -69,14 +91,17 @@ export class MiddlewareBase {
|
|
|
69
91
|
* @param {string} rewritePath the destionation path
|
|
70
92
|
* @param {NextRequest} req the current request
|
|
71
93
|
* @param {NextResponse} res the current response
|
|
94
|
+
* @param {boolean} [skipHeader] don't write 'x-sc-rewrite' header
|
|
72
95
|
*/
|
|
73
|
-
rewrite(rewritePath, req, res) {
|
|
96
|
+
rewrite(rewritePath, req, res, skipHeader) {
|
|
74
97
|
// Note an absolute URL is required: https://nextjs.org/docs/messages/middleware-relative-urls
|
|
75
98
|
const rewriteUrl = req.nextUrl.clone();
|
|
76
99
|
rewriteUrl.pathname = rewritePath;
|
|
77
100
|
const response = NextResponse.rewrite(rewriteUrl, res);
|
|
78
101
|
// Share rewrite path with following executed middlewares
|
|
79
|
-
|
|
102
|
+
if (!skipHeader) {
|
|
103
|
+
response.headers.set(REWRITE_HEADER_NAME, rewritePath);
|
|
104
|
+
}
|
|
80
105
|
return response;
|
|
81
106
|
}
|
|
82
107
|
}
|
|
@@ -11,7 +11,8 @@ import regexParser from 'regex-parser';
|
|
|
11
11
|
import { NextResponse } from 'next/server';
|
|
12
12
|
import { GraphQLRedirectsService, REDIRECT_TYPE_301, REDIRECT_TYPE_302, REDIRECT_TYPE_SERVER_TRANSFER, } from '@sitecore-jss/sitecore-jss/site';
|
|
13
13
|
import { debug } from '@sitecore-jss/sitecore-jss';
|
|
14
|
-
import {
|
|
14
|
+
import { areURLSearchParamsEqual, escapeNonSpecialQuestionMarks, isRegexOrUrl, mergeURLSearchParams, } from '@sitecore-jss/sitecore-jss/utils';
|
|
15
|
+
import { MiddlewareBase, REWRITE_HEADER_NAME } from './middleware';
|
|
15
16
|
const REGEXP_CONTEXT_SITE_LANG = new RegExp(/\$siteLang/, 'i');
|
|
16
17
|
const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
17
18
|
/**
|
|
@@ -19,13 +20,100 @@ const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
|
19
20
|
* compares with current url and redirects to target url
|
|
20
21
|
*/
|
|
21
22
|
export class RedirectsMiddleware extends MiddlewareBase {
|
|
22
|
-
/**
|
|
23
|
-
* @param {RedirectsMiddlewareConfig} [config] redirects middleware config
|
|
24
|
-
*/
|
|
25
23
|
constructor(config) {
|
|
26
24
|
super(config);
|
|
27
25
|
this.config = config;
|
|
28
|
-
|
|
26
|
+
// NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
|
|
27
|
+
// (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
|
|
28
|
+
this.redirectsService = new GraphQLRedirectsService(Object.assign(Object.assign({}, config), { fetch: fetch }));
|
|
29
|
+
this.locales = config.locales;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets the Next.js middleware handler with error handling
|
|
33
|
+
* @returns route handler
|
|
34
|
+
*/
|
|
35
|
+
getHandler() {
|
|
36
|
+
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
try {
|
|
38
|
+
return this.processRedirectRequest(req, res);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.log('Redirect middleware failed:');
|
|
42
|
+
console.log(error);
|
|
43
|
+
return res || NextResponse.next();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Method returns RedirectInfo when matches
|
|
49
|
+
* @param {NextRequest} req request
|
|
50
|
+
* @param {string} siteName site name
|
|
51
|
+
* @returns Promise<RedirectInfo | undefined> The redirect info or undefined if no redirect is found
|
|
52
|
+
* @protected
|
|
53
|
+
*/
|
|
54
|
+
getExistsRedirect(req, siteName) {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
const { pathname: incomingURL, search: incomingQS = '' } = this.normalizeUrl(req.nextUrl.clone());
|
|
57
|
+
const locale = this.getLanguage(req);
|
|
58
|
+
const normalizedPath = incomingURL.replace(/\/*$/gi, '').toLowerCase();
|
|
59
|
+
const redirects = yield this.getRedirects(siteName);
|
|
60
|
+
const language = this.getLanguage(req);
|
|
61
|
+
const modifyRedirects = structuredClone(redirects);
|
|
62
|
+
let matchedQueryString;
|
|
63
|
+
const localePath = `/${locale.toLowerCase()}${normalizedPath}`;
|
|
64
|
+
return modifyRedirects.length
|
|
65
|
+
? modifyRedirects.find((redirect) => {
|
|
66
|
+
// process static URL (non-regex) rules
|
|
67
|
+
if (isRegexOrUrl(redirect.pattern) === 'url') {
|
|
68
|
+
const urlArray = redirect.pattern.endsWith('/')
|
|
69
|
+
? redirect.pattern.slice(0, -1).split('?')
|
|
70
|
+
: redirect.pattern.split('?');
|
|
71
|
+
const patternQS = urlArray[1];
|
|
72
|
+
let patternPath = urlArray[0].toLowerCase();
|
|
73
|
+
// nextjs routes are case-sensitive, but locales should be compared case-insensitively
|
|
74
|
+
const patternParts = patternPath.split('/');
|
|
75
|
+
const maybeLocale = patternParts[1].toLowerCase();
|
|
76
|
+
// case insensitive lookup of locales
|
|
77
|
+
if (new RegExp(this.locales.join('|'), 'i').test(maybeLocale)) {
|
|
78
|
+
patternPath = patternPath.replace(`/${patternParts[1]}`, `/${maybeLocale}`);
|
|
79
|
+
}
|
|
80
|
+
return ((patternPath === localePath || patternPath === normalizedPath) &&
|
|
81
|
+
(!patternQS ||
|
|
82
|
+
areURLSearchParamsEqual(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
|
|
83
|
+
}
|
|
84
|
+
// process regex rules
|
|
85
|
+
// Modify the redirect pattern to ignore the language prefix in the path
|
|
86
|
+
// And escapes non-special "?" characters in a string or regex.
|
|
87
|
+
redirect.pattern = escapeNonSpecialQuestionMarks(redirect.pattern.replace(new RegExp(`^[^]?/${language}/`, 'gi'), ''));
|
|
88
|
+
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
|
|
89
|
+
redirect.pattern = `/^\/${redirect.pattern
|
|
90
|
+
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
|
|
91
|
+
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
|
|
92
|
+
.replace(/^\^|\$$/g, '') // Further cleans up anchors
|
|
93
|
+
.replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
|
|
94
|
+
// Redirect pattern matches the full incoming URL with query string present
|
|
95
|
+
matchedQueryString = [
|
|
96
|
+
regexParser(redirect.pattern).test(`/${localePath}${incomingQS}`),
|
|
97
|
+
regexParser(redirect.pattern).test(`${normalizedPath}${incomingQS}`),
|
|
98
|
+
].some(Boolean)
|
|
99
|
+
? incomingQS
|
|
100
|
+
: undefined;
|
|
101
|
+
// Save the matched query string (if found) into the redirect object
|
|
102
|
+
redirect.matchedQueryString = matchedQueryString || '';
|
|
103
|
+
return (!!(regexParser(redirect.pattern).test(`/${req.nextUrl.locale}${incomingURL}`) ||
|
|
104
|
+
regexParser(redirect.pattern).test(incomingURL) ||
|
|
105
|
+
matchedQueryString) && (redirect.locale ? redirect.locale.toLowerCase() === locale.toLowerCase() : true));
|
|
106
|
+
})
|
|
107
|
+
: undefined;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* @param {NextRequest} req request
|
|
112
|
+
* @param {Response} res response
|
|
113
|
+
* @returns {Promise<NextResponse>} The redirect response.
|
|
114
|
+
*/
|
|
115
|
+
processRedirectRequest(req, res) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
117
|
const pathname = req.nextUrl.pathname;
|
|
30
118
|
const language = this.getLanguage(req);
|
|
31
119
|
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
@@ -37,70 +125,69 @@ export class RedirectsMiddleware extends MiddlewareBase {
|
|
|
37
125
|
hostname,
|
|
38
126
|
});
|
|
39
127
|
const createResponse = () => __awaiter(this, void 0, void 0, function* () {
|
|
40
|
-
|
|
128
|
+
var _a;
|
|
129
|
+
const response = res || NextResponse.next();
|
|
130
|
+
if (this.config.disabled && this.config.disabled(req, response)) {
|
|
41
131
|
debug.redirects('skipped (redirects middleware is disabled)');
|
|
42
|
-
return
|
|
132
|
+
return response;
|
|
43
133
|
}
|
|
44
134
|
if (this.isPreview(req) || this.excludeRoute(pathname)) {
|
|
45
135
|
debug.redirects('skipped (%s)', this.isPreview(req) ? 'preview' : 'route excluded');
|
|
46
|
-
return
|
|
136
|
+
return response;
|
|
47
137
|
}
|
|
48
|
-
|
|
138
|
+
// Skip prefetch requests from Next.js, which are not original client requests
|
|
139
|
+
// as they load unnecessary requests that burden the redirects middleware with meaningless traffic
|
|
140
|
+
if (this.isPrefetch(req)) {
|
|
141
|
+
debug.redirects('skipped (prefetch)');
|
|
142
|
+
response.headers.set('x-middleware-cache', 'no-cache');
|
|
143
|
+
response.headers.set('Cache-Control', 'no-store, must-revalidate');
|
|
144
|
+
return response;
|
|
145
|
+
}
|
|
146
|
+
site = this.getSite(req, response);
|
|
49
147
|
// Find the redirect from result of RedirectService
|
|
50
148
|
const existsRedirect = yield this.getExistsRedirect(req, site.name);
|
|
51
149
|
if (!existsRedirect) {
|
|
52
150
|
debug.redirects('skipped (redirect does not exist)');
|
|
53
|
-
return
|
|
151
|
+
return response;
|
|
54
152
|
}
|
|
153
|
+
debug.redirects('Matched redirect rule: %o', { existsRedirect });
|
|
55
154
|
// Find context site language and replace token
|
|
56
155
|
if (REGEXP_CONTEXT_SITE_LANG.test(existsRedirect.target) &&
|
|
57
156
|
!(REGEXP_ABSOLUTE_URL.test(existsRedirect.target) &&
|
|
58
157
|
existsRedirect.target.includes(hostname))) {
|
|
59
158
|
existsRedirect.target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, site.language);
|
|
159
|
+
req.nextUrl.locale = site.language;
|
|
60
160
|
}
|
|
61
|
-
const url = req.nextUrl.clone();
|
|
161
|
+
const url = this.normalizeUrl(req.nextUrl.clone());
|
|
62
162
|
if (REGEXP_ABSOLUTE_URL.test(existsRedirect.target)) {
|
|
63
|
-
|
|
163
|
+
return this.dispatchRedirect(existsRedirect.target, existsRedirect.redirectType, req, response, true);
|
|
64
164
|
}
|
|
65
165
|
else {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const urlFirstPart =
|
|
166
|
+
const isUrl = isRegexOrUrl(existsRedirect.pattern) === 'url';
|
|
167
|
+
const targetParts = existsRedirect.target.split('/');
|
|
168
|
+
const urlFirstPart = targetParts[1];
|
|
69
169
|
if (this.locales.includes(urlFirstPart)) {
|
|
70
|
-
|
|
170
|
+
req.nextUrl.locale = urlFirstPart;
|
|
71
171
|
existsRedirect.target = existsRedirect.target.replace(`/${urlFirstPart}`, '');
|
|
72
172
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
.replace(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
102
|
-
default:
|
|
103
|
-
return res || NextResponse.next();
|
|
173
|
+
const targetSegments = isUrl
|
|
174
|
+
? existsRedirect.target.split('?')
|
|
175
|
+
: url.pathname.replace(/\/*$/gi, '') + existsRedirect.matchedQueryString;
|
|
176
|
+
const [targetPath, targetQueryString] = isUrl
|
|
177
|
+
? targetSegments
|
|
178
|
+
: targetSegments
|
|
179
|
+
.replace(regexParser(existsRedirect.pattern), existsRedirect.target)
|
|
180
|
+
.replace(/^\/\//, '/')
|
|
181
|
+
.split('?');
|
|
182
|
+
const mergedQueryString = existsRedirect.isQueryStringPreserved
|
|
183
|
+
? mergeURLSearchParams(new URLSearchParams((_a = url.search) !== null && _a !== void 0 ? _a : ''), new URLSearchParams(targetQueryString || ''))
|
|
184
|
+
: targetQueryString || '';
|
|
185
|
+
const prepareNewURL = new URL(`${targetPath}${mergedQueryString ? `?${mergedQueryString}` : ''}`, url.origin);
|
|
186
|
+
url.href = prepareNewURL.href;
|
|
187
|
+
url.pathname = prepareNewURL.pathname;
|
|
188
|
+
url.search = prepareNewURL.search;
|
|
189
|
+
url.locale = req.nextUrl.locale;
|
|
190
|
+
return this.dispatchRedirect(url, existsRedirect.redirectType, req, response, false);
|
|
104
191
|
}
|
|
105
192
|
});
|
|
106
193
|
const response = yield createResponse();
|
|
@@ -112,59 +199,94 @@ export class RedirectsMiddleware extends MiddlewareBase {
|
|
|
112
199
|
});
|
|
113
200
|
return response;
|
|
114
201
|
});
|
|
115
|
-
// NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
|
|
116
|
-
// (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
|
|
117
|
-
this.redirectsService = new GraphQLRedirectsService(Object.assign(Object.assign({}, config), { fetch: fetch }));
|
|
118
|
-
this.locales = config.locales;
|
|
119
202
|
}
|
|
120
203
|
/**
|
|
121
|
-
*
|
|
122
|
-
* @
|
|
204
|
+
* Fetches all redirects for a given site from the Sitecore instance
|
|
205
|
+
* @param {string} siteName - The name of the site to fetch redirects for
|
|
206
|
+
* @returns {Promise<RedirectInfo[]>} A promise that resolves to an array of redirect information
|
|
207
|
+
* @protected
|
|
123
208
|
*/
|
|
124
|
-
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
return yield this.handler(req, res);
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
console.log('Redirect middleware failed:');
|
|
131
|
-
console.log(error);
|
|
132
|
-
return res || NextResponse.next();
|
|
133
|
-
}
|
|
209
|
+
getRedirects(siteName) {
|
|
210
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
return this.redirectsService.fetchRedirects(siteName);
|
|
134
212
|
});
|
|
135
213
|
}
|
|
136
214
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* @
|
|
141
|
-
* @
|
|
215
|
+
* When a user clicks on a link generated by the Link component from next/link,
|
|
216
|
+
* Next.js adds special parameters in the route called path.
|
|
217
|
+
* This method removes these special parameters.
|
|
218
|
+
* @param {NextURL} url
|
|
219
|
+
* @returns {string} normalize url
|
|
142
220
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
221
|
+
normalizeUrl(url) {
|
|
222
|
+
if (!url.search)
|
|
223
|
+
return url;
|
|
224
|
+
/**
|
|
225
|
+
* Prepare special parameters for exclusion.
|
|
226
|
+
*/
|
|
227
|
+
const splittedPathname = url.pathname
|
|
228
|
+
.split('/')
|
|
229
|
+
.filter((route) => route)
|
|
230
|
+
.map((route) => `path=${route}`);
|
|
231
|
+
/**
|
|
232
|
+
* Remove special parameters(Next.JS)
|
|
233
|
+
* Example: /about/contact/us
|
|
234
|
+
* When a user clicks on this link, Next.js should generate a link for the middleware, formatted like this:
|
|
235
|
+
* http://host/about/contact/us?path=about&path=contact&path=us
|
|
236
|
+
*/
|
|
237
|
+
const newQueryString = url.search
|
|
238
|
+
.replace(/^\?/, '')
|
|
239
|
+
.split('&')
|
|
240
|
+
.filter((param) => !splittedPathname.includes(param))
|
|
241
|
+
.join('&');
|
|
242
|
+
const newUrl = new URL(`${url.pathname.toLowerCase()}?${newQueryString}`, url.origin);
|
|
243
|
+
url.search = newUrl.search;
|
|
244
|
+
url.pathname = newUrl.pathname.toLowerCase();
|
|
245
|
+
url.href = newUrl.href;
|
|
246
|
+
return url;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Dispatch a redirect or rewrite based on type.
|
|
250
|
+
* @param {NextURL | string} target Final target to redirect/rewrite to (NextURL or string for externals).
|
|
251
|
+
* @param {string} type One of `REDIRECT_TYPE_301`, `REDIRECT_TYPE_302`, or `REDIRECT_TYPE_SERVER_TRANSFER`.
|
|
252
|
+
* @param {NextRequest} req Incoming request.
|
|
253
|
+
* @param {NextResponse} res Current response (used for header cleanup/carry-over).
|
|
254
|
+
* @param {boolean} isExternal Set to `true` when target is an external absolute URL.
|
|
255
|
+
* @returns A NextResponse.
|
|
256
|
+
*/
|
|
257
|
+
dispatchRedirect(target, type, req, res, isExternal = false) {
|
|
258
|
+
switch (type) {
|
|
259
|
+
case REDIRECT_TYPE_301:
|
|
260
|
+
return this.createRedirectResponse(target, res, 301, 'Moved Permanently');
|
|
261
|
+
case REDIRECT_TYPE_302:
|
|
262
|
+
return this.createRedirectResponse(target, res, 302, 'Found');
|
|
263
|
+
case REDIRECT_TYPE_SERVER_TRANSFER:
|
|
264
|
+
// rewrite expects a string; unwrap NextURL if needed
|
|
265
|
+
return this.rewrite(typeof target === 'string' ? target : target.href, req, res, isExternal);
|
|
266
|
+
default:
|
|
267
|
+
// Unknown type: return the input response unchanged
|
|
268
|
+
return res;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Helper function to create a redirect response and remove the x-middleware-next header.
|
|
273
|
+
* @param {NextURL} url The URL to redirect to.
|
|
274
|
+
* @param {Response} res The response object.
|
|
275
|
+
* @param {number} status The HTTP status code of the redirect.
|
|
276
|
+
* @param {string} statusText The status text of the redirect.
|
|
277
|
+
* @returns {NextResponse<unknown>} The redirect response.
|
|
278
|
+
*/
|
|
279
|
+
createRedirectResponse(url, res, status, statusText) {
|
|
280
|
+
const redirect = NextResponse.redirect(url, {
|
|
281
|
+
status,
|
|
282
|
+
statusText,
|
|
283
|
+
headers: res === null || res === void 0 ? void 0 : res.headers,
|
|
168
284
|
});
|
|
285
|
+
if (res === null || res === void 0 ? void 0 : res.headers) {
|
|
286
|
+
redirect.headers.delete('x-middleware-next');
|
|
287
|
+
redirect.headers.delete('x-middleware-rewrite');
|
|
288
|
+
redirect.headers.delete(REWRITE_HEADER_NAME);
|
|
289
|
+
}
|
|
290
|
+
return redirect;
|
|
169
291
|
}
|
|
170
292
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitecore-jss/sitecore-jss-nextjs",
|
|
3
|
-
"version": "21.10.
|
|
3
|
+
"version": "21.10.1",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"module": "dist/esm/index.js",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
"react-dom": "^19.1.0"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@sitecore-jss/sitecore-jss": "21.10.
|
|
73
|
-
"@sitecore-jss/sitecore-jss-dev-tools": "21.10.
|
|
74
|
-
"@sitecore-jss/sitecore-jss-react": "21.10.
|
|
72
|
+
"@sitecore-jss/sitecore-jss": "21.10.1",
|
|
73
|
+
"@sitecore-jss/sitecore-jss-dev-tools": "21.10.1",
|
|
74
|
+
"@sitecore-jss/sitecore-jss-react": "21.10.1",
|
|
75
75
|
"@vercel/kv": "^0.2.1",
|
|
76
76
|
"node-html-parser": "^6.1.4",
|
|
77
77
|
"regex-parser": "^2.2.11",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"description": "",
|
|
81
81
|
"types": "types/index.d.ts",
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "aaeb09bfb4b3e9a40f2799342feebfaa4cf5f619",
|
|
83
83
|
"files": [
|
|
84
84
|
"dist",
|
|
85
85
|
"types",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SiteInfo, SiteResolver } from '@sitecore-jss/sitecore-jss/site';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
export declare const REWRITE_HEADER_NAME = "x-sc-rewrite";
|
|
3
4
|
export type MiddlewareBaseConfig = {
|
|
4
5
|
/**
|
|
5
6
|
* function, determines if middleware should be turned off, based on cookie, header, or other considerations
|
|
@@ -47,6 +48,12 @@ export declare abstract class MiddlewareBase {
|
|
|
47
48
|
protected extractDebugHeaders(incomingHeaders: Headers): {
|
|
48
49
|
[key: string]: string;
|
|
49
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Determines if the request is a Next.js (next/link) prefetch request
|
|
53
|
+
* @param {NextRequest} req request
|
|
54
|
+
* @returns {boolean} is prefetch
|
|
55
|
+
*/
|
|
56
|
+
protected isPrefetch(req: NextRequest): boolean;
|
|
50
57
|
/**
|
|
51
58
|
* Provides used language
|
|
52
59
|
* @param {NextRequest} req request
|
|
@@ -71,6 +78,7 @@ export declare abstract class MiddlewareBase {
|
|
|
71
78
|
* @param {string} rewritePath the destionation path
|
|
72
79
|
* @param {NextRequest} req the current request
|
|
73
80
|
* @param {NextResponse} res the current response
|
|
81
|
+
* @param {boolean} [skipHeader] don't write 'x-sc-rewrite' header
|
|
74
82
|
*/
|
|
75
|
-
protected rewrite(rewritePath: string, req: NextRequest, res: NextResponse): NextResponse;
|
|
83
|
+
protected rewrite(rewritePath: string, req: NextRequest, res: NextResponse, skipHeader?: boolean): NextResponse;
|
|
76
84
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import { GraphQLRedirectsServiceConfig } from '@sitecore-jss/sitecore-jss/site';
|
|
2
|
+
import { RedirectInfo, GraphQLRedirectsServiceConfig } from '@sitecore-jss/sitecore-jss/site';
|
|
3
3
|
import { MiddlewareBase, MiddlewareBaseConfig } from './middleware';
|
|
4
|
+
type RedirectResult = RedirectInfo & {
|
|
5
|
+
matchedQueryString?: string;
|
|
6
|
+
};
|
|
4
7
|
/**
|
|
5
8
|
* extended RedirectsMiddlewareConfig config type for RedirectsMiddleware
|
|
6
9
|
*/
|
|
@@ -19,22 +22,59 @@ export declare class RedirectsMiddleware extends MiddlewareBase {
|
|
|
19
22
|
protected config: RedirectsMiddlewareConfig;
|
|
20
23
|
private redirectsService;
|
|
21
24
|
private locales;
|
|
22
|
-
/**
|
|
23
|
-
* @param {RedirectsMiddlewareConfig} [config] redirects middleware config
|
|
24
|
-
*/
|
|
25
25
|
constructor(config: RedirectsMiddlewareConfig);
|
|
26
26
|
/**
|
|
27
27
|
* Gets the Next.js middleware handler with error handling
|
|
28
28
|
* @returns route handler
|
|
29
29
|
*/
|
|
30
30
|
getHandler(): (req: NextRequest, res?: NextResponse) => Promise<NextResponse>;
|
|
31
|
-
private handler;
|
|
32
31
|
/**
|
|
33
32
|
* Method returns RedirectInfo when matches
|
|
34
33
|
* @param {NextRequest} req request
|
|
35
34
|
* @param {string} siteName site name
|
|
36
|
-
* @returns Promise<RedirectInfo | undefined>
|
|
37
|
-
* @
|
|
35
|
+
* @returns Promise<RedirectInfo | undefined> The redirect info or undefined if no redirect is found
|
|
36
|
+
* @protected
|
|
37
|
+
*/
|
|
38
|
+
protected getExistsRedirect(req: NextRequest, siteName: string): Promise<RedirectResult | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* @param {NextRequest} req request
|
|
41
|
+
* @param {Response} res response
|
|
42
|
+
* @returns {Promise<NextResponse>} The redirect response.
|
|
43
|
+
*/
|
|
44
|
+
protected processRedirectRequest(req: NextRequest, res?: NextResponse): Promise<NextResponse>;
|
|
45
|
+
/**
|
|
46
|
+
* Fetches all redirects for a given site from the Sitecore instance
|
|
47
|
+
* @param {string} siteName - The name of the site to fetch redirects for
|
|
48
|
+
* @returns {Promise<RedirectInfo[]>} A promise that resolves to an array of redirect information
|
|
49
|
+
* @protected
|
|
50
|
+
*/
|
|
51
|
+
protected getRedirects(siteName: string): Promise<RedirectInfo[]>;
|
|
52
|
+
/**
|
|
53
|
+
* When a user clicks on a link generated by the Link component from next/link,
|
|
54
|
+
* Next.js adds special parameters in the route called path.
|
|
55
|
+
* This method removes these special parameters.
|
|
56
|
+
* @param {NextURL} url
|
|
57
|
+
* @returns {string} normalize url
|
|
58
|
+
*/
|
|
59
|
+
private normalizeUrl;
|
|
60
|
+
/**
|
|
61
|
+
* Dispatch a redirect or rewrite based on type.
|
|
62
|
+
* @param {NextURL | string} target Final target to redirect/rewrite to (NextURL or string for externals).
|
|
63
|
+
* @param {string} type One of `REDIRECT_TYPE_301`, `REDIRECT_TYPE_302`, or `REDIRECT_TYPE_SERVER_TRANSFER`.
|
|
64
|
+
* @param {NextRequest} req Incoming request.
|
|
65
|
+
* @param {NextResponse} res Current response (used for header cleanup/carry-over).
|
|
66
|
+
* @param {boolean} isExternal Set to `true` when target is an external absolute URL.
|
|
67
|
+
* @returns A NextResponse.
|
|
68
|
+
*/
|
|
69
|
+
private dispatchRedirect;
|
|
70
|
+
/**
|
|
71
|
+
* Helper function to create a redirect response and remove the x-middleware-next header.
|
|
72
|
+
* @param {NextURL} url The URL to redirect to.
|
|
73
|
+
* @param {Response} res The response object.
|
|
74
|
+
* @param {number} status The HTTP status code of the redirect.
|
|
75
|
+
* @param {string} statusText The status text of the redirect.
|
|
76
|
+
* @returns {NextResponse<unknown>} The redirect response.
|
|
38
77
|
*/
|
|
39
|
-
private
|
|
78
|
+
private createRedirectResponse;
|
|
40
79
|
}
|
|
80
|
+
export {};
|