@sitecore-content-sdk/nextjs 0.1.0-beta.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/LICENSE.txt +202 -0
- package/README.md +10 -0
- package/dist/cjs/ComponentBuilder.js +63 -0
- package/dist/cjs/components/BYOCWrapper.js +41 -0
- package/dist/cjs/components/ComponentPropsContext.js +57 -0
- package/dist/cjs/components/FEaaSWrapper.js +43 -0
- package/dist/cjs/components/Link.js +87 -0
- package/dist/cjs/components/NextImage.js +82 -0
- package/dist/cjs/components/Placeholder.js +49 -0
- package/dist/cjs/components/RichText.js +95 -0
- package/dist/cjs/editing/constants.js +10 -0
- package/dist/cjs/editing/editing-config-middleware.js +62 -0
- package/dist/cjs/editing/editing-render-middleware.js +182 -0
- package/dist/cjs/editing/feaas-render-middleware.js +101 -0
- package/dist/cjs/editing/index.js +16 -0
- package/dist/cjs/editing/render-middleware.js +43 -0
- package/dist/cjs/graphql/index.js +7 -0
- package/dist/cjs/index.js +119 -0
- package/dist/cjs/middleware/index.js +13 -0
- package/dist/cjs/middleware/middleware.js +97 -0
- package/dist/cjs/middleware/multisite-middleware.js +93 -0
- package/dist/cjs/middleware/personalize-middleware.js +231 -0
- package/dist/cjs/middleware/redirects-middleware.js +264 -0
- package/dist/cjs/monitoring/healthcheck-middleware.js +30 -0
- package/dist/cjs/monitoring/index.js +5 -0
- package/dist/cjs/services/base-graphql-sitemap-service.js +206 -0
- package/dist/cjs/services/component-props-service.js +167 -0
- package/dist/cjs/services/graphql-sitemap-service.js +64 -0
- package/dist/cjs/services/mutisite-graphql-sitemap-service.js +81 -0
- package/dist/cjs/sharedTypes/component-props.js +2 -0
- package/dist/cjs/sharedTypes/module-factory.js +2 -0
- package/dist/cjs/site/index.js +5 -0
- package/dist/cjs/utils/index.js +11 -0
- package/dist/cjs/utils/utils.js +42 -0
- package/dist/esm/ComponentBuilder.js +59 -0
- package/dist/esm/components/BYOCWrapper.js +36 -0
- package/dist/esm/components/ComponentPropsContext.js +19 -0
- package/dist/esm/components/FEaaSWrapper.js +38 -0
- package/dist/esm/components/Link.js +48 -0
- package/dist/esm/components/NextImage.js +76 -0
- package/dist/esm/components/Placeholder.js +12 -0
- package/dist/esm/components/RichText.js +55 -0
- package/dist/esm/editing/constants.js +7 -0
- package/dist/esm/editing/editing-config-middleware.js +58 -0
- package/dist/esm/editing/editing-render-middleware.js +177 -0
- package/dist/esm/editing/feaas-render-middleware.js +97 -0
- package/dist/esm/editing/index.js +5 -0
- package/dist/esm/editing/render-middleware.js +39 -0
- package/dist/esm/graphql/index.js +1 -0
- package/dist/esm/index.js +23 -0
- package/dist/esm/middleware/index.js +5 -0
- package/dist/esm/middleware/middleware.js +93 -0
- package/dist/esm/middleware/multisite-middleware.js +89 -0
- package/dist/esm/middleware/personalize-middleware.js +227 -0
- package/dist/esm/middleware/redirects-middleware.js +257 -0
- package/dist/esm/monitoring/healthcheck-middleware.js +26 -0
- package/dist/esm/monitoring/index.js +1 -0
- package/dist/esm/services/base-graphql-sitemap-service.js +201 -0
- package/dist/esm/services/component-props-service.js +160 -0
- package/dist/esm/services/graphql-sitemap-service.js +59 -0
- package/dist/esm/services/mutisite-graphql-sitemap-service.js +77 -0
- package/dist/esm/sharedTypes/component-props.js +1 -0
- package/dist/esm/sharedTypes/module-factory.js +1 -0
- package/dist/esm/site/index.js +1 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/esm/utils/utils.js +37 -0
- package/editing.d.ts +1 -0
- package/editing.js +1 -0
- package/global.d.ts +21 -0
- package/graphql.d.ts +1 -0
- package/graphql.js +1 -0
- package/middleware.d.ts +1 -0
- package/middleware.js +1 -0
- package/monitoring.d.ts +1 -0
- package/monitoring.js +1 -0
- package/package.json +92 -0
- package/site.d.ts +1 -0
- package/site.js +1 -0
- package/types/ComponentBuilder.d.ts +59 -0
- package/types/components/BYOCWrapper.d.ts +20 -0
- package/types/components/ComponentPropsContext.d.ts +18 -0
- package/types/components/FEaaSWrapper.d.ts +22 -0
- package/types/components/Link.d.ts +10 -0
- package/types/components/NextImage.d.ts +6 -0
- package/types/components/Placeholder.d.ts +8 -0
- package/types/components/RichText.d.ts +32 -0
- package/types/editing/constants.d.ts +7 -0
- package/types/editing/editing-config-middleware.d.ts +29 -0
- package/types/editing/editing-render-middleware.d.ts +79 -0
- package/types/editing/feaas-render-middleware.d.ts +32 -0
- package/types/editing/index.d.ts +5 -0
- package/types/editing/render-middleware.d.ts +24 -0
- package/types/graphql/index.d.ts +1 -0
- package/types/index.d.ts +24 -0
- package/types/middleware/index.d.ts +5 -0
- package/types/middleware/middleware.d.ts +82 -0
- package/types/middleware/multisite-middleware.d.ts +39 -0
- package/types/middleware/personalize-middleware.d.ts +102 -0
- package/types/middleware/redirects-middleware.d.ts +57 -0
- package/types/monitoring/healthcheck-middleware.d.ts +12 -0
- package/types/monitoring/index.d.ts +1 -0
- package/types/services/base-graphql-sitemap-service.d.ts +148 -0
- package/types/services/component-props-service.d.ts +81 -0
- package/types/services/graphql-sitemap-service.d.ts +51 -0
- package/types/services/mutisite-graphql-sitemap-service.d.ts +42 -0
- package/types/sharedTypes/component-props.d.ts +26 -0
- package/types/sharedTypes/module-factory.d.ts +32 -0
- package/types/site/index.d.ts +1 -0
- package/types/utils/index.d.ts +3 -0
- package/types/utils/utils.d.ts +8 -0
- package/utils.d.ts +1 -0
- package/utils.js +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { NextResponse } from 'next/server';
|
|
11
|
+
import { GraphQLPersonalizeService, getPersonalizedRewrite, CdpHelper, DEFAULT_VARIANT, } from '@sitecore-content-sdk/core/personalize';
|
|
12
|
+
import { debug } from '@sitecore-content-sdk/core';
|
|
13
|
+
import { MiddlewareBase } from './middleware';
|
|
14
|
+
import { CloudSDK } from '@sitecore-cloudsdk/core/server';
|
|
15
|
+
import { personalize } from '@sitecore-cloudsdk/personalize/server';
|
|
16
|
+
/**
|
|
17
|
+
* Middleware / handler to support Sitecore Personalize
|
|
18
|
+
*/
|
|
19
|
+
export class PersonalizeMiddleware extends MiddlewareBase {
|
|
20
|
+
/**
|
|
21
|
+
* @param {PersonalizeMiddlewareConfig} [config] Personalize middleware config
|
|
22
|
+
*/
|
|
23
|
+
constructor(config) {
|
|
24
|
+
super(config);
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.handler = (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const pathname = req.nextUrl.pathname;
|
|
28
|
+
const language = this.getLanguage(req);
|
|
29
|
+
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
30
|
+
const startTimestamp = Date.now();
|
|
31
|
+
const timeout = this.config.cdpConfig.timeout;
|
|
32
|
+
// Response will be provided if other middleware is run before us (e.g. redirects)
|
|
33
|
+
let response = res || NextResponse.next();
|
|
34
|
+
debug.personalize('personalize middleware start: %o', {
|
|
35
|
+
pathname,
|
|
36
|
+
language,
|
|
37
|
+
hostname,
|
|
38
|
+
headers: this.extractDebugHeaders(req.headers),
|
|
39
|
+
});
|
|
40
|
+
if (this.config.disabled && this.config.disabled(req, response)) {
|
|
41
|
+
debug.personalize('skipped (personalize middleware is disabled)');
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
if (response.redirected || // Don't attempt to personalize a redirect
|
|
45
|
+
this.isPreview(req) || // No need to personalize for preview (layout data is already prepared for preview)
|
|
46
|
+
this.excludeRoute(pathname)) {
|
|
47
|
+
debug.personalize('skipped (%s)', response.redirected ? 'redirected' : this.isPreview(req) ? 'preview' : 'route excluded');
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
const site = this.getSite(req, response);
|
|
51
|
+
// Get personalization info from Experience Edge
|
|
52
|
+
const personalizeInfo = yield this.personalizeService.getPersonalizeInfo(pathname, language, site.name);
|
|
53
|
+
if (!personalizeInfo) {
|
|
54
|
+
// Likely an invalid route / language
|
|
55
|
+
debug.personalize('skipped (personalize info not found)');
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
if (personalizeInfo.variantIds.length === 0) {
|
|
59
|
+
debug.personalize('skipped (no personalization configured)');
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
if (this.isPrefetch(req)) {
|
|
63
|
+
debug.personalize('skipped (prefetch)');
|
|
64
|
+
// Personalized, but this is a prefetch request.
|
|
65
|
+
// In this case, don't execute a personalize request; otherwise, the metrics for component A/B experiments would be inaccurate.
|
|
66
|
+
// Disable preflight caching to force revalidation on client-side navigation (personalization WILL be influenced).
|
|
67
|
+
// Note the reason we don't move this any earlier in the middleware is that we would then be sacrificing performance for non-personalized pages.
|
|
68
|
+
response.headers.set('x-middleware-cache', 'no-cache');
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
yield this.initPersonalizeServer({
|
|
72
|
+
hostname,
|
|
73
|
+
siteName: site.name,
|
|
74
|
+
request: req,
|
|
75
|
+
response,
|
|
76
|
+
});
|
|
77
|
+
const params = this.getExperienceParams(req);
|
|
78
|
+
const executions = this.getPersonalizeExecutions(personalizeInfo, language);
|
|
79
|
+
const identifiedVariantIds = [];
|
|
80
|
+
yield Promise.all(executions.map((execution) => this.personalize({
|
|
81
|
+
friendlyId: execution.friendlyId,
|
|
82
|
+
variantIds: execution.variantIds,
|
|
83
|
+
params,
|
|
84
|
+
language,
|
|
85
|
+
timeout,
|
|
86
|
+
}, req).then((personalization) => {
|
|
87
|
+
const variantId = personalization.variantId;
|
|
88
|
+
if (variantId) {
|
|
89
|
+
if (!execution.variantIds.includes(variantId)) {
|
|
90
|
+
debug.personalize('invalid variant %s', variantId);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
identifiedVariantIds.push(variantId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})));
|
|
97
|
+
if (identifiedVariantIds.length === 0) {
|
|
98
|
+
debug.personalize('skipped (no variant(s) identified)');
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
// Path can be rewritten by previously executed middleware
|
|
102
|
+
const basePath = (res === null || res === void 0 ? void 0 : res.headers.get('x-sc-rewrite')) || pathname;
|
|
103
|
+
// Rewrite to persononalized path
|
|
104
|
+
const rewritePath = getPersonalizedRewrite(basePath, identifiedVariantIds);
|
|
105
|
+
response = this.rewrite(rewritePath, req, response);
|
|
106
|
+
// Disable preflight caching to force revalidation on client-side navigation (personalization MAY be influenced).
|
|
107
|
+
// See https://github.com/vercel/next.js/pull/32767
|
|
108
|
+
response.headers.set('x-middleware-cache', 'no-cache');
|
|
109
|
+
debug.personalize('personalize middleware end in %dms: %o', Date.now() - startTimestamp, {
|
|
110
|
+
rewritePath,
|
|
111
|
+
headers: this.extractDebugHeaders(response.headers),
|
|
112
|
+
});
|
|
113
|
+
return response;
|
|
114
|
+
});
|
|
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.personalizeService = new GraphQLPersonalizeService(Object.assign(Object.assign({}, config.edgeConfig), { fetch: fetch }));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Gets the Next.js middleware handler with error handling
|
|
121
|
+
* @returns middleware handler
|
|
122
|
+
*/
|
|
123
|
+
getHandler() {
|
|
124
|
+
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
try {
|
|
126
|
+
return yield this.handler(req, res);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.log('Personalize middleware failed:');
|
|
130
|
+
console.log(error);
|
|
131
|
+
return res || NextResponse.next();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
initPersonalizeServer(_a) {
|
|
136
|
+
return __awaiter(this, arguments, void 0, function* ({ hostname, siteName, request, response, }) {
|
|
137
|
+
yield CloudSDK(request, response, {
|
|
138
|
+
sitecoreEdgeUrl: this.config.cdpConfig.sitecoreEdgeUrl,
|
|
139
|
+
sitecoreEdgeContextId: this.config.cdpConfig.sitecoreEdgeContextId,
|
|
140
|
+
siteName,
|
|
141
|
+
cookieDomain: hostname,
|
|
142
|
+
enableServerCookie: true,
|
|
143
|
+
})
|
|
144
|
+
.addPersonalize({ enablePersonalizeCookie: true })
|
|
145
|
+
.initialize();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
personalize(_a, request_1) {
|
|
149
|
+
return __awaiter(this, arguments, void 0, function* ({ params, friendlyId, language, timeout, variantIds, }, request) {
|
|
150
|
+
var _b;
|
|
151
|
+
debug.personalize('executing experience for %s %o', friendlyId, params);
|
|
152
|
+
return (yield personalize(request, {
|
|
153
|
+
channel: this.config.cdpConfig.channel || 'WEB',
|
|
154
|
+
currency: (_b = this.config.cdpConfig.currency) !== null && _b !== void 0 ? _b : 'USD',
|
|
155
|
+
friendlyId,
|
|
156
|
+
params,
|
|
157
|
+
language,
|
|
158
|
+
pageVariantIds: variantIds,
|
|
159
|
+
}, { timeout }));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
getExperienceParams(req) {
|
|
163
|
+
const utm = {
|
|
164
|
+
campaign: req.nextUrl.searchParams.get('utm_campaign') || undefined,
|
|
165
|
+
content: req.nextUrl.searchParams.get('utm_content') || undefined,
|
|
166
|
+
medium: req.nextUrl.searchParams.get('utm_medium') || undefined,
|
|
167
|
+
source: req.nextUrl.searchParams.get('utm_source') || undefined,
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
// It's expected that the header name "referer" is actually a misspelling of the word "referrer"
|
|
171
|
+
// req.referrer is used during fetching to determine the value of the Referer header of the request being made,
|
|
172
|
+
// used as a fallback
|
|
173
|
+
referrer: req.headers.get('referer') || req.referrer,
|
|
174
|
+
utm: utm,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
excludeRoute(pathname) {
|
|
178
|
+
// ignore files
|
|
179
|
+
return pathname.includes('.') || super.excludeRoute(pathname);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Aggregates personalize executions based on the provided route personalize information and language
|
|
183
|
+
* @param {PersonalizeInfo} personalizeInfo the route personalize information
|
|
184
|
+
* @param {string} language the language
|
|
185
|
+
* @returns An array of personalize executions
|
|
186
|
+
*/
|
|
187
|
+
getPersonalizeExecutions(personalizeInfo, language) {
|
|
188
|
+
if (personalizeInfo.variantIds.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const results = [];
|
|
192
|
+
return personalizeInfo.variantIds.reduce((results, variantId) => {
|
|
193
|
+
if (variantId.includes('_')) {
|
|
194
|
+
// Component-level personalization in format "<ComponentID>_<VariantID>"
|
|
195
|
+
const componentId = variantId.split('_')[0];
|
|
196
|
+
const friendlyId = CdpHelper.getComponentFriendlyId(personalizeInfo.pageId, componentId, language, this.config.scope || this.config.edgeConfig.scope);
|
|
197
|
+
const execution = results.find((x) => x.friendlyId === friendlyId);
|
|
198
|
+
if (execution) {
|
|
199
|
+
execution.variantIds.push(variantId);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// The default/control variant (format "<ComponentID>_default") is also a valid value returned by the execution
|
|
203
|
+
const defaultVariant = `${componentId}${DEFAULT_VARIANT}`;
|
|
204
|
+
results.push({
|
|
205
|
+
friendlyId,
|
|
206
|
+
variantIds: [defaultVariant, variantId],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Embedded (page-level) personalization in format "<VariantID>"
|
|
212
|
+
const friendlyId = CdpHelper.getPageFriendlyId(personalizeInfo.pageId, language, this.config.scope || this.config.edgeConfig.scope);
|
|
213
|
+
const execution = results.find((x) => x.friendlyId === friendlyId);
|
|
214
|
+
if (execution) {
|
|
215
|
+
execution.variantIds.push(variantId);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
results.push({
|
|
219
|
+
friendlyId,
|
|
220
|
+
variantIds: [variantId],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return results;
|
|
225
|
+
}, results);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { debug } from '@sitecore-content-sdk/core';
|
|
11
|
+
import { GraphQLRedirectsService, REDIRECT_TYPE_301, REDIRECT_TYPE_302, REDIRECT_TYPE_SERVER_TRANSFER, } from '@sitecore-content-sdk/core/site';
|
|
12
|
+
import { areURLSearchParamsEqual, escapeNonSpecialQuestionMarks, isRegexOrUrl, mergeURLSearchParams, } from '@sitecore-content-sdk/core/utils';
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
import regexParser from 'regex-parser';
|
|
15
|
+
import { MiddlewareBase } from './middleware';
|
|
16
|
+
const REGEXP_CONTEXT_SITE_LANG = new RegExp(/\$siteLang/, 'i');
|
|
17
|
+
const REGEXP_ABSOLUTE_URL = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
18
|
+
/**
|
|
19
|
+
* Middleware / handler fetches all redirects from Sitecore instance by grapqhl service
|
|
20
|
+
* compares with current url and redirects to target url
|
|
21
|
+
*/
|
|
22
|
+
export class RedirectsMiddleware extends MiddlewareBase {
|
|
23
|
+
/**
|
|
24
|
+
* @param {RedirectsMiddlewareConfig} [config] redirects middleware config
|
|
25
|
+
*/
|
|
26
|
+
constructor(config) {
|
|
27
|
+
super(config);
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.handler = (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
const pathname = req.nextUrl.pathname;
|
|
31
|
+
const language = this.getLanguage(req);
|
|
32
|
+
const hostname = this.getHostHeader(req) || this.defaultHostname;
|
|
33
|
+
let site;
|
|
34
|
+
const startTimestamp = Date.now();
|
|
35
|
+
debug.redirects('redirects middleware start: %o', {
|
|
36
|
+
pathname,
|
|
37
|
+
language,
|
|
38
|
+
hostname,
|
|
39
|
+
});
|
|
40
|
+
const createResponse = () => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
var _a;
|
|
42
|
+
const response = res || NextResponse.next();
|
|
43
|
+
if (this.config.disabled && this.config.disabled(req, res || NextResponse.next())) {
|
|
44
|
+
debug.redirects('skipped (redirects middleware is disabled)');
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
if (this.isPreview(req) || this.excludeRoute(pathname)) {
|
|
48
|
+
debug.redirects('skipped (%s)', this.isPreview(req) ? 'preview' : 'route excluded');
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
// Skip prefetch requests from Next.js, which are not original client requests
|
|
52
|
+
// as they load unnecessary requests that burden the redirects middleware with meaningless traffic
|
|
53
|
+
if (this.isPrefetch(req)) {
|
|
54
|
+
debug.redirects('skipped (prefetch)');
|
|
55
|
+
response.headers.set('x-middleware-cache', 'no-cache');
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
site = this.getSite(req, res);
|
|
59
|
+
// Find the redirect from result of RedirectService
|
|
60
|
+
const existsRedirect = yield this.getExistsRedirect(req, site.name);
|
|
61
|
+
if (!existsRedirect) {
|
|
62
|
+
debug.redirects('skipped (redirect does not exist)');
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
// Find context site language and replace token
|
|
66
|
+
if (REGEXP_CONTEXT_SITE_LANG.test(existsRedirect.target) &&
|
|
67
|
+
!(REGEXP_ABSOLUTE_URL.test(existsRedirect.target) &&
|
|
68
|
+
existsRedirect.target.includes(hostname))) {
|
|
69
|
+
existsRedirect.target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, site.language);
|
|
70
|
+
req.nextUrl.locale = site.language;
|
|
71
|
+
}
|
|
72
|
+
const url = this.normalizeUrl(req.nextUrl.clone());
|
|
73
|
+
if (REGEXP_ABSOLUTE_URL.test(existsRedirect.target)) {
|
|
74
|
+
url.href = existsRedirect.target;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const isUrl = isRegexOrUrl(existsRedirect.pattern) === 'url';
|
|
78
|
+
const targetParts = existsRedirect.target.split('/');
|
|
79
|
+
const urlFirstPart = targetParts[1];
|
|
80
|
+
if (this.locales.includes(urlFirstPart)) {
|
|
81
|
+
req.nextUrl.locale = urlFirstPart;
|
|
82
|
+
existsRedirect.target = existsRedirect.target.replace(`/${urlFirstPart}`, '');
|
|
83
|
+
}
|
|
84
|
+
const targetSegments = isUrl
|
|
85
|
+
? existsRedirect.target.split('?')
|
|
86
|
+
: url.pathname.replace(/\/*$/gi, '') + existsRedirect.matchedQueryString;
|
|
87
|
+
const [targetPath, targetQueryString] = isUrl
|
|
88
|
+
? targetSegments
|
|
89
|
+
: targetSegments
|
|
90
|
+
.replace(regexParser(existsRedirect.pattern), existsRedirect.target)
|
|
91
|
+
.replace(/^\/\//, '/')
|
|
92
|
+
.split('?');
|
|
93
|
+
const mergedQueryString = existsRedirect.isQueryStringPreserved
|
|
94
|
+
? mergeURLSearchParams(new URLSearchParams((_a = url.search) !== null && _a !== void 0 ? _a : ''), new URLSearchParams(targetQueryString || ''))
|
|
95
|
+
: targetQueryString || '';
|
|
96
|
+
const prepareNewURL = new URL(`${targetPath}${mergedQueryString ? '?' + mergedQueryString : ''}`, url.origin);
|
|
97
|
+
url.href = prepareNewURL.href;
|
|
98
|
+
url.pathname = prepareNewURL.pathname;
|
|
99
|
+
url.search = prepareNewURL.search;
|
|
100
|
+
url.locale = req.nextUrl.locale;
|
|
101
|
+
}
|
|
102
|
+
/** return Response redirect with http code of redirect type */
|
|
103
|
+
switch (existsRedirect.redirectType) {
|
|
104
|
+
case REDIRECT_TYPE_301: {
|
|
105
|
+
return this.createRedirectResponse(url, response, 301, 'Moved Permanently');
|
|
106
|
+
}
|
|
107
|
+
case REDIRECT_TYPE_302: {
|
|
108
|
+
return this.createRedirectResponse(url, response, 302, 'Found');
|
|
109
|
+
}
|
|
110
|
+
case REDIRECT_TYPE_SERVER_TRANSFER: {
|
|
111
|
+
return this.rewrite(url.href, req, response);
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const response = yield createResponse();
|
|
118
|
+
debug.redirects('redirects middleware end in %dms: %o', Date.now() - startTimestamp, {
|
|
119
|
+
redirected: response.redirected,
|
|
120
|
+
status: response.status,
|
|
121
|
+
url: response.url,
|
|
122
|
+
headers: this.extractDebugHeaders(response.headers),
|
|
123
|
+
});
|
|
124
|
+
return response;
|
|
125
|
+
});
|
|
126
|
+
// NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
|
|
127
|
+
// (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
|
|
128
|
+
this.redirectsService = new GraphQLRedirectsService(Object.assign(Object.assign({}, config), { fetch: fetch }));
|
|
129
|
+
this.locales = config.locales;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Gets the Next.js middleware handler with error handling
|
|
133
|
+
* @returns route handler
|
|
134
|
+
*/
|
|
135
|
+
getHandler() {
|
|
136
|
+
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
try {
|
|
138
|
+
return yield this.handler(req, res);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.log('Redirect middleware failed:');
|
|
142
|
+
console.log(error);
|
|
143
|
+
return res || NextResponse.next();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Method returns RedirectInfo when matches
|
|
149
|
+
* @param {NextRequest} req request
|
|
150
|
+
* @param {string} siteName site name
|
|
151
|
+
* @returns Promise<RedirectInfo | undefined>
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
getExistsRedirect(req, siteName) {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
const { pathname: targetURL, search: targetQS = '', locale } = this.normalizeUrl(req.nextUrl.clone());
|
|
157
|
+
const normalizedPath = targetURL.replace(/\/*$/gi, '');
|
|
158
|
+
const redirects = yield this.redirectsService.fetchRedirects(siteName);
|
|
159
|
+
const language = this.getLanguage(req);
|
|
160
|
+
const modifyRedirects = structuredClone(redirects);
|
|
161
|
+
let matchedQueryString;
|
|
162
|
+
return modifyRedirects.length
|
|
163
|
+
? modifyRedirects.find((redirect) => {
|
|
164
|
+
var _a;
|
|
165
|
+
if (isRegexOrUrl(redirect.pattern) === 'url') {
|
|
166
|
+
const parseUrlPattern = redirect.pattern.endsWith('/')
|
|
167
|
+
? redirect.pattern.slice(0, -1).split('?')
|
|
168
|
+
: redirect.pattern.split('?');
|
|
169
|
+
return ((parseUrlPattern[0] === normalizedPath ||
|
|
170
|
+
parseUrlPattern[0] === `/${locale}${normalizedPath}`) &&
|
|
171
|
+
areURLSearchParamsEqual(new URLSearchParams((_a = parseUrlPattern[1]) !== null && _a !== void 0 ? _a : ''), new URLSearchParams(targetQS)));
|
|
172
|
+
}
|
|
173
|
+
// Modify the redirect pattern to ignore the language prefix in the path
|
|
174
|
+
// And escapes non-special "?" characters in a string or regex.
|
|
175
|
+
redirect.pattern = escapeNonSpecialQuestionMarks(redirect.pattern.replace(new RegExp(`^[^]?/${language}/`, 'gi'), ''));
|
|
176
|
+
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
|
|
177
|
+
redirect.pattern = `/^\/${redirect.pattern
|
|
178
|
+
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
|
|
179
|
+
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
|
|
180
|
+
.replace(/^\^|\$$/g, '') // Further cleans up anchors
|
|
181
|
+
.replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
|
|
182
|
+
matchedQueryString = [
|
|
183
|
+
regexParser(redirect.pattern).test(`${normalizedPath}${targetQS}`),
|
|
184
|
+
regexParser(redirect.pattern).test(`/${locale}${normalizedPath}${targetQS}`),
|
|
185
|
+
].some(Boolean)
|
|
186
|
+
? targetQS
|
|
187
|
+
: undefined;
|
|
188
|
+
// Save the matched query string (if found) into the redirect object
|
|
189
|
+
redirect.matchedQueryString = matchedQueryString || '';
|
|
190
|
+
return (!!(regexParser(redirect.pattern).test(targetURL) ||
|
|
191
|
+
regexParser(redirect.pattern).test(`/${req.nextUrl.locale}${targetURL}`) ||
|
|
192
|
+
matchedQueryString) && (redirect.locale ? redirect.locale.toLowerCase() === locale.toLowerCase() : true));
|
|
193
|
+
})
|
|
194
|
+
: undefined;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* When a user clicks on a link generated by the Link component from next/link,
|
|
199
|
+
* Next.js adds special parameters in the route called path.
|
|
200
|
+
* This method removes these special parameters.
|
|
201
|
+
* @param {NextURL} url
|
|
202
|
+
* @returns {string} normalize url
|
|
203
|
+
*/
|
|
204
|
+
normalizeUrl(url) {
|
|
205
|
+
if (!url.search) {
|
|
206
|
+
return url;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Prepare special parameters for exclusion.
|
|
210
|
+
*/
|
|
211
|
+
const splittedPathname = url.pathname
|
|
212
|
+
.split('/')
|
|
213
|
+
.filter((route) => route)
|
|
214
|
+
.map((route) => `path=${route}`);
|
|
215
|
+
/**
|
|
216
|
+
* Remove special parameters(Next.JS)
|
|
217
|
+
* Example: /about/contact/us
|
|
218
|
+
* When a user clicks on this link, Next.js should generate a link for the middleware, formatted like this:
|
|
219
|
+
* http://host/about/contact/us?path=about&path=contact&path=us
|
|
220
|
+
*/
|
|
221
|
+
const newQueryString = url.search
|
|
222
|
+
.replace(/^\?/, '')
|
|
223
|
+
.split('&')
|
|
224
|
+
.filter((param) => {
|
|
225
|
+
if (!splittedPathname.includes(param)) {
|
|
226
|
+
return param;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
})
|
|
230
|
+
.join('&');
|
|
231
|
+
const newUrl = new URL(`${url.pathname}?${newQueryString}`, url.origin);
|
|
232
|
+
url.search = newUrl.search;
|
|
233
|
+
url.pathname = newUrl.pathname;
|
|
234
|
+
url.href = newUrl.href;
|
|
235
|
+
return url;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Helper function to create a redirect response and remove the x-middleware-next header.
|
|
239
|
+
* @param {NextURL} url The URL to redirect to.
|
|
240
|
+
* @param {Response} res The response object.
|
|
241
|
+
* @param {number} status The HTTP status code of the redirect.
|
|
242
|
+
* @param {string} statusText The status text of the redirect.
|
|
243
|
+
* @returns {NextResponse<unknown>} The redirect response.
|
|
244
|
+
*/
|
|
245
|
+
createRedirectResponse(url, res, status, statusText) {
|
|
246
|
+
const redirect = NextResponse.redirect(url, {
|
|
247
|
+
status,
|
|
248
|
+
statusText,
|
|
249
|
+
headers: res === null || res === void 0 ? void 0 : res.headers,
|
|
250
|
+
});
|
|
251
|
+
if (res === null || res === void 0 ? void 0 : res.headers) {
|
|
252
|
+
redirect.headers.delete('x-middleware-next');
|
|
253
|
+
redirect.headers.delete('x-middleware-rewrite');
|
|
254
|
+
}
|
|
255
|
+
return redirect;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Middleware / handler for use in healthcheck Next.js API route (e.g. '/api/healthz').
|
|
12
|
+
*/
|
|
13
|
+
export class HealthcheckMiddleware {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.handler = (_req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
res.status(200).send('Healthy');
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Gets the Next.js API route handler
|
|
21
|
+
* @returns route handler
|
|
22
|
+
*/
|
|
23
|
+
getHandler() {
|
|
24
|
+
return this.handler;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HealthcheckMiddleware } from './healthcheck-middleware';
|