· ·

JSS End of Life: The JSS to Content SDK Checklist We Actually Use

Alex Lentz

Title card: JSS End of Life: The JSS to Content SDK Checklist. Content SDK post on Sitecore Saga.

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:

  1. 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)
  2. Copy your components into src/components
  3. 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.json config and the generated src/temp/config become sitecore.config.ts (runtime) and sitecore.cli.config.ts (build tooling)
  • Page data: the page props factory and its plugins become a single SitecoreClient (src/lib/sitecore-client.ts) with getPage, getPreview, getPagePaths and friends
  • Component registration: the generated component builder becomes .sitecore/component-map.ts, generated by sitecore-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 are NEXT_PUBLIC_DEFAULT_SITE_NAME and NEXT_PUBLIC_DEFAULT_LANGUAGE, and Edge is driven by SITECORE_EDGE_CONTEXT_ID / NEXT_PUBLIC_SITECORE_EDGE_CONTEXT_ID. Compare your old .env against the new .env.remote.example line by line
  • Analytics: Cloud SDK calls become initContentSdk with 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-nextjs to @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.json at 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

Alex Lentz

Alex Lentz

Senior Solutions Architect at Americaneagle.com and two-time Sitecore Technology MVP. Building on Sitecore since 2013, mostly SitecoreAI and headless Next.js these days.

LinkedIn    GitHub

Keep reading