diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27244d6513..11cdcef039 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,10 +18,11 @@ Our versioning strategy is as follows:
* `[templates/react]` `[sitecore-jss-react]` Replace package 'deep-equal' with 'fast-deep-equal'. No functionality change only performance improvement ([#1719](https://github.com/Sitecore/jss/pull/1719)) ([#1665](https://github.com/Sitecore/jss/pull/1665))
* `[templates/nextjs-xmcloud]` `[sitecore-jss]` `[sitecore-jss-nextjs]` `[sitecore-jss-react]` Add support for loading appropriate stylesheets whenever a theme is applied to BYOC and SXA components by introducing new function getComponentLibraryStylesheetLinks, which replaces getFEAASLibraryStylesheetLinks (which has been marked as deprecated) ([#1722](https://github.com/Sitecore/jss/pull/1722))
* `[templates/nextjs-xmcloud]` `[sitecore-jss-nextjs]` Add protected endpoint which provides configuration information (the sitecore packages used by the app and all registered components) to be used to determine feature compatibility on Pages side. ([#1724](https://github.com/Sitecore/jss/pull/1724))
-* `[templates/nextjs]` `[templates/nextjs-styleguide]` Modify all GraphQLRequestClient import statements so that it gets imported from the /graphql submodule ([#1728](https://github.com/Sitecore/jss/pull/1728))
+* `[sitecore-jss-nextjs]` `[templates/nextjs]` [BYOC] Component Builder integration endpoint ([#1729](https://github.com/Sitecore/jss/pull/1729))
### 🐛 Bug Fixes
+* `[templates/nextjs]` `[templates/nextjs-styleguide]` Modify all GraphQLRequestClient import statements so that it gets imported from the /graphql submodule ([#1728](https://github.com/Sitecore/jss/pull/1728))
* `[sitecore-jss-nextjs]` Internal link in RichText is broken when nested tags are added ([#1718](https://github.com/Sitecore/jss/pull/1718))
* `[templates/node-headless-ssr-proxy]` `[node-headless-ssr-proxy]` Add sc_site qs parameter to Layout Service requests by default ([#1660](https://github.com/Sitecore/jss/pull/1660))
* `[templates/nextjs-sxa]` Fixed Image component when there is using Banner variant which set property background-image when image is empty. ([#1689](https://github.com/Sitecore/jss/pull/1689)) ([#1692](https://github.com/Sitecore/jss/pull/1692))
diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js
index d79aeadee4..47d8d6ee82 100644
--- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js
+++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js
@@ -3,6 +3,15 @@
*/
const feaasPlugin = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
+ async rewrites() {
+ return [
+ ...await nextConfig.rewrites(),
+ {
+ source: '/feaas-render',
+ destination: '/api/editing/feaas/render',
+ },
+ ];
+ },
webpack: (config, options) => {
if (options.isServer) {
// Force use of CommonJS on the server for FEAAS SDK since JSS also uses CommonJS entrypoint to FEAAS SDK.
diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts
new file mode 100644
index 0000000000..8020e2e530
--- /dev/null
+++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts
@@ -0,0 +1,21 @@
+import type { NextRequest, NextFetchEvent } from 'next/server';
+import middleware from 'lib/middleware';
+
+// eslint-disable-next-line
+export default async function (req: NextRequest, ev: NextFetchEvent) {
+ return middleware(req, ev);
+}
+
+export const config = {
+ /*
+ * Match all paths except for:
+ * 1. /api routes
+ * 2. /_next (Next.js internals)
+ * 3. /sitecore/api (Sitecore API routes)
+ * 4. /- (Sitecore media)
+ * 5. /healthz (Health check)
+ * 6. /feaas-render (FEaaS render)
+ * 7. all root files inside /public
+ */
+ matcher: ['/', '/((?!api/|_next/|feaas-render|healthz|sitecore/api/|-/|favicon.ico|sc_logo.svg).*)'],
+};
diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts
new file mode 100644
index 0000000000..3830d592cb
--- /dev/null
+++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts
@@ -0,0 +1,16 @@
+import { FEAASRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';
+
+/**
+ * This Next.js API route is used to handle GET requests from Sitecore Component Builder.
+ *
+ * The `FEAASRenderMiddleware` will:
+ * 1. Enable Next.js Preview Mode.
+ * 2. Redirect the request to the /feaas/render page.
+ * - If "feaasSrc" query parameter is provided, the page will render a FEAAS component.
+ * - The page provides all the registered FEAAS components.
+ */
+
+// Wire up the FEAASRenderMiddleware handler
+const handler = new FEAASRenderMiddleware().getHandler();
+
+export default handler;
diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx
new file mode 100644
index 0000000000..2f3e2e30ff
--- /dev/null
+++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx
@@ -0,0 +1,32 @@
+import { GetServerSideProps } from 'next';
+import BYOC from 'src/byoc';
+import * as FEAAS from '@sitecore-feaas/clientside/react';
+
+/**
+ * The FEAASRender page is responsible for:
+ * - Rendering the FEAAS component if the "feaasSrc" is provided.
+ * - Rendering all the registered components.
+ * - The page is rendered only if it's requested by the api route (/api/editing/feaas/render) using the preview mode.
+ */
+const FEAASRender = ({ feaasSrc }: { feaasSrc: string }): JSX.Element => {
+ return (
+ <>
+ {/** Render the component if the "feaasSrc" is provided */}
+ {feaasSrc && }
+ {/** Render all the registered components */}
+
+ >
+ );
+};
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ return {
+ props: {
+ feaasSrc: context.query.feaasSrc || null,
+ },
+ // Don't show the page if it's not requested by the api route using the preview mode
+ notFound: !context.preview,
+ };
+};
+
+export default FEAASRender;
diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts
index d3d3bf5f93..789121278f 100644
--- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts
+++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts
@@ -5,12 +5,9 @@ import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/
import { parse } from 'node-html-parser';
import { EditingData } from './editing-data';
import { EditingDataService, editingDataService } from './editing-data-service';
-import {
- QUERY_PARAM_EDITING_SECRET,
- QUERY_PARAM_PROTECTION_BYPASS_SITECORE,
- QUERY_PARAM_PROTECTION_BYPASS_VERCEL,
-} from './constants';
+import { QUERY_PARAM_EDITING_SECRET } from './constants';
import { getJssEditingSecret } from '../utils/utils';
+import { RenderMiddlewareBase } from './render-middleware';
export interface EditingRenderMiddlewareConfig {
/**
@@ -52,7 +49,7 @@ export interface EditingRenderMiddlewareConfig {
* Middleware / handler for use in the editing render Next.js API route (e.g. '/api/editing/render')
* which is required for Sitecore editing support.
*/
-export class EditingRenderMiddleware {
+export class EditingRenderMiddleware extends RenderMiddlewareBase {
private editingDataService: EditingDataService;
private dataFetcher: AxiosDataFetcher;
private resolvePageUrl: (serverUrl: string, itemPath: string) => string;
@@ -62,6 +59,8 @@ export class EditingRenderMiddleware {
* @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config
*/
constructor(config?: EditingRenderMiddlewareConfig) {
+ super();
+
this.editingDataService = config?.editingDataService ?? editingDataService;
this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing });
this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl;
@@ -76,28 +75,6 @@ export class EditingRenderMiddleware {
return this.handler;
}
- /**
- * Gets query parameters that should be passed along to subsequent requests
- * @param {Object} query Object of query parameters from incoming URL
- * @returns Object of approved query parameters
- */
- protected getQueryParamsForPropagation = (
- query: Partial<{ [key: string]: string | string[] }>
- ): { [key: string]: string } => {
- const params: { [key: string]: string } = {};
- if (query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE]) {
- params[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = query[
- QUERY_PARAM_PROTECTION_BYPASS_SITECORE
- ] as string;
- }
- if (query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL]) {
- params[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = query[
- QUERY_PARAM_PROTECTION_BYPASS_VERCEL
- ] as string;
- }
- return params;
- };
-
private handler = async (req: NextApiRequest, res: NextApiResponse): Promise => {
const { method, query, body, headers } = req;
diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts
new file mode 100644
index 0000000000..3b98b23ab3
--- /dev/null
+++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts
@@ -0,0 +1,216 @@
+/* eslint-disable quotes */
+/* eslint-disable no-unused-expressions */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { expect, use } from 'chai';
+import { NextApiRequest, NextApiResponse } from 'next';
+import {
+ QUERY_PARAM_EDITING_SECRET,
+ QUERY_PARAM_PROTECTION_BYPASS_SITECORE,
+ QUERY_PARAM_PROTECTION_BYPASS_VERCEL,
+} from './constants';
+import { FEAASRenderMiddleware } from './feaas-render-middleware';
+import { spy } from 'sinon';
+import sinonChai from 'sinon-chai';
+
+use(sinonChai);
+
+type Query = {
+ [key: string]: string;
+};
+
+const mockRequest = (query?: Query, method?: string, host?: string) => {
+ return {
+ body: {},
+ method: method ?? 'GET',
+ query: query ?? {},
+ headers: { host: host ?? 'localhost:3000' },
+ } as NextApiRequest;
+};
+
+const mockResponse = () => {
+ const res = {} as NextApiResponse;
+ res.status = spy(() => {
+ return res;
+ });
+ res.send = spy(() => {
+ return res;
+ });
+ res.setPreviewData = spy(() => {
+ return res;
+ });
+ res.setHeader = spy(() => {
+ return res;
+ });
+ res.redirect = spy(() => {
+ return res;
+ });
+
+ return res;
+};
+
+describe('FEAASRenderMiddleware', () => {
+ const secret = 'secret1234';
+
+ beforeEach(() => {
+ process.env.JSS_EDITING_SECRET = secret;
+ });
+
+ after(() => {
+ delete process.env.JSS_EDITING_SECRET;
+ });
+
+ it('should handle request', async () => {
+ const query = {} as Query;
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({});
+ expect(res.redirect).to.have.been.calledOnce;
+ expect(res.redirect).to.have.been.calledWith('/feaas/render');
+ });
+
+ it('should handle request when feaasSrc query parameter is present', async () => {
+ const query = {
+ feaasSrc: 'https://feaas.blob.core.windows.net/components/xxx/xyz/responsive/staged',
+ } as Query;
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({});
+ expect(res.redirect).to.have.been.calledOnce;
+ expect(res.redirect).to.have.been.calledWith(
+ '/feaas/render?feaasSrc=https%3A%2F%2Ffeaas.blob.core.windows.net%2Fcomponents%2Fxxx%2Fxyz%2Fresponsive%2Fstaged'
+ );
+ });
+
+ it('should throw error', async () => {
+ const query = {} as Query;
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ res.setPreviewData = spy(() => {
+ throw new Error('Test Error');
+ });
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({});
+ expect(res.status).to.have.been.calledOnce;
+ expect(res.status).to.have.been.calledWith(500);
+ expect(res.send).to.have.been.calledOnce;
+ expect(res.send).to.have.been.calledWith('Error: Test Error');
+ });
+
+ it('should respondWith 405 for unsupported method', async () => {
+ const req = mockRequest({}, 'POST');
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.setHeader).to.have.been.calledWithExactly('Allow', 'GET');
+ expect(res.status).to.have.been.calledOnce;
+ expect(res.status).to.have.been.calledWith(405);
+ expect(res.send).to.have.been.calledOnce;
+ expect(res.send).to.have.been.calledWith(
+ "Invalid request method 'POST'"
+ );
+ });
+
+ it('should respond with 401 for missing secret', async () => {
+ const query = {} as Query;
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.status).to.have.been.calledOnce;
+ expect(res.status).to.have.been.calledWith(401);
+ expect(res.send).to.have.been.calledOnce;
+ expect(res.send).to.have.been.calledWith('Missing or invalid secret');
+ });
+
+ it('should respond with 401 for invalid secret', async () => {
+ const query = {} as Query;
+ query[QUERY_PARAM_EDITING_SECRET] = 'nope';
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.status).to.have.been.calledOnce;
+ expect(res.status).to.have.been.calledWith(401);
+ expect(res.send).to.have.been.calledOnce;
+ expect(res.send).to.have.been.calledWith('Missing or invalid secret');
+ });
+
+ it('should use custom pageUrl', async () => {
+ const query = {} as Query;
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const pageUrl = '/some/path/feaas/render';
+
+ const middleware = new FEAASRenderMiddleware({
+ pageUrl,
+ });
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.redirect).to.have.been.calledOnce;
+ expect(res.redirect).to.have.been.calledWith(pageUrl);
+ });
+
+ it('should pass along protection bypass query parameters', async () => {
+ const query = {} as Query;
+ const bypassTokenSitecore = 'token1234Sitecore';
+ const bypassTokenVercel = 'token1234Vercel';
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+ query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = bypassTokenSitecore;
+ query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel;
+ const previewData = {};
+
+ const req = mockRequest(query);
+ const res = mockResponse();
+
+ const middleware = new FEAASRenderMiddleware();
+ const handler = middleware.getHandler();
+
+ await handler(req, res);
+
+ expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData);
+ expect(res.redirect).to.have.been.calledOnce;
+ expect(res.redirect).to.have.been.calledWith(
+ '/feaas/render?x-sitecore-protection-bypass=token1234Sitecore&x-vercel-protection-bypass=token1234Vercel'
+ );
+ });
+});
diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts
new file mode 100644
index 0000000000..98c2e53a28
--- /dev/null
+++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts
@@ -0,0 +1,111 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { debug } from '@sitecore-jss/sitecore-jss';
+import { QUERY_PARAM_EDITING_SECRET } from './constants';
+import { getJssEditingSecret } from '../utils/utils';
+import { RenderMiddlewareBase } from './render-middleware';
+
+/**
+ * Configuration for `FEAASRenderMiddleware`.
+ */
+export interface FEAASRenderMiddlewareConfig {
+ /**
+ * Defines FEAAS page route to render.
+ * This may be necessary for certain custom Next.js routing configurations.
+ * @default /feaas/render
+ */
+ pageUrl?: string;
+}
+
+/**
+ * Middleware / handler for use in the feaas render Next.js API route (e.g. '/api/editing/feaas/render')
+ * which is required for Sitecore editing support.
+ */
+export class FEAASRenderMiddleware extends RenderMiddlewareBase {
+ private pageUrl: string;
+ private defaultPageUrl = '/feaas/render';
+
+ /**
+ * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config
+ */
+ constructor(protected config?: FEAASRenderMiddlewareConfig) {
+ super();
+
+ this.pageUrl = config?.pageUrl ?? this.defaultPageUrl;
+ }
+
+ /**
+ * Gets the Next.js API route handler
+ * @returns route handler
+ */
+ public getHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise {
+ return this.handler;
+ }
+
+ private handler = async (req: NextApiRequest, res: NextApiResponse): Promise => {
+ const { method, query, headers } = req;
+
+ const startTimestamp = Date.now();
+
+ debug.editing('feaas render middleware start: %o', {
+ method,
+ query,
+ headers,
+ });
+
+ if (method !== 'GET') {
+ debug.editing('invalid method - sent %s expected GET', method);
+ res.setHeader('Allow', 'GET');
+ return res.status(405).send(`Invalid request method '${method}'`);
+ }
+
+ // Validate secret
+ const secret = query[QUERY_PARAM_EDITING_SECRET];
+ if (secret !== getJssEditingSecret()) {
+ debug.editing(
+ 'invalid editing secret - sent "%s" expected "%s"',
+ secret,
+ getJssEditingSecret()
+ );
+ return res.status(401).send('Missing or invalid secret');
+ }
+
+ try {
+ // Get query string parameters to propagate on subsequent requests (e.g. for deployment protection bypass)
+ const params = this.getQueryParamsForPropagation(query);
+
+ // Enable Next.js Preview Mode
+ res.setPreviewData({});
+
+ const queryParams = new URLSearchParams();
+
+ for (const key in params) {
+ if ({}.hasOwnProperty.call(params, key)) {
+ queryParams.append(key, params[key]);
+ }
+ }
+
+ // Pass "feaasSrc" in case a FEAASComponent is being requested
+ if (query.feaasSrc) {
+ queryParams.append('feaasSrc', query.feaasSrc as string);
+ }
+
+ const redirectUrl =
+ this.pageUrl + (queryParams.toString() ? `?${queryParams.toString()}` : '');
+
+ debug.editing('redirecting to page route %s', redirectUrl);
+
+ debug.editing('feaas render middleware end in %dms', Date.now() - startTimestamp);
+
+ res.redirect(redirectUrl);
+ } catch (err) {
+ const error = err as Record;
+
+ console.info(
+ // eslint-disable-next-line quotes
+ "Hint: for non-standard server or Next.js route configurations, you may need to override the 'pageUrl' available on the 'FEAASRenderMiddleware' config."
+ );
+
+ res.status(500).send(`${error}`);
+ }
+ };
+}
diff --git a/packages/sitecore-jss-nextjs/src/editing/index.ts b/packages/sitecore-jss-nextjs/src/editing/index.ts
index 0c55468641..98e7ea60ab 100644
--- a/packages/sitecore-jss-nextjs/src/editing/index.ts
+++ b/packages/sitecore-jss-nextjs/src/editing/index.ts
@@ -15,6 +15,7 @@ export {
editingDataService,
} from './editing-data-service';
export { VercelEditingDataCache } from './vercel-editing-data-cache';
+export { FEAASRenderMiddleware, FEAASRenderMiddlewareConfig } from './feaas-render-middleware';
export {
EditingConfigMiddleware,
EditingConfigMiddlewareConfig,
diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts
new file mode 100644
index 0000000000..7b56134fe5
--- /dev/null
+++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts
@@ -0,0 +1,38 @@
+/* eslint-disable dot-notation */
+import chai from 'chai';
+import chaiString from 'chai-string';
+import { RenderMiddlewareBase } from './render-middleware';
+import {
+ QUERY_PARAM_EDITING_SECRET,
+ QUERY_PARAM_PROTECTION_BYPASS_SITECORE,
+ QUERY_PARAM_PROTECTION_BYPASS_VERCEL,
+} from './constants';
+
+const expect = chai.use(chaiString).expect;
+
+type Query = {
+ [key: string]: string;
+};
+
+describe('RenderMiddlewareBase', () => {
+ class SampleMiddleware extends RenderMiddlewareBase {}
+
+ describe('getQueryParamsForPropagation', () => {
+ it('should construct query params for protection bypass', () => {
+ const middleware = new SampleMiddleware();
+
+ const secret = 'secret1234';
+ const query = {} as Query;
+ const bypassTokenSitecore = 'token1234Sitecore';
+ const bypassTokenVercel = 'token1234Vercel';
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
+ query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = bypassTokenSitecore;
+ query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel;
+
+ expect(middleware['getQueryParamsForPropagation'](query)).to.deep.equal({
+ [QUERY_PARAM_PROTECTION_BYPASS_SITECORE]: bypassTokenSitecore,
+ [QUERY_PARAM_PROTECTION_BYPASS_VERCEL]: bypassTokenVercel,
+ });
+ });
+ });
+});
diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts
new file mode 100644
index 0000000000..2d2503efd9
--- /dev/null
+++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts
@@ -0,0 +1,31 @@
+import {
+ QUERY_PARAM_PROTECTION_BYPASS_SITECORE,
+ QUERY_PARAM_PROTECTION_BYPASS_VERCEL,
+} from './constants';
+
+/**
+ * Base class for middleware that handles pages and components rendering in Sitecore Editors.
+ */
+export abstract class RenderMiddlewareBase {
+ /**
+ * Gets query parameters that should be passed along to subsequent requests (e.g. for deployment protection bypass)
+ * @param {Object} query Object of query parameters from incoming URL
+ * @returns Object of approved query parameters
+ */
+ protected getQueryParamsForPropagation = (
+ query: Partial<{ [key: string]: string | string[] }>
+ ): { [key: string]: string } => {
+ const params: { [key: string]: string } = {};
+ if (query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE]) {
+ params[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = query[
+ QUERY_PARAM_PROTECTION_BYPASS_SITECORE
+ ] as string;
+ }
+ if (query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL]) {
+ params[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = query[
+ QUERY_PARAM_PROTECTION_BYPASS_VERCEL
+ ] as string;
+ }
+ return params;
+ };
+}