Well, it’s June. If you’ve been putting off the JSS to Content SDK migration, this is the month the excuse runs out.
Sitecore’s docs have been clear about this for a while: JSS 22.x has been in maintenance mode until June 2026, and after June 2026 it reaches end of life – no more updates, no more security patches. Content SDK is the supported SDK for SitecoreAI, and with 2.x it’s well past “new and shiny”.
We’ve now moved enough head apps that the process is pretty repeatable. This is the checklist.
Step 0: Know what you’re moving
Before anyone estimates anything, get a real picture of how much JSS is in the codebase. This little Node script lists every symbol imported from @sitecore-jss/*, sorted by how many files use it:
// jss-imports.mjs - list every JSS import in a head app, grouped by symbol
// Usage: node jss-imports.mjs [srcDir]
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
const root = process.argv[2] ?? 'src';
const importRe = /import\s+(type\s+)?\{([^}]+)\}\s+from\s+['"](@sitecore-jss\/[^'"]+)['"]/g;
const usage = new Map(); // "symbol (package)" -> Set of files
function walk(dir) {
for (const name of readdirSync(dir)) {
const full = join(dir, name);
if (name === 'node_modules' || name.startsWith('.')) continue;
if (statSync(full).isDirectory()) walk(full);
else if (/\.(tsx?|jsx?)$/.test(name)) scan(full);
}
}
function scan(file) {
const text = readFileSync(file, 'utf8');
for (const [, , names, pkg] of text.matchAll(importRe)) {
for (const raw of names.split(',')) {
const symbol = raw.replace(/^\s*type\s+/, '').split(/\s+as\s+/)[0].trim();
if (!symbol) continue;
const key = `${symbol} (${pkg})`;
if (!usage.has(key)) usage.set(key, new Set());
usage.get(key).add(relative(root, file));
}
}
}
walk(root);
const rows = [...usage.entries()].sort((a, b) => b[1].size - a[1].size);
for (const [key, files] of rows) {
console.log(`${String(files.size).padStart(4)} ${key}`);
}
console.log(`\n${rows.length} distinct JSS symbols across ${new Set(rows.flatMap(([, f]) => [...f])).size} files`);
Sample output:
48 Text (@sitecore-jss/sitecore-jss-nextjs)
41 Field (@sitecore-jss/sitecore-jss-nextjs)
22 withDatasourceCheck (@sitecore-jss/sitecore-jss-nextjs)
3 GraphQLRequestClient (@sitecore-jss/sitecore-jss-nextjs/graphql)
1 SitecorePagePropsFactory (...)
The top of that list (field components and types) is mostly a rename. The long tail is where the real work is: anything touching page props factories, GraphQL clients, custom middleware plugins and the Experience Editor.
Step 1: Start from a fresh scaffold, not an in-place upgrade
This is the biggest lesson we learned. Don’t try to upgrade the JSS app in place. The project structure is different enough that you’ll spend more time fighting the old structure than porting code.
Instead:
- Scaffold a new app with
npx create-content-sdk-app(Pages Router, unless you’ve specifically decided to take on the App Router at the same time – I wouldn’t) - Copy your components into
src/components - Port everything else deliberately, one concern at a time
Step 2: Map the concepts
Here’s the translation table we keep pinned in the team channel:
- Config:
scjssconfig.json,package.jsonconfig and the generatedsrc/temp/configbecomesitecore.config.ts(runtime) andsitecore.cli.config.ts(build tooling) - Page data: the page props factory and its plugins become a single
SitecoreClient(src/lib/sitecore-client.ts) withgetPage,getPreview,getPagePathsand friends - Component registration: the generated component builder becomes
.sitecore/component-map.ts, generated bysitecore-tools - Middleware plugins: the plugin folder becomes explicit, composed proxies in
src/proxy.ts(MultisiteProxy,RedirectsProxy,PersonalizeProxy) on Content SDK 2.x - Editing: Experience Editor support is gone. Content SDK targets Page builder. If your authors still live in Experience Editor, that’s a training conversation, not a code one
- Environment variables: the editing secret is
SITECORE_EDITING_SECRET, the site and language defaults areNEXT_PUBLIC_DEFAULT_SITE_NAMEandNEXT_PUBLIC_DEFAULT_LANGUAGE, and Edge is driven bySITECORE_EDGE_CONTEXT_ID/NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID. Compare your old.envagainst the new.env.remote.exampleline by line - Analytics: Cloud SDK calls become
initContentSdkwith the analytics and events plugins on 2.x
The SitecoreClient change is the one that simplifies things the most. On a typical JSS app, the logic for “get this page” is spread across a factory and four or five plugins. In Content SDK it reads like this:
// src/lib/sitecore-client.ts
import { SitecoreClient } from '@sitecore-content-sdk/nextjs/client';
import scConfig from 'sitecore.config';
const client = new SitecoreClient({ ...scConfig });
export default client;
// src/pages/[[...path]].tsx (inside getStaticProps)
const page = context.preview
? await client.getPreview(context.previewData)
: await client.getPage(path, { locale: context.locale });
If you’d customized a page props plugin (adding data to every page, say), move that logic into getStaticProps / getServerSideProps around the getPage call, or into component-level data fetching.
Step 3: Components
For most renderings this is the boring part, which is exactly what you want:
- Change imports from
@sitecore-jss/sitecore-jss-nextjsto@sitecore-content-sdk/nextjs - Re-check field types (
Field,ImageField,LinkField…) – the compiler will tell you - Components that fetched their own data need to move to the Content SDK component-level data fetching pattern
- Delete anything that only existed for Experience Editor chromes
Do a handful by hand first so the team agrees on the patterns. Then hand the mechanical ones to your AI coding assistant along with the agent skills that ship with the Content SDK scaffold – this is exactly the kind of repetitive change they’re good at, and the skills keep them on the SDK’s conventions.
Step 4: Editing host and deployment
- Point the editing host config in
xmcloud.build.jsonat the new app path, with the right Node version - Set the new environment variables on the editing host (if you’re on decoupled deployments, remember the editing host has its own)
- Make sure the editing secret matches what SitecoreAI expects
Step 5: The test pass that matters
What we test on every migration, because these are where differences actually show up:
- Every page template renders in Page builder, and fields are editable
- Multisite host resolution, if you have more than one site
- Redirects, especially with multiple languages (the defaults changed in Content SDK 2.0)
- Personalization variants
- Sitemap and robots.txt
- 404 and 500 pages
- Preview for unpublished content
- Core Web Vitals before and after – Content SDK apps are leaner, and it’s nice to have the numbers to show for it
If you can’t make June
It happens. If a migration can’t land before end of life, at least get on the latest JSS 22.x, write down the risk, and agree on a date. End of life doesn’t mean the site stops working the next morning – it means you’re on your own for security fixes. That’s a business decision, so make sure the business is the one making it.
Resources
- JSS 22.x maintenance and end of life: https://doc.sitecore.com/xmc/en/developers/jss/22/jss-xmc/introducing-sitecore-javascript-rendering-sdk.html
- From JSS to Content SDK – what it means for your business: https://www.sitecore.com/solutions/topics/content-management/from-jss-to-content-sdk
- Content SDK on GitHub: https://github.com/Sitecore/content-sdk




