Skip to content

Instantly share code, notes, and snippets.

@jbreuer
Created November 10, 2025 11:37
Show Gist options
  • Select an option

  • Save jbreuer/9ee71eacf6b58f27153b814bf61a1f84 to your computer and use it in GitHub Desktop.

Select an option

Save jbreuer/9ee71eacf6b58f27153b814bf61a1f84 to your computer and use it in GitHub Desktop.
Testing the Sitecore Content SDK App Router beta
import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
/**
* Product Availability API
*
* Simulates checking inventory/stock from a slow backend system.
* Represents real-world scenarios like checking warehouse inventory,
* fetching real-time pricing, or validating product availability.
*/
export async function GET(request: NextRequest) {
// Simulate slow external API (2-second delay for demo)
await new Promise((resolve) => setTimeout(resolve, 2000));
// Random stock level (simulates real-time inventory check)
const now = new Date();
const time = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const ms = now.getMilliseconds().toString().padStart(3, '0');
const data = {
quantity: Math.floor(Math.random() * 50) + 10, // Random 10-60
lastUpdated: time.replace(' AM', `:${ms} AM`).replace(' PM', `:${ms} PM`),
};
return NextResponse.json(data);
}
import React, { JSX, Suspense } from 'react';
import {
NextImage as ContentSdkImage,
Link as ContentSdkLink,
RichText as ContentSdkRichText,
ImageField,
Field,
LinkField,
} from '@sitecore-content-sdk/nextjs';
import { ComponentProps } from 'lib/component-props';
/**
* Async Server Component - Fetches data server-side
* This demonstrates async/await in Server Components
*/
async function AsyncPromoData({ productId }: { productId: number }) {
// Fetch product availability
const response = await fetch(`http://localhost:3000/api/products/availability?id=${productId}`, {
cache: 'no-store',
});
const data = await response.json();
return (
<div style={{ marginTop: '0.5rem', padding: '0.5rem', background: '#d4edda', borderRadius: '4px' }}>
<strong>In Stock:</strong> {data.quantity} items • Updated: {data.lastUpdated}
</div>
);
}
interface Fields {
PromoIcon: ImageField;
PromoText: Field<string>;
PromoLink: LinkField;
PromoText2: Field<string>;
}
type PromoProps = ComponentProps & {
fields: Fields;
};
interface PromoContentProps extends PromoProps {
renderText: (fields: Fields) => JSX.Element;
}
const PromoContent = (props: PromoContentProps): JSX.Element => {
const { fields, params, renderText } = props;
const { styles, RenderingIdentifier: id } = params;
const Wrapper = ({ children }: { children: JSX.Element }): JSX.Element => (
<div className={`component promo ${styles}`} id={id}>
<div className="component-content">{children}</div>
</div>
);
if (!fields) {
return (
<Wrapper>
<span className="is-empty-hint">Promo</span>
</Wrapper>
);
}
return (
<Wrapper>
<>
<div className="field-promoicon">
<ContentSdkImage field={fields.PromoIcon} />
</div>
<div className="promo-text">{renderText(fields)}</div>
</>
</Wrapper>
);
};
export const Default = async (props: PromoProps): Promise<JSX.Element> => {
// Delay to make streaming visible (remove in production)
await new Promise(resolve => setTimeout(resolve, 2000));
const renderText = (fields: Fields) => (
<>
<div className="field-promotext">
<ContentSdkRichText field={fields.PromoText} />
</div>
<div className="field-promolink">
<ContentSdkLink field={fields.PromoLink} />
</div>
{/* Nested Suspense for async data streaming */}
<Suspense fallback={<p style={{ marginTop: '0.5rem', fontStyle: 'italic' }}>Loading...</p>}>
<AsyncPromoData productId={Math.random()} />
</Suspense>
</>
);
return <PromoContent {...props} renderText={renderText} />;
};
export const WithText = (props: PromoProps): JSX.Element => {
const renderText = (fields: Fields) => (
<>
<div className="field-promotext">
<ContentSdkRichText className="promo-text" field={fields.PromoText} />
</div>
<div className="field-promotext">
<ContentSdkRichText className="promo-text" field={fields.PromoText2} />
</div>
</>
);
return <PromoContent {...props} renderText={renderText} />;
};
import { type NextRequest, type NextFetchEvent } from 'next/server';
import {
defineMiddleware,
AppRouterMultisiteMiddleware,
PersonalizeMiddleware,
RedirectsMiddleware,
LocaleMiddleware,
} from '@sitecore-content-sdk/nextjs/middleware';
import sites from '.sitecore/sites.json';
import scConfig from 'sitecore.config';
import { routing } from './i18n/routing';
// Determine if we're using Edge mode or local container mode
const isEdgeMode = !!(
scConfig.api.edge?.contextId ||
scConfig.api.edge?.clientContextId
);
const locale = new LocaleMiddleware({
/**
* List of sites for site resolver to work with
*/
sites,
/**
* List of all supported locales configured in routing.ts
*/
locales: routing.locales.slice(),
// This function determines if the middleware should be turned off on per-request basis.
// Certain paths are ignored by default (e.g. files and Next.js API routes), but you may wish to disable more.
// This is an important performance consideration since Next.js Edge middleware runs on every request.
// in multilanguage scenarios, we need locale middleware to always run first to ensure locale is set and used correctly by the rest of the middlewares
skip: () => false,
});
// Edge-dependent middlewares: only create when using XM Cloud Edge
// These middlewares are not compatible with local container development
const middlewareChain = isEdgeMode
? [
locale,
new AppRouterMultisiteMiddleware({
sites,
...scConfig.api.edge,
...scConfig.multisite,
skip: () => false,
}),
new RedirectsMiddleware({
sites,
...scConfig.api.edge,
...scConfig.redirects,
skip: () => false,
}),
new PersonalizeMiddleware({
sites,
...scConfig.api.edge,
...scConfig.personalize,
skip: () => false,
}),
]
: [locale]; // Local container mode: only use locale middleware
export function middleware(req: NextRequest, ev: NextFetchEvent) {
return defineMiddleware(...middlewareChain).exec(req, ev);
}
export const config = {
/*
* Match all paths except for:
* 1. API route handlers
* 2. /_next (Next.js internals)
* 3. /sitecore/api (Sitecore API routes)
* 4. /- (Sitecore media)
* 5. /healthz (Health check)
* 7. all root files inside /public
*/
matcher: [
'/',
'/((?!api/|sitemap|robots|_next/|healthz|sitecore/api/|-/|favicon.ico|sc_logo.svg).*)',
],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment