Upgrade JSS 22.x Angular apps to Content SDK 1.0

Version: 1.x

The Sitecore Content SDK for Angular replaces the JSS SDK for Angular. This topic describes how to migrate an existing JSS Angular app to a Content SDK app using Angular. For simplicity, we'll use the NgModule-based starter that's shipped with JSS SDK for Angular.

The two apps differ in more than package names. The JSS app is built around NgModule, a custom route matcher/resolver pair, a JssContextService that streams state through RxJS, integrated-mode SSR (server.bundle.ts) behind a separate Node proxy, and client-side Cloud SDK initialization.

The Content SDK app is a standalone-first application without NgModule that uses route resolvers, known as loaders, a SitecoreClient, server-side middleware for multisite and personalization, an Express SSR host (server.ts), and a SitecoreContextService exposed as Angular signals.

This topic covers the following:

  1. What's new in Content SDK for Angular 1.0?
  2. Create a new Content SDK application
  3. Configure your environment
  4. Migrate your components
  5. Read context data
  6. Fetch data with SitecoreClient and loaders
  7. Migrate existing internationalization implementation
  8. Obsolete and revised functionality
  9. Optional steps

What's new in Content SDK for Angular 1.0?

Content SDK for Angular consolidates the JSS Angular packages into a single @sitecore-content-sdk/angular package and modernizes the app architecture using the latest Angular features, such as standalone components, signals, and route resolvers:

  • Standalone, no NgModule usage - boots through bootstrapApplication with an ApplicationConfig (app.config.ts) instead of AppModule/AppServerModule. Components are standalone and declare their own imports.
  • Route resolvers (known as loaders) - data fetching moves out of JssContextService and into loader functions. These are Angular route resolvers registered with provideLoaderRegistry() and wired into routes with loaderResolver(). The same loaders run on the server (initial render) and the client (subsequent navigations, via a /_data endpoint).
  • SitecoreClient - A single client object exposes getPage, getPreview, getErrorPage, getDictiona ry, and getHeadLinks. It replaces the JSS GraphQLLayoutService / GraphQLDictionaryService factories.
  • Server middleware - multisite, personalization, bot tracking, and SXA redirects all run as Express middleware on the SSR host (server.ts) rather than as client-side components. Editing, experimental features, cache admin, revalidation, and the loader-data endpoint are also middleware. The host exports its Node request handler as the module's default export, in addition to the existing reqHandler named export.
  • Signals-based context - SitecoreContextService exposes the current page() and helpers like isEditing() as signals, replacing the RxJS state observable on JssContextService.
  • sitecore-tools CLI - sitecore.config.ts and sitecore.cli.config.ts drive configuration, the generated component map, sites.json, and metadata.json under .sitecore/.

Create a new Content SDK application

You can scaffold the new app with the latest version of the create-content-sdk-app package:

  1. Run the following command:

    npx create-content-sdk-app@latest
  2. When prompted to choose a template, choose angular.

  3. Install dependencies and confirm the app runs before migrating anything:

    
    npm install
    npm run dev
    

The generator creates a complete, runnable Content SDK for Angular app with the following:

  • Standalone bootstrap (app.config.ts / app.config.server.ts).
  • Routing with locale matching, loaders, and 404/500 error routes (app.routes.ts / app.routes.server.ts).
  • A SitecoreClient and out-of-the-box loaders under src/content-sdk/.
  • An Express SSR host (server.ts) with multisite, personalization, bot tracking, SXA redirects, editing, cache, and loader-data middleware already wired.
  • sitecore.config.ts/sitecore.cli.config.ts and the .sitecore/ generated files (component map, sites.json, metadata.json).
  • A set of out-of-the-box components (Image, Promo, RichText, Navigation, and so on).

Structure of the generated app

You do not have to author these files, but understanding the mapping from your JSS app makes the migration steps clearer.

  • Bootstrap - AppModule/AppServerModule are replaced by an ApplicationConfig (app.config.ts), which registers the router, HTTP client, Sitecore providers, the loader registry, the component map, and translation. provideSitecoreAngular() replaces both JssModule.withComponents(...) and the EDGE_CONFIG provider in a single call.

    // app.config.ts (generated)
    export const appConfig: ApplicationConfig = {
      providers: [
        provideHttpClient(withFetch()),
        provideRouter(routes, withNavigationErrorHandler(handleNavigationError())),
        provideSitecoreAngular({
          notFoundRoute: '/404',
          errorRoute: '/500',
          sitecoreConfig: scConfig,
          sitecoreClient: getClient(),
        }),
        provideLoaderRegistry(LOADERS),
        { provide: SITECORE_COMPONENT_MAP, useValue: componentMap },
        provideTranslateService({ loader: provideTranslateLoader(SitecoreTranslateLoader) }),
        { provide: UrlSerializer, useClass: LocaleUrlSerializer },
      ],
    };
  • Routing - the custom JSS UrlMatcher (jssRouteMatcher built on JssRouteBuilderService) is replaced by scLocaleMatcher() that provides locale-aware matching to expose locale as a route param.

    The JSS resolver (jssRouteResolver → JssContextService.changeRoute) is replaced by loaderResolver() that runs a named loader and places the result on route.data:

    // app.routes.ts (generated)
    export const routes: Routes = [
      ...errorRoutes, // /404, /500 (and :locale-prefixed)  loaderResolver('404'|'500')
      {
        matcher: scLocaleMatcher(scConfig.angular.locales),
        children: [
          {
            path: '**',
            component: PageComponent,
            resolve: { page: loaderResolver('page'), dictionary: loaderResolver('dictionary') },
          },
        ],
      },
    ];

    A companion app.routes.server.ts declares the 404/500 server routes so @angular/ssr returns the correct HTTP status.

  • SSR host - the integrated-mode server.bundle.ts plus the separate node-xmcloud-proxy project and proxy.conf.js are replaced by a single Express host, server.ts, built on @angular/ssr/node. It defines the SDK middleware plugins, including multisite, personalization, bot tracking, SXA redirects, editing (config, render, and experimental features), cache admin, revalidation, and loader-data, and exports the resulting request handler both as a named reqHandler export and as the module's default export. You only need to customize this file if you want to add additional middleware.

Configure your environment

Content SDK centralizes configuration in the sitecore.config.ts file, splits client and server values, and generates environment files. Environment variables change name and shape from the JSS environment.js/JssConfig.

Client-exposed values use the CSDK_PUBLIC_ prefix and are baked into src/environments/environment.ts by npm run gen:env:dev/gen:env:prod. Server-only values are read from process.env, loaded by src/load-env.ts. Create a copy of the .env.example file, rename it to .env, and fill in the appropriate values from SitecoreAI:

Old environment variable

New environment variable

SITECORE_API_KEY

CSDK_PUBLIC_SITECORE_API_KEY

SITECORE_API_HOST

CSDK_PUBLIC_SITECORE_API_HOST

SITECORE_SITE_NAME

CSDK_PUBLIC_DEFAULT_SITE_NAME

DEFAULT_LANGUAGE

CSDK_PUBLIC_DEFAULT_LANGUAGE

SITECORE_EDGE_CONTEXT_ID

SITECORE_EDGE_CONTEXT_ID

CSDK_PUBLIC_SITECORE_EDGE_CONTEXT_ID

SITECORE_EDGE_URL

CSDK_PUBLIC_SITECORE_EDGE_HOSTNAME

SITECORE_EXPERIENCE_EDGE_HOSTNAME

GRAPH_QL_ENDPOINT

No equivalent variable. Derived by the SDK from Edge/API config

PERSONALIZE_SCOPE

No equivalent variable. Derived from config.personalize.scope in sitecore.config.ts.

The following factories you used in JSS to build services from these values have no equivalent.

  • graphql-client-factory
  • layout-service-factory
  • dictionary-service-factory
  • src/app/lib/config.ts

Migrate your components

In JSS, components were declared in an auto-generated AppComponentsModule through JssModule.withComponents([...]). They used an @Input() rendering, templateUrl / *.html files, and standalone: false. The shared SxaComponent base class read rendering.params.

In Content SDK, components are standalone, declare their own imports, and are discovered by the sitecore-tools CLI into a generated .sitecore/component-map.ts.

The following example shows how new Angular practices can be applied to components:

@Component({
  selector: 'app-content-block',
  imports: [ScTextDirective, ScRichTextDirective],
  template: `
    <div class="contentBlock">
      <h2 class="contentTitle" *scText="headingField()"></h2>
      <div class="contentDescription">
        @if (contentField(); as content) {
          <div *scRichText="content"></div>
        }
      </div>
    </div>
  `,
})
export class ContentBlockComponent extends SxaComponent {
  readonly headingField = computed(() => /* ... */);
  readonly contentField = computed(() => /* ... */);
}
export default ContentBlockComponent;

The key component differences stem from both Angular and Content SDK changes:

  • Base class - new SxaComponent uses signal inputs and exposes fields(), params(), rendering(), renderingId(), and styles() as signals/computeds:

    export abstract class SxaComponent {
      readonly fields = input<Record<string, unknown>>();
      readonly params = input<Record<string, string>>();
      readonly rendering = input<ComponentRendering>();
      readonly renderingId = computedRenderingId(() => this.params());
      readonly styles = computed(() => this.params()?.Styles?.trim() ?? '');
    }

    Components receive fields/params as bound inputs from the placeholder, not a single rendering object.

  • Directives - *scText, *scRichText, *scImage, *scLink, and <sc-placeholder> come from @sitecore-content-sdk/angular and must be added to each component's imports array as they are no longer globally available through JssModule.

  • Variants - JSS selected template variants with @ViewChild/TemplateRef and rendering.params.FieldNames. Content SDK exports a named class per variant and lets the component map resolve them. For example:

    export { ImageDefaultComponent as Default, ImageBannerComponent as Banner };
  • Host bindings -SXA host classes and IDs move to the host ({ '[attr.class]': "...", '[attr.id]': 'renderingId()' }) and use signals (styles(), renderingId()).

  • Editing checks - replaces JssContextService.state.subscribe(... context.pageEditing ...) with inject(SitecoreContextService).isEditing(). See Read context data from SitecoreContextService for more information.

The auto-generated AppComponentsModule is replaced by .sitecore/component-map.ts, which sitecore-tools generates from the components under src/app/components. After adding or moving a component, regenerate the map with the following command:

npm run sitecore-tools:generate-map

The map is provided through SITECORE_COMPONENT_MAP in app.config.ts and passed to the editing config middleware in server.ts, both of which are already wired in the generated app.

Note

You can disable automatic component-map generation and maintain one manually. For more information, see Register a component in the component map.

Read context data

If your JSS components or services read from Sitecore context, you must migrate those reads. JSS exposed context as an RxJS observable (JssContextService.state) built on JssStateService<JssState>, plus a custom JssState class. Components subscribed to it and had to unsubscribe in ngOnDestroy.

Content SDK exposes context as signals through SitecoreContextService. There is no JssState, no changeRoute, and no manual subscription management. Derive view state with computed() and side effects with effect() instead of subscribing.

JSS (Old)

Content SDK (New)

JssContextService (including JssState and JssStateService)

SitecoreContextService

jssContext.state.subscribe(s => s.sitecore...)

context.page() (signal)

s.sitecore.context.pageEditing

editMode

context.isEditing()

page().mode.isEditing

s.sitecore.context.language

page().locale

s.sitecore.context.variantId

page().layout.sitecore.context.variantId

s.sitecore.route

page().layout.sitecore.route

jssContext.changeLanguage()/changeRoute()

Handled by loaders and LocaleUrlSerializer

JssMetaService/JssLinkService

Handled by the Title service and SitecoreClient.getHeadLinks()

For example, the JSS image component subscribed to context to compute an editing flag:

// JSS
this.contextSubscription = this.jssContext.state.subscribe((s) => {
  this.isEditing = s.sitecore && s.sitecore.context.pageEditing;
});

The Content SDK equivalent reads the signal directly inside a computed(), with no subscription to clean up:

// Content SDK
private readonly context = inject(SitecoreContextService);
readonly wrapWithLink = computed(() => !this.context.isEditing() && !!this.href()?.trim());

Language switching likewise moves from an RxJS subscription in AppComponent to an effect() in the root component that reads context.page()?.locale.

Fetch data with SitecoreClient and loaders

If you fetched layout, dictionary, or other Sitecore data directly (through JssLayoutService, JssContextService.changeRoute, or the GraphQL service factories), migrate that logic to SitecoreClient.

For one-off, non-route data fetches, you can call the getClient() method on getPage, getDictionary, getErrorPage, or getHeadLinks) directly:

import { getClient } from '../client/sitecore-client';
...
const scClient = getClient();
scClient.getPage(...) // get layout data for a specific page

Migrate existing internationalization implementation

JSS wired @ngx-translate to a custom JssTranslationLoaderService/JssTranslationClientLoaderService pair that called a GraphQLDictionaryService (created by dictionary-service-factory) and used TransferState to pass server data to the client. AppComponent switched languages by subscribing to JssContextService.state.

The new app provides translation out of the box through SitecoreTranslateLoader (registered in app.config.ts) and a dictionary route loader that calls getClient().getDictionary(...), keyed by the resolved site and locale. Language switching uses the signal-based effect described in Read context data.

Delete src/app/i18n/* and dictionary-service-factory.ts from your old code. Do not port them. If you used dictionary phrases in components, the lookup API (@ngx-translate translate pipe/service) is unchanged.

Obsolete and revised functionality

Some parts of existing JSS apps are either revised or no longer required in Content SDK apps, meaning they can be removed. However, if your app contains custom code that uses any of this obsolete functionality, or if you have any customizations in the affected files or folders, you will need to modify your app accordingly.

JSS (Old)

Content SDK (New)

AppModule/AppServerModule (NgModule)

app.config.ts/app.config.server.ts (ApplicationConfig)

RoutingModule

jssRouteMatcher

JssRouteBuilderService

app.route.ts

scLocaleMatcher()

LocaleUrlSerializer

jssRouteResolver (ResolveFn) → JssContextService.changeRoute

loaderResolver('page') and loader functions

JssLayoutService and layout-service-factory (GraphQLLayoutService)

SitecoreClient.getPage/getPreview/getErrorPage

dictionary-service-factory (GraphQLDictionaryService) and i18n/* loaders

Dictionary loader with SitecoreClient.getDictionary and SitecoreTranslateLoader

graphql-client-factory/GraphQLModule

Configured internally by SitecoreClient

JssContextService/JssState/JssStateService(RxJS)

SitecoreContextService (signals)

JssMetaService/JssLinkService

Title service + SitecoreClient.getHeadLinks()

AppComponentsModule (JssModule.withComponents)

.sitecore/component-map.ts + SITECORE_COMPONENT_MAP

EDGE_CONFIG provider

provideSitecoreAngular({ sitecoreConfig, sitecoreClient })

@Input() rendering, templateUrl, standalone: false

Signal input(), inline template, standalone

CloudSdkInitComponent, @sitecore-cloudsdk/* (browser)

createPersonalizeMiddleware and SITECORE_ANALYTICS

CdpPageViewComponent (RxJS subscribe and pageView)

CdpPageViewComponent (effect and SITECORE_ANALYTICS.pageView)

LayoutComponent with LayoutState enum

PageComponent, LayoutComponent with /404, /500 routes

server.bundle.ts (integrated mode) with node-xmcloud-proxy and proxy.conf.js

server.ts (Express with @angular/ssr/node and SDK middleware)

environment.js

JssConfig

src/app/lib/config.ts

sitecore.config.ts with CSDK_PUBLIC_* environment variables and load-env.ts

jss CLI and @sitecore-jss/...-schematics (jss scaffold)

sitecore-tools (generate-map, build) and sitecore.cli.config.ts

No in-app multisite

createMultisiteMiddleware and the generated .sitecore/sites.json

No loader cache

createLoaderCache and revalidate

Removal of Experience Editor

The Content SDK does not support the Experience Editor. During migration, drop anything in your JSS app that exists solely to support it.

The following table shows how JSS packages map to Content SDK packages:

JSS (Old)

Content SDK (New)

@sitecore-jss/sitecore-jss-angular

@sitecore-content-sdk/angular

@sitecore-jss/sitecore-jss

Folded into @sitecore-content-sdk/angular/@sitecore-content-sdk/content

@sitecore-cloudsdk/core/browser

@sitecore-cloudsdk/events/browser (client initialization)

Server analytics via SITECORE_ANALYTICS and the personalize middleware

@sitecore-jss/sitecore-jss-angular-schematics (jss scaffold)

sitecore-tools component map generation

Optional steps

The following steps apply only if you customized personalization, multisite, or other middleware in the proxy companion application in JSS. Otherwise, you can skip this section.

Migrate personalization

In the JSS app, personalization was driven from the client: CloudSdkInitComponent called CloudSDK({...}).addEvents().initialize() in the browser, and CdpPageViewComponent subscribed to context state and called pageView(...) from @sitecore-cloudsdk/events/browser. Variant resolution depended on the separate Node proxy.

The new app handles personalization out of the box in two places:

  1. Variant resolution and server middleware - createPersonalizeMiddleware(...) in server.ts identifies page/component variants through Sitecore CDP and writes them onto req.scParams. The page loader reads them with getVariantId(context)/getComponentVariantIds(context) and passes them to getClient().getPage(..., { personalize: { ... } }). This is already wired; configure it through config.personalize rather than in components.

  2. Page-view analytics and SITECORE_ANALYTICS facade - The generated CdpPageViewComponent is template-less, reads SitecoreContextService.page() in an effect(), and dispatches through the injected SITECORE_ANALYTICS facade. The Cloud SDK is no longer initialized by hand in the browser:

    export class CdpPageViewComponent {
      private readonly analytics = inject(SITECORE_ANALYTICS);
      private readonly context = inject(SitecoreContextService);
      constructor() {
        effect(() => {
          const page = this.context.page();
          if (!page || !page.mode.isNormal) return;
          const route = page.layout.sitecore.route;
          if (!route?.itemId) return;
          const pageVariantId = CdpHelper.getPageVariantId(
            route.itemId, page.locale || config.defaultLanguage,
            page.layout.sitecore.context.variantId as string, config.personalize.scope
          );
          void this.analytics.pageView({ channel: 'WEB', currency: 'USD', page: route.name, pageVariantId, language: page.locale });
        });
      }
    }

    Remove CloudSdkInitComponent, ScriptsModule/ScriptsComponent, and the @sitecore-cloudsdk/* browser imports from your old code. Do not port them. If you had custom event logic (extra events, consent handling), reimplement it against SITECORE_ANALYTICS.

Note

Personalize requires Edge configuration (context ID/client context ID) and does not run against local containers. The middleware disables itself when that configuration is missing, which is expected in local development.

Migrate multisite and other middleware customizations

The JSS Angular starter was effectively single-site: sitecoreSiteName came from the environment, and multisite resolution was handled outside the Angular app by the node-xmcloud-proxy companion app. There was no in-app site resolver.

The new app provides multisite support out-of-the-box with createMultisiteMiddleware(...) plus a generated .sitecore/sites.json file produced by the sitecore-tools build command. It resolves the site per request (sc_site query → cookie → hostname → default), writes it onto req.scParams, and loaders read it with getSiteName(context). This works out of the box. If you previously had no multisite logic, keep this as is.

The new app also wires up the following middleware out of the box, so you do not need to reimplement them unless you had custom logic beyond what they provide:

  • createBotTrackingMiddleware(...) - detects bots by User-Agent, sets the sc_bot cookie, and sends a dedicated bot page-view event. It does not run in dev/localhost environments.
  • createRedirectsMiddleware(...) - matches each request against the site's Sitecore redirects (locale, static, and regex rules) and issues a 301/302 redirect or an internal server-transfer rewrite. If your old JSS setup had custom redirect logic in the node-xmcloud-proxy companion app or elsewhere, review whether your Sitecore redirect items already cover it before writing custom code.
  • createExperimentalFeaturesMiddleware() - exposes an /api/editing/experimental endpoint that reports which Content SDK experimental features are available and enabled.

If you had other custom middleware in your old setup (custom headers, host rules, or extra proxy behavior not covered by the middleware above), reimplement it in server.ts. The host exposes a shared middlewareMatcher that controls which requests the request-scoped middlewares process. The SDK already skips /api/*, /sitecore/*, static files, and editing/preview requests:

const middlewareMatcher = {
  excludePaths: ['/healthz', '/metrics', /\.[^/]+$/],
  // includePaths: [/^\/[a-z]{2}(-[A-Z]{2})?(\/|$)/], // restrict to locale-prefixed routes
};

The order of the middleware matters:

  • createExperimentalFeaturesMiddleware runs between createEditingConfigMiddleware and createEditingRenderMiddleware.
  • createMultisiteMiddleware must run before createBotTrackingMiddleware, createRedirectsMiddleware, and createPersonalizeMiddleware, all of which depend on the resolved site.
  • createBotTrackingMiddleware must run before createPersonalizeMiddleware so the bot cookie is set before personalize decides whether to skip the request.
  • createRedirectsMiddleware must run before createPersonalizeMiddleware so a redirect can short-circuit the request before a CDP call is made.

Add your custom middleware around these, not in place of them.

Next steps

To finalize the upgrade process, make sure you resolve any errors and warnings you encounter. Use the following items as a checklist to verify all the functionality:

  • Bring your components across one at a time. Convert each component to standalone with signal inputs, add the required *sc* directives to imports, and run npm run sitecore-tools:generate-map so it appears in the .sitecore/component-map.ts file.
  • Run npm run dev and verify routing, locale prefixes, dictionary phrases, and editing in Sitecore Pages (metadata mode).
  • Configure your sites and verify multisite resolution against .sitecore/sites.json.
  • Configure Edge (context ID/client context ID) and verify personalization and CDP page-view events in a deployed environment as these do not run against local containers.
  • Review the Loaders cache layer and tags revalidation topic. Consider using hooks and the POST /api/revalidate endpoint for your hosting environment.
If you have suggestions for improving this article, let us know!