Content SDK + Next.js App Router: middleware that actually works (even when you develop locally)

The Content SDK with the Next.js App Router is a solid step forward. Finally App Router support, React Server Components, streaming—inside a Sitecore head application. Sitecore’s documentation on this is genuinely decent.

But the moment you think, “Cool, I’ll just run this locally in containers like I’ve been doing for years”… you get the digital equivalent of a door slammed in your face.

The problem: local containers and App Router don’t (yet) play nice

I was messing around with the Content SDK and the SitecoreAI starterkit. Plan: run everything locally in containers so multiple devs can serialize items without stepping on each other’s toes.
Reality: local containers don’t work anymore in combination with the App Router beta. Jeroen Breuer ran into the same thing.

His fix got me closer, but it didn’t fully work for our setup. So I built an alternative solution.

Why this happens (in normal human language)

The Content SDK App Router setup leans heavily on Next.js middleware. Middleware runs on every request and gets compiled in the Edge runtime. Next.js is pretty clear about this: keep middleware lean, modular, and route-aware, or you’ll wreck performance and predictability.

In the default Content SDK setup, some middleware gets instantiated eagerly—even if you don’t need it locally at all. And locally you usually don’t have an Edge context, but you do have sites.json, redirects, personalize config, locale routing… basically the perfect recipe for “it works on my machine.” Except now it doesn’t. Irony included for free.

The fix: lazy middleware chain + Edge detection

I had AI make a small but meaningful adjustment:

  • Locale middleware always first and always on.
    In multi-language setups, you want locale set before multisite/redirects/personalize tries anything. 
  • Only instantiate Edge-only middleware if you’re actually in Edge mode.
    Don’t call constructors “just in case.”
  • Build the middleware chain dynamically.
    No unnecessary work in local/dev.

Here’s the middleware:

import { type NextRequest, type NextFetchEvent } from 'next/server';

import {
  defineMiddleware,
  AppRouterMultisiteMiddleware,
  PersonalizeMiddleware,
  RedirectsMiddleware,
  LocaleMiddleware,
  type Middleware,
} from '@sitecore-content-sdk/nextjs/middleware';

import sites from '.sitecore/sites.json';
import scConfig from 'sitecore.config';
import { routing } from './i18n/routing';
import { XBECorsMiddleware } from 'xbe-lib/middleware';

const isEdgeMode = !!(
  scConfig.api.edge?.contextId ||
  scConfig.api.edge?.clientContextId
);

/**
 * LocaleMiddleware always runs first to ensure locale is set correctly
 * for subsequent middleware in multilanguage scenarios
 */
const locale = new LocaleMiddleware({
  sites,
  locales: routing.locales.slice(),
  skip: () => false,
});

/**
 * CORS middleware configuration for XBE
 */
const cors = new XBECorsMiddleware({
  enabled: true,
  allowedOrigins: [], // Configure with specific origins if needed
  sitecoreApiHost: scConfig.api.local.apiHost || scConfig.api?.edge?.clientContextId,
  sites,
});

/**
 * Build the middleware chain based on configuration
 */
const buildMiddlewareChain = (): Middleware[] => {
  const chain: Middleware[] = [locale];

  // Multisite middleware for Edge mode
  const multisite = new AppRouterMultisiteMiddleware({
    sites,
    ...scConfig.api.edge,
    ...scConfig.multisite,
    skip: () => false,
  });
  chain.push(multisite);

  if (isEdgeMode) {
    // Redirects middleware for Edge mode
    const redirects = new RedirectsMiddleware({
      sites,
      ...scConfig.api.edge,
      ...scConfig.redirects,
      skip: () => false,
    });
    chain.push(redirects);
  }

  // CORS middleware runs after locale and optional Edge middlewares
  //chain.push(cors);

  if (isEdgeMode) {
    // Personalize middleware runs last
    const personalize = new PersonalizeMiddleware({
      sites,
      ...scConfig.api.edge,
      ...scConfig.personalize,
      skip: () => false,
    });
    chain.push(personalize);
  }

  return chain;
};

const middlewareChain = buildMiddlewareChain();

export function middleware(req: NextRequest, ev: NextFetchEvent) {
  return defineMiddleware(...middlewareChain).exec(req, ev);
}

export const config = {
  matcher: [
    '/',
    '/((?!api/|sitemap|robots|_next/|healthz|sitecore/api/|-/|favicon.ico|sc_logo.svg).*)',
  ],
};

 

What you get out of it

  • locally, no Edge middleware blowing up your dev setup;
  • redirects/personalize only active on the Edge;
  • consistent locale routing (so no more “why is my site suddenly EN-US?”).

What I expect going forward

App Router + Content SDK is a great move. Long term, I expect local development to fade away. But until that glorious day, your local dev environment still has to work—so developers can actually collaborate without resorting to ritual sacrifice.