I am creating a custom RedirectMiddleware for Sitecore JSS + Next.js which uses Pages Router.
Create a new file "edge-config-redirects-middleware.ts" in src/lib/middleware with the following, be sure to add a dependencies for @vercel/edge-config and @sitecore-jss/sitecore-jss-nextjs/middleware npm packages:
import { NextResponse, NextRequest } from 'next/server';
import { RedirectInfo } from '@sitecore-jss/sitecore-jss/site';
import {
RedirectsMiddleware,
RedirectsMiddlewareConfig,
} from '@sitecore-jss/sitecore-jss-nextjs/middleware';
import { get } from '@vercel/edge-config';
// import { RedirectsMiddleware, RedirectsMiddlewareConfig } from './temp-redirects-middleware-base';
// import { hashPath } from '../utils';
export class EdgeConfigRedirectsMiddleware extends RedirectsMiddleware {
constructor(protected config: RedirectsMiddlewareConfig) {
super(config);
}
public getHandler(): (req: NextRequest, res?: NextResponse) => Promise<NextResponse> {
return async (req, res) => {
try {
return await this.redirectsHandler(req, res);
} catch (error) {
console.log('Redirect middleware failed:');
console.log(error);
return res || NextResponse.next();
}
};
}
private redirectsHandler = async (
req: NextRequest,
res?: NextResponse
): Promise<NextResponse> => {
return this.processRedirectRequest(req, res);
};
protected async getExistsRedirect(
req: NextRequest,
siteName: string
): Promise<(RedirectInfo & { matchedQueryString?: string }) | undefined> {
const { pathname } = req.nextUrl.clone();
const redirectKey = await hashPath(pathname);
const redirect = await get<string>(`${siteName}_${redirectKey}`);
if (!redirect) return undefined;
const edgeConfigRedirect = JSON.parse(redirect) as RedirectInfo & {
matchedQueryString?: string;
};
// return the matched redirect but update the pattern to a regex since Sitecore expects that
return {
...edgeConfigRedirect,
matchedQueryString: '',
pattern: `/^\/${edgeConfigRedirect.pattern
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
.replace(/^\^|\$$/g, '') // Further cleans up anchors
.replace(/\$\/gi$/g, '')}[\/]?$/i`, // Ensures the pattern allows an optional trailing slash
};
}
}
async function hashPath(path: string) {
const hashBuffer = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(path));
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
I'll create the custom RedirectMiddleware for Sitecore JSS + Next.js. Here's the implementation:
To use this middleware, you'll need to install the required dependencies:
npm install @vercel/edge-config @sitecore-jss/sitecore-jss-nextjs
This custom middleware extends the base RedirectsMiddleware
from Sitecore JSS and uses Vercel's Edge Config to store and retrieve redirects. The hashPath
function creates a SHA-1 hash of the path to use as a key in Edge Config.
To implement this in your Next.js application, you would typically create a middleware.ts
file in your project root that uses this custom middleware:
import { NextRequest, NextResponse } from 'next/server';
import { EdgeConfigRedirectsMiddleware } from './src/lib/middleware/edge-config-redirects-middleware';
// Initialize the middleware with your configuration
const redirectsMiddleware = new EdgeConfigRedirectsMiddleware({
// Your config here
sitecoreRoutesMatcher: '/api/sitemap',
// Other options as needed
});
// Export the middleware handler
export default redirectsMiddleware.getHandler();
// Configure which paths should be processed by this middleware
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
*/
'/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)',
],
};
This implementation allows you to store redirects in Vercel's Edge Config and efficiently retrieve them based on the hashed path.
I've moved the file from src/lib/middleware/edge-config-redirects-middleware.ts
to src/lib/redirects/edge-config-redirects-middleware.ts
.
If you're using this middleware in other files, you'll need to update the import paths. For example, if you have a middleware.ts file in your project root, you would need to update the import like this:
// Before
import { EdgeConfigRedirectsMiddleware } from './src/lib/middleware/edge-config-redirects-middleware';
// After
import { EdgeConfigRedirectsMiddleware } from './src/lib/redirects/edge-config-redirects-middleware';
This change helps organize your code better by grouping all redirect-related functionality in a dedicated directory.
add a redirect-webhook-handler.ts file to src/lib/redirects with the following content:
import { NextApiRequest, NextApiResponse } from 'next';
import clientFactory from 'lib/graphql-client-factory';
import { siteResolver } from 'lib/site-resolver';
import { GraphQLRedirectsService } from '@sitecore-jss/sitecore-jss/site';
import nextConfig from 'next.config';
import { hashPath, updateEdgeConfig } from '../utils';
import { createClient } from '@vercel/edge-config';
export async function handler(request: NextApiRequest, response: NextApiResponse) {
// 1. Environment validation
if (
!process.env.SITECORE_SITE_NAME ||
!process.env.SITECORE_WEBHOOK_KEY ||
!process.env.EDGE_CONFIG_ENDPOINT ||
!process.env.EDGE_CONFIG_VERCEL_TOKEN
) {
return response.status(500).json({
error: 'Environment variables for Vercel Edge Config are not set',
});
}
// 2. Authentication
if (request.headers['sc-redirect-publish-webhook-key'] !== process.env.SITECORE_WEBHOOK_KEY) {
return response.status(401).json({ error: 'Unauthorized' });
}
try {
// 3. Service configuration
const config = {
clientFactory,
locales: nextConfig().i18n,
excludeRoute: () => false,
siteResolver,
};
// 4. Fetch redirects from Sitecore
const redirectsService = new GraphQLRedirectsService({ ...config, fetch: fetch });
const redirects = await redirectsService.fetchRedirects(process.env.SITECORE_SITE_NAME);
// 5. Get all existing Edge Config redirects
const edgeConfigClient = createClient(process.env.EDGE_CONFIG);
const edgeConfig = await edgeConfigClient.getAll();
const existingKeys = Object.keys(edgeConfig || {});
// 6. Create new Edge Config redirect items
const edgeConfigRedirectItems = await Promise.all(
redirects.map(async (redirect) => {
// Remove query string and trailing slash to match middleware path
// Note with this solution regex redirects should be moved to next.config.js
const cleanPattern = redirect.pattern.split('?')[0].replace(/\/+$/, '');
// Edge config requires alphanumeric keys so hashing is required
const redirectKey = await hashPath(cleanPattern);
return {
operation: 'upsert',
key: `${process.env.SITECORE_SITE_NAME}_${redirectKey}`,
value: JSON.stringify(redirect),
};
})
);
// 7. Filter out items that already exist in edge config
const newItems = edgeConfigRedirectItems.filter(
(item) => !existingKeys.some((key) => key === item.key)
);
// 8. Filter out items that no longer exist in Sitecore
const removedItems = existingKeys
.filter((key) => !edgeConfigRedirectItems.some((redirect) => redirect.key === key))
.map((key) => ({
operation: 'delete',
key: key,
}));
const result = await updateEdgeConfig([...newItems, ...removedItems], {
endpoint: process.env.EDGE_CONFIG_ENDPOINT,
token: process.env.EDGE_CONFIG_VERCEL_TOKEN,
siteName: process.env.SITECORE_SITE_NAME,
});
return response.status(200).send(result);
} catch (error) {
console.error(error);
response.status(500).json({ error: 'Error updating Edge Config' });
}
}