---
title: "Upgrade JSS 22.x Angular apps to Content SDK 1.0"
slug: "upgrade-jss-22-x-angular-apps-to-content-sdk-1-0"
abstract: "How to upgrade your JSS 22.x Angular apps to Content SDK 1.0."
---
# Upgrade JSS 22.x Angular apps to Content SDK 1.0

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](https://angular.dev/guide/signals).

This topic covers the following:

:::walkthrough
1. [What's new in Content SDK for Angular 1.0?](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#whats-new-in-content-sdk-for-angular-10)
2. [Create a new Content SDK application](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#create-a-new-content-sdk-application)
3. [Configure your environment](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#configure-your-environment)
4. [Migrate your components](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#migrate-your-components)
5. [Read context data](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#read-context-data)
6. [Fetch data with SitecoreClient and loaders](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#fetch-data-with-sitecoreclient-and-loaders)
7. [Migrate existing internationalization implementation](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#migrate-existing-internationalization-implementation)
8. [Obsolete and revised functionality](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#obsolete-and-revised-functionality)
9. [Optional steps](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#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:

    ```text
    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.

    ```ts
    // 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`:

    ```csharp
    // 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:

<table style="width:100%;" class="informaltable frame-void rules-rows"><thead><tr><th class="th"><p>Old environment variable</p></th><th class="th"><p>New environment variable</p></th></tr></thead><tbody><tr><td class="td"><p><code class="code">SITECORE_API_KEY</code></p></td><td class="td"><p><code class="code">CSDK_PUBLIC_SITECORE_API_KEY</code></p></td></tr><tr><td class="td"><p><code class="code">SITECORE_API_HOST</code></p></td><td class="td"><p><code class="code">CSDK_PUBLIC_SITECORE_API_HOST</code></p></td></tr><tr><td class="td"><p><code class="code">SITECORE_SITE_NAME</code></p></td><td class="td"><p><code class="code">CSDK_PUBLIC_DEFAULT_SITE_NAME</code></p></td></tr><tr><td class="td"><p><code class="code">DEFAULT_LANGUAGE</code></p></td><td class="td"><p><code class="code">CSDK_PUBLIC_DEFAULT_LANGUAGE</code></p></td></tr><tr><td class="td"><p><code class="code">SITECORE_EDGE_CONTEXT_ID</code></p></td><td class="td"><p><code class="code">SITECORE_EDGE_CONTEXT_ID</code></p><p><code class="code">CSDK_PUBLIC_SITECORE_EDGE_CONTEXT_ID</code></p></td></tr><tr><td class="td"><p><code class="code">SITECORE_EDGE_URL</code></p></td><td class="td"><p><code class="code">CSDK_PUBLIC_SITECORE_EDGE_HOSTNAME</code></p><p><code class="code">SITECORE_EXPERIENCE_EDGE_HOSTNAME</code></p></td></tr><tr><td class="td"><p><code class="code">GRAPH_QL_ENDPOINT</code></p></td><td class="td"><p>No equivalent variable. Derived by the SDK from Edge/API config</p></td></tr><tr><td class="td"><p><code class="code">PERSONALIZE_SCOPE</code></p></td><td class="td"><p>No equivalent variable. Derived from <code class="code">config.personalize.scope</code> in <code class="code">sitecore.config.ts</code>.</p></td></tr></tbody></table>

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:

```ts
@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:

    ```ts
    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:

    ```ts
    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:

```shell
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](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/register-a-component-in-the-component-map.html "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.

<table style="width:100%;" class="informaltable frame-void rules-rows"><thead><tr><th class="th"><p>JSS (Old)</p></th><th class="th"><p>Content SDK (New)</p></th></tr></thead><tbody><tr><td class="td"><p><code class="code">JssContextService</code> (including <code class="code">JssState</code> and <code class="code">JssStateService</code>)</p></td><td class="td"><p><code class="code">SitecoreContextService</code></p></td></tr><tr><td class="td"><p><code class="code">jssContext.state.subscribe(s => s.sitecore...)</code></p></td><td class="td"><p><code class="code">context.page()</code> (signal)</p></td></tr><tr><td class="td"><p><code class="code">s.sitecore.context.pageEditing</code></p><p><code class="code">editMode</code></p></td><td class="td"><p><code class="code">context.isEditing()</code></p><p><code class="code">page().mode.isEditing</code></p></td></tr><tr><td class="td"><p><code class="code">s.sitecore.context.language</code></p></td><td class="td"><p><code class="code">page().locale</code></p></td></tr><tr><td class="td"><p><code class="code">s.sitecore.context.variantId</code></p></td><td class="td"><p><code class="code">page().layout.sitecore.context.variantId</code></p></td></tr><tr><td class="td"><p><code class="code">s.sitecore.route</code></p></td><td class="td"><p><code class="code">page().layout.sitecore.route</code></p></td></tr><tr><td class="td"><p><code class="code">jssContext.changeLanguage()</code>/<code class="code">changeRoute()</code></p></td><td class="td"><p>Handled by loaders and <code class="code">LocaleUrlSerializer</code></p></td></tr><tr><td class="td"><p><code class="code">JssMetaService</code>/<code class="code">JssLinkService</code></p></td><td class="td"><p>Handled by the <code class="code">Title</code> service and <code class="code">SitecoreClient.getHeadLinks()</code></p></td></tr></tbody></table>

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

```csharp
// 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:

```csharp
// 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:

```javascript
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](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/upgrade-jss-22-x-angular-apps-to-content-sdk-1-0.html#UUID-3e25af1b-c7c1-f556-a114-d0a01e204f07_section-id235684068431791 "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.

<table style="width:100%;" class="informaltable frame-void rules-rows"><thead><tr><th class="th"><p>JSS (Old)</p></th><th class="th"><p>Content SDK (New)</p></th></tr></thead><tbody><tr><td class="td"><p><code class="code">AppModule</code>/<code class="code">AppServerModule</code> (<code class="code">NgModule</code>)</p></td><td class="td"><p><code class="code">app.config.ts</code>/<code class="code">app.config.server.ts</code> (<code class="code">ApplicationConfig</code>)</p></td></tr><tr><td class="td"><p><code class="code">RoutingModule</code></p><p><code class="code">jssRouteMatcher </code></p><p><code class="code">JssRouteBuilderService</code></p></td><td class="td"><p><code class="code">app.route.ts</code></p><p><code class="code">scLocaleMatcher() </code></p><p><code class="code">LocaleUrlSerializer</code></p></td></tr><tr><td class="td"><p><code class="code">jssRouteResolver (ResolveFn) → JssContextService.changeRoute</code></p></td><td class="td"><p><code class="code">loaderResolver('page')</code> and loader functions</p></td></tr><tr><td class="td"><p><code class="code">JssLayoutService</code> and <code class="code">layout-service-factory</code> (<code class="code">GraphQLLayoutService</code>)</p></td><td class="td"><p><code class="code">SitecoreClient</code>.<code class="code">getPage</code>/<code class="code">getPreview</code>/<code class="code">getErrorPage</code></p></td></tr><tr><td class="td"><p><code class="code">dictionary-service-factory</code> (<code class="code">GraphQLDictionaryService</code>) and <code class="code">i18n/*</code> loaders</p></td><td class="td"><p>Dictionary loader with <code class="code">SitecoreClient.getDictionary</code> and <code class="code">SitecoreTranslateLoader</code></p></td></tr><tr><td class="td"><p><code class="code">graphql-client-factory</code>/<code class="code">GraphQLModule</code></p></td><td class="td"><p>Configured internally by <code class="code">SitecoreClient</code></p></td></tr><tr><td class="td"><p><code class="code">JssContextService</code>/<code class="code">JssState</code>/<code class="code">JssStateService</code>(<code class="code">RxJS</code>)</p></td><td class="td"><p><code class="code">SitecoreContextService</code> (signals)</p></td></tr><tr><td class="td"><p><code class="code">JssMetaService</code>/<code class="code">JssLinkService</code></p></td><td class="td"><p><code class="code">Title</code> service + <code class="code">SitecoreClient.getHeadLinks()</code></p></td></tr><tr><td class="td"><p><code class="code">AppComponentsModule</code> (JssModule.withComponents)</p></td><td class="td"><p>.sitecore/component-map.ts + SITECORE_COMPONENT_MAP</p></td></tr><tr><td class="td"><p><code class="code">EDGE_CONFIG</code> provider</p></td><td class="td"><p>provideSitecoreAngular({ sitecoreConfig, sitecoreClient })</p></td></tr><tr><td class="td"><p><code class="code">@Input() rendering</code>, <code class="code">templateUrl</code>, <code class="code">standalone: false</code></p></td><td class="td"><p>Signal <code class="code">input()</code>, inline template, standalone</p></td></tr><tr><td class="td"><p><code class="code">CloudSdkInitComponent</code>, <code class="code">@sitecore-cloudsdk/*</code> (browser)</p></td><td class="td"><p><code class="code">createPersonalizeMiddleware</code> and <code class="code">SITECORE_ANALYTICS</code></p></td></tr><tr><td class="td"><p><code class="code">CdpPageViewComponent</code> (<code class="code">RxJS</code> subscribe and <code class="code">pageView</code>)</p></td><td class="td"><p><code class="code">CdpPageViewComponent</code> (effect and <code class="code">SITECORE_ANALYTICS.pageView</code>)</p></td></tr><tr><td class="td"><p><code class="code">LayoutComponent</code> with <code class="code">LayoutState</code> enum</p></td><td class="td"><p><code class="code">PageComponent</code>, <code class="code">LayoutComponent</code> with <code class="code">/404</code>, <code class="code">/500</code> routes</p></td></tr><tr><td class="td"><p><code class="code">server.bundle.ts</code> (integrated mode) with <code class="code">node-xmcloud-proxy</code> and <code class="code">proxy.conf.js</code></p></td><td class="td"><p><code class="code">server.ts</code> (Express with <code class="code">@angular/ssr/node</code> and SDK middleware)</p></td></tr><tr><td class="td"><p><code class="code">environment.js</code></p><p><code class="code">JssConfig </code></p><p><code class="code">src/app/lib/config.ts</code></p></td><td class="td"><p><code class="code">sitecore.config.ts</code> with <code class="code">CSDK_PUBLIC_*</code> environment variables and <code class="code">load-env.ts</code></p></td></tr><tr><td class="td"><p><code class="code">jss</code> CLI and <code class="code">@sitecore-jss/...-schematics</code> (<code class="code">jss scaffold</code>)</p></td><td class="td"><p><code class="code">sitecore-tools</code> (<code class="code">generate-map</code>, <code class="code">build</code>) and <code class="code">sitecore.cli.config.ts</code></p></td></tr><tr><td class="td"><p>No in-app multisite</p></td><td class="td"><p><code class="code">createMultisiteMiddleware</code> and the generated <code class="code">.sitecore/sites.json</code></p></td></tr><tr><td class="td"><p>No loader cache</p></td><td class="td"><p><code class="code">createLoaderCache</code> and <code class="code">revalidate</code></p></td></tr></tbody></table>

### 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:

<table style="width:100%;" class="informaltable frame-void rules-rows"><thead><tr><th class="th"><p>JSS (Old)</p></th><th class="th"><p>Content SDK (New)</p></th></tr></thead><tbody><tr><td class="td"><p><code class="code">@sitecore-jss/sitecore-jss-angular</code></p></td><td class="td"><p><code class="code">@sitecore-content-sdk/angular</code></p></td></tr><tr><td class="td"><p><code class="code">@sitecore-jss/sitecore-jss</code></p></td><td class="td"><p>Folded into <code class="code">@sitecore-content-sdk/angular</code>/<code class="code">@sitecore-content-sdk/content</code></p></td></tr><tr><td class="td"><p><code class="code">@sitecore-cloudsdk/core/browser</code></p><p><code class="code">@sitecore-cloudsdk/events/browser</code> (client initialization)</p></td><td class="td"><p>Server analytics via <code class="code">SITECORE_ANALYTICS</code> and the personalize middleware</p></td></tr><tr><td class="td"><p><code class="code">@sitecore-jss/sitecore-jss-angular-schematics</code> (<code class="code">jss scaffold</code>)</p></td><td class="td"><p><code class="code">sitecore-tools</code> component map generation</p></td></tr></tbody></table>

## 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:

    ```csharp
    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:

```ts
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](https://doc.sitecore.com/sai/en/developers/content-sdk/angular/1x/loaders-cache-layer-and-tags-revalidation.html "Loaders cache layer and tags revalidation") topic. Consider using hooks and the POST `/api/revalidate` endpoint for your hosting environment.