create-content-sdk-app 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,6 @@ The functionality available in Content SDK Angular right now includes:
10
10
 
11
11
  What can you expect coming in the future ahead of the 1.0 release:
12
12
  - SXA Redirects support
13
- - More varied hosting options
14
13
  - Ongoing optimizations and bug fixes
15
14
 
16
15
  ## Deployment
@@ -32,10 +31,126 @@ But, temporarily, we ask you to perform some additional actions post-deploy in y
32
31
 
33
32
  These variables are needed to ensure SAI backend infrastructure correctly sets up and uses your Angular sample for Pages editing and preview. They will be switched to be set automatically between now and the 1.0 release.
34
33
 
35
- ### Deploying to Netlify
34
+ ## Non-Sitecore AI deployments
36
35
 
37
- Coming soon
36
+ For both deploying to Netlify and Vercel, ensure your `src/server.ts` file has the default export for the request handler:
37
+ ```
38
+ export default reqHandler;
39
+ ```
38
40
 
39
41
  ### Deploying to Vercel
40
42
 
41
- Coming soon
43
+ Content SDK for Angular sample uses ExpressJS extensively to enhance the base Angular capabilities with Sitecore AI features.
44
+ Since Express is being used, and since Vercel has no Angular SSR support, you need to create a Vercel function file in order for Vercel to correctly serve the application.
45
+ For example, create a `vercel/index.mjs` file and import the Angular's compiled `server.mjs`output:
46
+ ```
47
+ export { default } from '../dist/<your_app_name>/server/server.mjs';
48
+ ```
49
+ where `<your_app_name>` is the name of application set in `package.json`.
50
+
51
+ After that, configure redirect rules for Vercel request to reach your app code, via `vercel.json` file:
52
+ ```
53
+ {
54
+ "$schema": "https://openapi.vercel.sh/vercel.json",
55
+ "buildCommand": "npm run build",
56
+ "outputDirectory": "dist/<your_app_name>/browser",
57
+ "rewrites": [
58
+ {
59
+ "source": "/(.*)",
60
+ "destination": "/vercel/index"
61
+ }
62
+ ],
63
+ "functions": {
64
+ "vercel/index.mjs": {
65
+ "includeFiles": "dist/<your_app_name>/**"
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ 3. In Vercel portal, set the required Content SDK environment variables. Additionally, set `NG_ALLOWED_HOSTS` environment variable to allow Vercel URLs:
72
+ ```
73
+ NG_ALLOWED_HOSTS=*.vercel.app`
74
+ ```
75
+
76
+ Deploying your app to Vercel should now result in the site being correctly served.
77
+
78
+ ### Deploying to Netlify
79
+
80
+ As ExpressJS is being used by Content SDK for Angular, the natural instinct would be to follow the [express guide from Netlify](https://docs.netlify.com/build/frameworks/framework-setup-guides/express/#deploy-your-express-app-with-netlify-cli)
81
+ Unfortunately, this will result in your Netlify functions being compiled as CJS and some built in functionality of Angular (like `import.meta` values) being unavailable.
82
+ In order to build the function with Netlify's latest v2 API and have it compiled as ESM, some hoop jumps are required, using the express deployment guide as basis.
83
+
84
+ 1. `serverless-http` module provides the required bridge between Express and Netlify, so you should install it alongside few additional dependencies that your Netlify function will use:
85
+ ```
86
+ npm i express serverless-http @netlify/functions @types/express
87
+ ```
88
+ 2. Create a function implementation file that will import Angular's SSR artifacts and transform them to be used by `serverless-http` and v2 functions. These transformations are needed for Netlify CLI to correctly identify the function as ESM-compatible. The below example is for a TypeScript implementaion. We assume this function is located in the `netlify/functions/server.mts` file:
89
+ ```
90
+ import serverless from 'serverless-http';
91
+ import type { Context } from '@netlify/functions';
92
+ import { default as app } from '../../dist/<your_app_name>/server/server.mjs';
93
+
94
+ const handler = serverless(app);
95
+ const FUNCTION_BASE = '/.netlify/functions/server';
96
+
97
+ /** serverless-http's result for a Lambda Function URL ("2.0") event. */
98
+ interface LambdaResult {
99
+ statusCode: number;
100
+ headers: Record<string, string>;
101
+ cookies: string[];
102
+ body: string;
103
+ isBase64Encoded: boolean;
104
+ }
105
+
106
+ export default async (request: Request, context: Context) => {
107
+ const url = new URL(request.url);
108
+ // The netlify.toml rewrite passes the original path; direct invocations
109
+ // arrive prefixed with the function base path — strip it.
110
+ const path = url.pathname.startsWith(FUNCTION_BASE)
111
+ ? url.pathname.slice(FUNCTION_BASE.length) || '/'
112
+ : url.pathname;
113
+
114
+ // Lambda Function URL ("2.0") event — the leanest shape serverless-http accepts
115
+ const lambdaRequest = {
116
+ version: '2.0',
117
+ rawPath: path,
118
+ rawQueryString: url.searchParams.toString(),
119
+ headers: Object.fromEntries(request.headers),
120
+ requestContext: { http: { method: request.method } },
121
+ body: Buffer.from(await request.arrayBuffer()),
122
+ };
123
+ const result = (await handler(lambdaRequest, context)) as LambdaResult;
124
+
125
+ // set-cookie comes back separately; `|| null` keeps 204/304 responses body-less.
126
+ const headers = new Headers(result.headers);
127
+ for (const cookie of result.cookies) headers.append('set-cookie', cookie);
128
+ return new Response(
129
+ result.isBase64Encoded ? Buffer.from(result.body, 'base64') : result.body || null,
130
+ { status: result.statusCode, headers }
131
+ );
132
+ };
133
+ ```
134
+ where `<your_app_name>` is the name of application set in `package.json`.
135
+
136
+ 3. Finally, create the `netlify.toml` file to correctly configure Netlify in handling request to your function:
137
+ ```
138
+ [build]
139
+ command = "npm run build"
140
+ publish = "dist/content-sdk-angular/browser"
141
+
142
+ [functions.server]
143
+ # Ship the full dist folder so express.static and any runtime file access work.
144
+ included_files = ["dist/content-sdk-angular/**"]
145
+
146
+ [[redirects]]
147
+ from = "/*"
148
+ status = 200
149
+ to = "/.netlify/functions/server/:splat"
150
+
151
+ ```
152
+
153
+ 4. In Netlify portal, set the required Content SDK environment variables. Additionally, set `NG_ALLOWED_HOSTS` environment variable to allow netlify URLs:
154
+ ```
155
+ NG_ALLOWED_HOSTS=*.netlify.app`
156
+ ```
@@ -4,7 +4,7 @@ import { ScTextDirective, ScRichTextDirective, TextField } from '@sitecore-conte
4
4
  import { SxaComponent } from './content-sdk/sxa.component';
5
5
 
6
6
  /**
7
- * Parity with kit-nextjs-skate-park ContentBlock (minimal section, no Sitecore chrome wrapper).
7
+ * Minimal content section (no Sitecore chrome wrapper).
8
8
  */
9
9
  @Component({
10
10
  selector: 'app-content-block',
@@ -1,4 +1,4 @@
1
- /** Minimal JSON-LD builders aligned with kit-nextjs-skate-park structured data. */
1
+ /** Minimal JSON-LD builders for structured data. */
2
2
 
3
3
  export type ArticleJsonLd = {
4
4
  '@context': 'https://schema.org';
@@ -3,7 +3,7 @@ import { computed, type Signal } from '@angular/core';
3
3
  type LayoutParams = { RenderingIdentifier?: string; Styles?: string };
4
4
 
5
5
  /**
6
- * CSS class/id helpers aligned with kit-nextjs-skate-park component wrappers.
6
+ * CSS class/id helpers for component wrappers.
7
7
  */
8
8
  export function computedRenderingId(
9
9
  params: () => { [key: string]: string } | undefined,
@@ -2,7 +2,7 @@ import { Component, computed, input } from '@angular/core';
2
2
  import { ComponentRendering, ScPlaceholderComponent } from '@sitecore-content-sdk/angular';
3
3
 
4
4
  /**
5
- * Parity with kit-nextjs-skate-park PartialDesignDynamicPlaceholder:
5
+ * Partial design dynamic placeholder:
6
6
  * renders a single placeholder whose name comes from rendering.params.sig.
7
7
  */
8
8
  @Component({
@@ -1,7 +1,7 @@
1
1
  import { Component, ElementRef, Renderer2, effect, inject, input } from '@angular/core';
2
2
 
3
3
  /**
4
- * Emits a JSON-LD script tag (kit-nextjs-skate-park StructuredData parity).
4
+ * Emits a JSON-LD script tag.
5
5
  * Uses the DOM API so Angular does not strip script tags from templates.
6
6
  */
7
7
  @Component({
@@ -6,7 +6,7 @@ import { LayoutComponent } from '../shared/layout.component';
6
6
 
7
7
  /**
8
8
  * 500 error page. Uses the Sitecore server-error page when the loader returns layout data;
9
- * otherwise shows a static fallback (aligned with kit-nextjs global-error behavior).
9
+ * otherwise shows a static fallback.
10
10
  */
11
11
  @Component({
12
12
  selector: 'app-error',
@@ -7,7 +7,7 @@ import { LayoutComponent } from '../shared/layout.component';
7
7
 
8
8
  /**
9
9
  * 404 page. Uses the Sitecore not-found page when the loader returns layout data;
10
- * otherwise shows a static fallback (aligned with kit-nextjs not-found behavior).
10
+ * otherwise shows a static fallback.
11
11
  */
12
12
  @Component({
13
13
  selector: 'app-404',
@@ -9,7 +9,7 @@ import express from 'express';
9
9
  import { join } from 'node:path';
10
10
  import memoryDriver from 'unstorage/drivers/memory';
11
11
  import {
12
- createCacheAdminMiddleware,
12
+ createBotTrackingMiddleware,
13
13
  createEditingConfigMiddleware,
14
14
  createEditingRenderMiddleware,
15
15
  createLoaderCache,
@@ -17,6 +17,7 @@ import {
17
17
  createRobotsMiddleware,
18
18
  createMultisiteMiddleware,
19
19
  createPersonalizeMiddleware,
20
+ createRedirectsMiddleware,
20
21
  createSitecoreRevalidateMiddleware,
21
22
  createSitemapMiddleware,
22
23
  } from '@sitecore-content-sdk/angular';
@@ -76,9 +77,6 @@ app.use(
76
77
  })
77
78
  );
78
79
 
79
- /** Admin endpoints for cache inspection and invalidation (see `/api/_cache`). */
80
- app.use(createCacheAdminMiddleware({ cache: loaderCache, endpoint: '/api/_cache' }));
81
-
82
80
  /**
83
81
  * Editing config endpoint (`/api/editing/config`). Replies with the registered
84
82
  * component map keys and `editMode: 'metadata'` so Sitecore Pages can negotiate
@@ -99,8 +97,8 @@ app.use(
99
97
  app.use(createEditingRenderMiddleware());
100
98
 
101
99
  /**
102
- * Shared path matcher for the request-scoped middlewares (multisite + personalize, and any
103
- * future redirects middleware). It decides which requests these middlewares act on.
100
+ * Shared path matcher for the request-scoped middlewares (multisite, redirects and personalize).
101
+ * It decides which requests these middlewares act on.
104
102
  *
105
103
  * Patterns are exact strings or RegExp. The SDK already skips API routes (`/api/*`), Sitecore
106
104
  * routes (`/sitecore/*`), static files (any path whose last segment has an extension) and
@@ -129,10 +127,45 @@ app.use(
129
127
  })
130
128
  );
131
129
 
130
+ /*
131
+ * Bot tracking middleware. Detects bots by User-Agent, sets the `sc_bot` cookie, and sends a
132
+ * dedicated bot page-view event. Must run before personalize so the bot cookie is set before
133
+ * personalize decides whether to skip. Does not run in dev/localhost environments.
134
+ */
135
+ app.use(
136
+ createBotTrackingMiddleware({
137
+ ...config.api.edge,
138
+ locales: config.angular.locales,
139
+ defaultLanguage: config.defaultLanguage,
140
+ defaultSite: config.defaultSite,
141
+ matcher: middlewareMatcher,
142
+ })
143
+ );
144
+
145
+ /**
146
+ * Redirects middleware. Matches each request against the site's Sitecore redirects (locale,
147
+ * static and regex rules) and issues a 301/302 redirect or an internal server-transfer rewrite.
148
+ * Runs after multisite (which resolves the site it fetches redirects for) and before personalize
149
+ * so a redirect short-circuits the request before a CDP call is made.
150
+ */
151
+ app.use(
152
+ createRedirectsMiddleware({
153
+ ...config.redirects,
154
+ ...config.api.edge,
155
+ ...(config.api.local ?? {}),
156
+ ...config.angular,
157
+ sites,
158
+ defaultLanguage: config.defaultLanguage,
159
+ defaultSite: config.defaultSite,
160
+ matcher: middlewareMatcher,
161
+ })
162
+ );
163
+
132
164
  /**
133
165
  * Personalize middleware. Identifies page/component variants for the request via
134
166
  * Sitecore CDP and writes them onto `req.scParams` so the page loader fetches the
135
- * personalized layout and the loader cache keys per variant.
167
+ * personalized layout and the loader cache keys per variant. Skips bot requests marked
168
+ * by the bot tracking middleware (`skipForBot`, default true).
136
169
  *
137
170
  * NOTE: Personalize requires Edge configuration (contextId/clientContextId) and
138
171
  * cannot work with local containers
@@ -198,3 +231,4 @@ if (isMainModule(import.meta.url)) {
198
231
  * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
199
232
  */
200
233
  export const reqHandler = createNodeRequestHandler(app);
234
+ export default reqHandler;
@@ -4,4 +4,11 @@ import { defineConfig } from '@sitecore-content-sdk/nextjs/config';
4
4
  * See the documentation for `defineConfig`:
5
5
  * https://doc.sitecore.com/xmc/en/developers/content-sdk/the-sitecore-configuration-file.html
6
6
  */
7
- export default defineConfig({});
7
+ export default defineConfig({
8
+ redirects: {
9
+ // Locale prefix strategy for App Router redirect targets, matching next-intl's
10
+ // `localePrefix` in src/i18n/routing.ts. `as-needed` keeps the default locale bare
11
+ // and only prefixes non-default locales. Use `always` if routing prefixes every locale.
12
+ appLocalePrefix: 'as-needed',
13
+ },
14
+ });
@@ -8,8 +8,9 @@ export const routing = defineRouting({
8
8
  // Used when no locale matches
9
9
  defaultLocale: sitecoreConfig.defaultLanguage,
10
10
 
11
- // No prefix is added for the default locale ("as-needed").
11
+ // Out of the box, syncs with the `appLocalePrefix` redirects setting for consistent behavior
12
+ // For "as-needed" value, no prefix is added for the default locale .
12
13
  // For other configuration options, refer to the next-intl documentation:
13
14
  // https://next-intl.dev/docs/routing/configuration
14
- localePrefix: 'as-needed',
15
+ localePrefix: sitecoreConfig.redirects?.appLocalePrefix || 'as-needed',
15
16
  });
@@ -14,4 +14,10 @@ export default defineConfig({
14
14
  enabled: false,
15
15
  },
16
16
  },
17
+ redirects: {
18
+ // Locale prefix strategy for App Router redirect targets, matching next-intl's
19
+ // `localePrefix` in src/i18n/routing.ts. `as-needed` keeps the default locale bare
20
+ // and only prefixes non-default locales. Use `always` if routing prefixes every locale.
21
+ appLocalePrefix: 'as-needed',
22
+ },
17
23
  });
@@ -29,20 +29,13 @@ export default async function Page({ params, searchParams }: PageProps) {
29
29
  notFound();
30
30
  }
31
31
 
32
- // Cached fetch first so missing routes can notFound() without dynamic APIs in the ancestor tree.
33
- const cachedPage = await getSitecorePage({ site, locale, path: path ?? [] });
34
-
35
- if (!cachedPage) {
36
- setCachedPageParams({ site, locale });
37
- notFound();
38
- }
39
-
40
32
  // Set site and locale to be available in src/i18n/request.ts for fetching the dictionary
41
33
  setRequestLocale(`${site}_${locale}`);
42
34
 
35
+ // Draft/preview first so editing is not blocked by locale-dependent cached lookups.
36
+ // Editing often resolves language via query string, while [locale] may fall back to defaultLanguage.
43
37
  const draft = await draftMode();
44
38
 
45
- // Fetch the page data from Sitecore
46
39
  let page;
47
40
  if (draft.isEnabled) {
48
41
  const editingParams = await searchParams;
@@ -52,7 +45,7 @@ export default async function Page({ params, searchParams }: PageProps) {
52
45
  page = await client.getPreview(editingParams);
53
46
  }
54
47
  } else {
55
- page = cachedPage;
48
+ page = await getSitecorePage({ site, locale, path: path ?? [] });
56
49
  }
57
50
 
58
51
  // If the page is not found, return a 404
@@ -8,8 +8,9 @@ export const routing = defineRouting({
8
8
  // Used when no locale matches
9
9
  defaultLocale: sitecoreConfig.defaultLanguage,
10
10
 
11
- // No prefix is added for the default locale ("as-needed").
11
+ // Out of the box, syncs with the `appLocalePrefix` redirects setting for consistent behavior
12
+ // For "as-needed" value, no prefix is added for the default locale .
12
13
  // For other configuration options, refer to the next-intl documentation:
13
14
  // https://next-intl.dev/docs/routing/configuration
14
- localePrefix: 'as-needed',
15
+ localePrefix: sitecoreConfig.redirects?.appLocalePrefix || 'as-needed',
15
16
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-content-sdk-app",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Sitecore Content SDK initializer",
5
5
  "bin": "./dist/index.js",
6
6
  "scripts": {
@@ -38,11 +38,11 @@
38
38
  "minimist": "^1.2.8"
39
39
  },
40
40
  "devDependencies": {
41
- "@sitecore-content-sdk/analytics-core": "^2.1.1",
42
- "@sitecore-content-sdk/angular": "~0.1.0",
43
- "@sitecore-content-sdk/cli": "^2.2.0",
44
- "@sitecore-content-sdk/events": "^2.1.1",
45
- "@sitecore-content-sdk/nextjs": "^2.2.1",
41
+ "@sitecore-content-sdk/analytics-core": "^2.1.2",
42
+ "@sitecore-content-sdk/angular": "~0.2.0",
43
+ "@sitecore-content-sdk/cli": "^2.2.1",
44
+ "@sitecore-content-sdk/events": "^2.1.2",
45
+ "@sitecore-content-sdk/nextjs": "^2.3.0",
46
46
  "@sitecore-content-sdk/personalize": "^2.1.0",
47
47
  "@stylistic/eslint-plugin": "^5.2.2",
48
48
  "@types/chai": "^5.2.2",