@brightsideoy/kama 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
@@ -30,7 +30,7 @@ async function f() {
30
30
  if (n(f)) l = (await import(t(f).href)).compileDefinitions;
31
31
  else if (n(p)) l = (await import(t(p).href)).compileDefinitions;
32
32
  else try {
33
- l = (await import("./dist-cK3xGnBZ.js")).compileDefinitions;
33
+ l = (await import("./dist-BmDMKgKk.js")).compileDefinitions;
34
34
  } catch (e) {
35
35
  console.error("\x1B[31m ✖\x1B[0m \x1B[31mCould not resolve Kama Schema Engine:\x1B[0m", e), process.exit(1);
36
36
  }
package/npm-update.sh ADDED
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+
3
+ # Exit immediately if any command fails
4
+ set -e
5
+
6
+ # 1. Require a target directory argument
7
+ TARGET_DIR=$1
8
+ if [ -z "$TARGET_DIR" ]; then
9
+ echo "❌ Error: Please provide the target project directory."
10
+ echo "Usage: ./npm-update.sh ../my-existing-project"
11
+ exit 1
12
+ fi
13
+
14
+ # Convert to absolute path so we don't lose it when changing directories
15
+ TARGET_DIR=$(cd "$TARGET_DIR" && pwd)
16
+
17
+ echo "🧹 1. Clearing npm cache..."
18
+ npm cache clean --force
19
+
20
+ echo "🔥 2. Building & packing KAMA package locally..."
21
+ npm run build --loglevel error
22
+ npm pack --loglevel error
23
+
24
+ # Find the newly created tarball
25
+ TARBALL=$(ls -1t *.tgz | head -n 1)
26
+
27
+ if [ -z "$TARBALL" ]; then
28
+ echo "❌ Error: Failed to find the packed .tgz file."
29
+ exit 1
30
+ fi
31
+
32
+ # Get the absolute path to the tarball
33
+ TARBALL_PATH="$(pwd)/$TARBALL"
34
+
35
+ echo "🚀 3. Navigating to target project: $TARGET_DIR"
36
+ cd "$TARGET_DIR"
37
+
38
+ echo "🗑️ 4. Nailing old Kama modules to force fresh extraction..."
39
+ # If we don't delete these, npm will aggressively cache the old version!
40
+ rm -rf node_modules/@brightsideoy/kama
41
+ if [ -d "kama-worker" ]; then rm -rf kama-worker/node_modules/@brightsideoy/kama; fi
42
+ if [ -d "sites" ]; then rm -rf sites/*/node_modules/@brightsideoy/kama; fi
43
+
44
+ echo "📦 5. Injecting fresh tarball into workspace..."
45
+ npm install "$TARBALL_PATH" --save --loglevel error
46
+
47
+ if [ -d "kama-worker" ]; then
48
+ echo " -> Updating kama-worker..."
49
+ npm install "$TARBALL_PATH" -w kama-worker --save --loglevel error
50
+ fi
51
+
52
+ # Automatically loop through ALL frontend sites and update them
53
+ if [ -d "sites" ]; then
54
+ for site in sites/*; do
55
+ if [ -d "$site" ]; then
56
+ echo " -> Updating $site..."
57
+ npm install "$TARBALL_PATH" -w "$site" --save --loglevel error
58
+ fi
59
+ done
60
+ fi
61
+
62
+ echo ""
63
+ echo "======================================================"
64
+ echo "✨ KAMA PACKAGE UPDATED SUCCESSFULLY!"
65
+ echo "Target: $TARGET_DIR"
66
+ echo "======================================================"
67
+ echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightsideoy/kama",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -28,11 +28,12 @@ const {
28
28
  language,
29
29
  } = Astro.props;
30
30
 
31
- // 🎯 Language Cascade
31
+ // 🎯 Language Cascade: explicit prop -> node language -> fallback undefined
32
+ // (MetaHead will resolve channel.defaultLanguage if this remains undefined)
32
33
  const pageLang = language || node?.language || undefined;
33
34
 
34
- // 🎯 Extract the footer blocks the backend already thoughtfully provided
35
- const footerBlocks = content?.footer || [];
35
+ // ❌ Removed: const footerBlocks = content?.footer || [];
36
+ // (The new Footer component handles its own data now!)
36
37
  ---
37
38
 
38
39
  <!doctype html>
@@ -56,7 +57,6 @@ const footerBlocks = content?.footer || [];
56
57
  </div>
57
58
  </main>
58
59
 
59
- <!-- 🎯 Pass the blocks and content map directly -->
60
- <Footer blocks={footerBlocks} contentMap={content} />
60
+ <Footer />
61
61
  </body>
62
62
  </html>
@@ -85,7 +85,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
85
85
  }
86
86
 
87
87
  // 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING
88
+ // 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING WITH SURGICAL WORKER CACHING
88
89
  if (pathname.startsWith("/media/")) {
90
+ const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
91
+ // Construct a clean cache key ignoring session headers
92
+ const cacheKey = new Request(context.url.toString(), { method: "GET" });
93
+
94
+ // 1. Check surgical Worker Edge Cache for pre-encoded media (15ms HIT)
95
+ if (!isDev && cache) {
96
+ try {
97
+ const cachedResponse = await cache.match(cacheKey);
98
+ if (cachedResponse) {
99
+ return cachedResponse;
100
+ }
101
+ } catch (e) {
102
+ // Fall back to fetch if cache read fails
103
+ }
104
+ }
105
+
106
+ // 2. Cold Fetch: Pass to CMS worker for AVIF transformation
89
107
  const cmsWorker = workerEnv?.CMS_WORKER;
90
108
  const envApiUrl = import.meta.env?.PUBLIC_API_URL;
91
109
  const workerApiUrl = workerEnv?.PUBLIC_API_URL;
@@ -93,12 +111,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
93
111
  const cleanApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl.slice(0, -1) : baseApiUrl;
94
112
 
95
113
  const mediaUrl = `${cleanApiUrl}${pathname}${context.url.search}`;
96
- const mediaReq = new Request(mediaUrl, {
97
- headers: context.request.headers
98
- });
114
+ const mediaReq = new Request(mediaUrl, { headers: context.request.headers });
99
115
 
100
116
  let mediaResponse: Response;
101
-
102
117
  if (cmsWorker && typeof cmsWorker.fetch === "function") {
103
118
  mediaResponse = await cmsWorker.fetch(mediaReq);
104
119
  } else {
@@ -107,17 +122,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
107
122
 
108
123
  const newHeaders = new Headers(mediaResponse.headers);
109
124
 
110
- // 🎯 DEV FIX: Only apply 1-year cache on media in Production
111
125
  if (!isDev) {
112
126
  newHeaders.set("Cache-Control", "public, max-age=31536000, s-maxage=31536000, immutable");
113
127
  }
114
128
  newHeaders.set("Access-Control-Allow-Origin", "*");
115
129
 
116
- return new Response(mediaResponse.body, {
130
+ const finalResponse = new Response(mediaResponse.body, {
117
131
  status: mediaResponse.status,
118
132
  statusText: mediaResponse.statusText,
119
133
  headers: newHeaders
120
134
  });
135
+
136
+ // 3. Store ONLY successful image transformations in Worker Edge Cache
137
+ if (!isDev && cache && mediaResponse.status === 200) {
138
+ if (cfContext && typeof cfContext.waitUntil === 'function') {
139
+ cfContext.waitUntil(cache.put(cacheKey, finalResponse.clone()));
140
+ } else {
141
+ cache.put(cacheKey, finalResponse.clone()).catch(() => { });
142
+ }
143
+ }
144
+
145
+ return finalResponse;
121
146
  }
122
147
 
123
148
  // 🎯 0C. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
@@ -147,8 +172,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
147
172
  // A. Purge Workers Caching Layer (Astro cfContext)
148
173
  if (typeof cfContext?.cache?.purge === 'function') {
149
174
  try {
150
- await cfContext.cache.purge({ purgeEverything: true });
151
- console.log("✅ New Cloudflare Workers Cache Engine Purged");
175
+ // 🎯 FIX: Only execute global purge if the CMS explicitly requested it!
176
+ if (body.purge_everything) {
177
+ await cfContext.cache.purge({ purgeEverything: true });
178
+ console.log("✅ New Cloudflare Workers Cache Engine Purged (Global)");
179
+ }
152
180
  } catch (e) {
153
181
  console.error("❌ Failed to purge new Workers Cache Engine", e);
154
182
  }
@@ -161,22 +189,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
161
189
 
162
190
  if (body.purge_everything) {
163
191
  // 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
164
- await HTML_CACHE.put("site_version", now);
165
- // Ensure root alias is forced to new version
166
- await HTML_CACHE.put("v:/", now);
167
-
168
- console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
192
+ try {
193
+ await HTML_CACHE.put("site_version", now);
194
+ await HTML_CACHE.put("v:/", now);
195
+ console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
196
+ } catch (kvLimitErr: any) {
197
+ console.warn(`[KV Warning] Skipped global version bump: ${kvLimitErr.message}`);
198
+ }
169
199
  } else if (Array.isArray(body.urls) && body.urls.length > 0) {
170
- // 🎯 SURGICAL PURGE: Parallel execution for page version bumps
171
- const purgeTasks = [];
172
- for (const targetUrl of body.urls) {
200
+ // 🎯 SURGICAL PURGE: Parallel execution for page version bumps safely wrapped for quota
201
+ const purgeTasks = body.urls.map(async (targetUrl: string) => {
173
202
  const urlPath = new URL(targetUrl).pathname;
174
203
  const cleanPath = urlPath.replace(/\/$/, "") || "/";
175
-
176
- purgeTasks.push(HTML_CACHE.put(`v:${cleanPath}`, now));
177
- }
204
+ try {
205
+ await HTML_CACHE.put(`v:${cleanPath}`, now);
206
+ } catch (kvLimitErr: any) {
207
+ console.warn(`[KV Warning] Skipped version tag for ${cleanPath}: ${kvLimitErr.message}`);
208
+ }
209
+ });
178
210
  await Promise.all(purgeTasks);
179
211
  console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
212
+ // CMS CacheManager will automatically send the X-Cache-Warmer requests now!
180
213
  }
181
214
  } catch (kvErr) {
182
215
  console.error("❌ KV Version Tagging Error:", kvErr);
@@ -246,18 +279,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
246
279
  let purgedKv = false;
247
280
 
248
281
  if (HTML_CACHE) {
249
- const [siteVer, pageVer] = await Promise.all([
250
- HTML_CACHE.get("site_version"),
251
- HTML_CACHE.get(`v:${cleanPath}`)
252
- ]);
253
-
254
- if (siteVer || pageVer) {
255
- const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
256
- await HTML_CACHE.delete(activeCacheKey);
257
- purgedKv = true;
282
+ try {
283
+ const [siteVer, pageVer] = await Promise.all([
284
+ HTML_CACHE.get("site_version"),
285
+ HTML_CACHE.get(`v:${cleanPath}`)
286
+ ]);
287
+
288
+ if (siteVer || pageVer) {
289
+ const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
290
+ await HTML_CACHE.delete(activeCacheKey);
291
+ purgedKv = true;
292
+ }
293
+ await HTML_CACHE.delete(`v:${cleanPath}`);
294
+ } catch (kvLimitErr: any) {
295
+ console.warn(`[KV Warning] Skipped deletion for ${cleanPath}: ${kvLimitErr.message}`);
258
296
  }
259
-
260
- await HTML_CACHE.delete(`v:${cleanPath}`);
261
297
  }
262
298
 
263
299
  const cleanUrl = targetUrl.split('?')[0];
@@ -318,17 +354,33 @@ export const onRequest = defineMiddleware(async (context, next) => {
318
354
  HTML_CACHE.get(`v:${cleanLookupPath}`)
319
355
  ]);
320
356
 
357
+ // 🎯 Combine versions deterministically
321
358
  activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
322
359
 
323
- const cachedHtml = await HTML_CACHE.get(activeCacheKey);
324
- if (cachedHtml) {
325
- return new Response(cachedHtml, {
360
+ const cachedData = await HTML_CACHE.get(activeCacheKey);
361
+ if (cachedData) {
362
+ let finalHtml = cachedData;
363
+
364
+ try {
365
+ if (cachedData.startsWith("{")) {
366
+ const parsed = JSON.parse(cachedData);
367
+ if (parsed.html) {
368
+ finalHtml = parsed.html;
369
+ }
370
+ }
371
+ } catch (jsonErr) {
372
+ // Fallback to raw HTML
373
+ }
374
+
375
+ return new Response(finalHtml, {
326
376
  status: 200,
327
377
  headers: {
328
378
  "Content-Type": "text/html; charset=utf-8",
329
379
  "X-Cache-Status": "HIT-KV-STATICFILE",
330
380
  "X-Cache-Key": activeCacheKey,
331
- "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate"
381
+ // 🎯 ARCHITECTURE FIX: Prevent Cloudflare Edge from locking stale HTML in RAM.
382
+ // Force Edge to revalidate with Worker logic while keeping browser cache fresh.
383
+ "Cache-Control": "public, max-age=0, must-revalidate"
332
384
  }
333
385
  });
334
386
  }
@@ -343,26 +395,47 @@ export const onRequest = defineMiddleware(async (context, next) => {
343
395
  // 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
344
396
  if (
345
397
  !isDev &&
346
- isHtmlPage &&
347
- HTML_CACHE &&
398
+ isHtmlPage && HTML_CACHE &&
348
399
  !isTimeTraveling &&
349
400
  response.status === 200 &&
350
401
  response.headers.get("content-type")?.includes("text/html")
351
402
  ) {
352
403
  const htmlText = await response.clone().text();
353
404
 
405
+ // Extract the TTL injected by Astro SSR
406
+ const kvTtlHeader = response.headers.get("X-KV-Expiration");
407
+ const kvTtl = kvTtlHeader ? parseInt(kvTtlHeader, 10) : undefined;
408
+
409
+ // Clean up the internal header so it doesn't leak to the public browser
410
+ response.headers.delete("X-KV-Expiration");
411
+
354
412
  const writeTask = (async () => {
355
413
  if (activeCacheKey === cleanLookupPath || isWarmer) {
356
414
  const forcedVersion = Date.now().toString();
415
+ try {
416
+ await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
417
+ const siteVer = await HTML_CACHE.get("site_version") || "1";
418
+ activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
419
+ } catch (kvLimitErr: any) {
420
+ console.warn(`[KV Warning] Skipped prewarm version bump: ${kvLimitErr.message}`);
421
+ }
422
+ }
357
423
 
358
- await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
424
+ // 🎯 Architecture Fix: Save TTL alongside HTML so CDN can properly cache reads
425
+ const storedTtl = kvTtl !== undefined ? Math.max(60, kvTtl) : 31536000;
426
+ const payload = JSON.stringify({ html: htmlText, ttl: storedTtl });
359
427
 
360
- const siteVer = await HTML_CACHE.get("site_version") || "1";
361
- activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
428
+ try {
429
+ if (kvTtl !== undefined) {
430
+ await HTML_CACHE.put(activeCacheKey, payload, { expirationTtl: storedTtl });
431
+ console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Expires in ${storedTtl}s)`);
432
+ } else {
433
+ await HTML_CACHE.put(activeCacheKey, payload);
434
+ console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Infinite TTL)`);
435
+ }
436
+ } catch (kvLimitErr: any) {
437
+ console.warn(`[KV Warning] Skipped HTML cache save (Quota limit exceeded): ${kvLimitErr.message}`);
362
438
  }
363
-
364
- await HTML_CACHE.put(activeCacheKey, htmlText);
365
- console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey}`);
366
439
  })();
367
440
 
368
441
  if (cfContext && typeof cfContext.waitUntil === 'function') {
@@ -26,7 +26,7 @@ Astro.response.headers.set(
26
26
  "public, no-cache, must-revalidate",
27
27
  );
28
28
 
29
- // 1. STATIC ASSET & SYSTEM ROUTE CHECK
29
+ // 1. STATIC ASSET CHECK
30
30
  const staticExts = [
31
31
  ".ico",
32
32
  ".png",
@@ -36,33 +36,13 @@ const staticExts = [
36
36
  ".txt",
37
37
  ".xml",
38
38
  ".webmanifest",
39
- ".astro",
40
- ".ts",
41
- ".tsx",
42
- ".js",
43
- ".jsx",
44
- ".css",
45
- ".map",
46
- ".json",
47
39
  ];
48
-
49
40
  const slugStr = String(slug || "");
50
- // Strip leading slashes to correctly evaluate both "src/..." and "/src/..."
51
- const cleanSlug = slugStr.replace(/^\/+/, "").toLowerCase();
52
-
53
- // Check extension regardless of slash placement
54
- const isStaticAsset = staticExts.some(ext => cleanSlug.endsWith(ext));
55
-
56
- // Block Vite client internal hits, source file paths, and node_modules
57
- const isSystemPath =
58
- cleanSlug.startsWith("src/") ||
59
- cleanSlug.startsWith("@vite/") ||
60
- cleanSlug.startsWith("node_modules/") ||
61
- cleanSlug.startsWith("users/") ||
62
- cleanSlug.includes("components/") ||
63
- cleanSlug.includes("npm-test/");
64
-
65
- if (isStaticAsset || isSystemPath) {
41
+ const isStaticAsset =
42
+ !slugStr.includes("/") &&
43
+ staticExts.some(ext => slugStr.toLowerCase().endsWith(ext));
44
+
45
+ if (isStaticAsset) {
66
46
  return new Response(null, { status: 404 });
67
47
  }
68
48
 
@@ -70,6 +50,11 @@ if (isStaticAsset || isSystemPath) {
70
50
  const safeEnv = (Astro.locals as any).safeEnv || {};
71
51
  const cmsWorker = safeEnv.CMS_WORKER;
72
52
 
53
+ // Extract environment configuration & fallback slug
54
+ const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
55
+ const workerHomeSlug = safeEnv.PUBLIC_HOME_SLUG;
56
+ const homeSlug = String(envHomeSlug || workerHomeSlug || "index").trim();
57
+
73
58
  const envApiUrl = import.meta.env.PUBLIC_API_URL;
74
59
  const workerApiUrl = safeEnv.PUBLIC_API_URL;
75
60
  const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
@@ -121,7 +106,8 @@ const treeId = getTreeId();
121
106
  // 3. PATH & CONCURRENT FETCH RESOLUTION
122
107
  const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
123
108
  const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
124
- const targetSlug = pathParts.join("/"); // 🎯 This is now "" for the root URL!
109
+ const requestedSlug = pathParts.join("/");
110
+ const targetSlug = requestedSlug || homeSlug;
125
111
 
126
112
  let pageData: NodeResponse | null = null;
127
113
  let navTree: TreeNode[] = [];
@@ -133,7 +119,6 @@ try {
133
119
  console.error(`❌ [SSR Error] Failed to fetch tree "${treeId}":`, err);
134
120
  return [];
135
121
  }),
136
- // 🎯 We just pass the targetSlug (even if it's empty). The backend handles it!
137
122
  getNodeByPath(targetSlug, channelId, isPreview, customFetch).catch(err => {
138
123
  console.error(
139
124
  `❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
@@ -144,7 +129,7 @@ try {
144
129
  ]);
145
130
 
146
131
  navTree = treeResult || [];
147
- pageData = nodeResult;
132
+ pageData = nodeResult as NodeResponse | null;
148
133
  } catch (err) {
149
134
  console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
150
135
  }
@@ -156,8 +141,53 @@ if (!pageData || !pageData.node) {
156
141
  const node = pageData.node;
157
142
  const content = pageData.content || {};
158
143
 
159
- // 🎯 Vastly simplified homepage detection!
160
- const isHomePage = targetSlug === "" || node?.path === "/";
144
+ // 🎯 SMART EDGE CACHING LOGIC (Dynamic TTL Math)
145
+ let cacheTtl = 31536000; // Default: 1 Year (Forever)
146
+
147
+ const now = Date.now();
148
+ const publishAt = node?.publishAt ? new Date(node.publishAt).getTime() : null;
149
+ const unpublishAt = node?.unpublishAt
150
+ ? new Date(node.unpublishAt).getTime()
151
+ : null;
152
+
153
+ // If the page is scheduled to appear in the future, cache the 404/current state until that exact second
154
+ if (publishAt && publishAt > now) {
155
+ cacheTtl = Math.floor((publishAt - now) / 1000);
156
+ }
157
+ // If the page is scheduled to disappear, cache the live page until that exact second
158
+ else if (unpublishAt && unpublishAt > now) {
159
+ cacheTtl = Math.floor((unpublishAt - now) / 1000);
160
+ }
161
+
162
+ // Clamp to Cloudflare's 60-second minimum for KV expirations
163
+ cacheTtl = Math.max(60, cacheTtl);
164
+
165
+ if (isTimeTraveling) {
166
+ Astro.response.headers.set(
167
+ "Cache-Control",
168
+ "no-cache, no-store, must-revalidate",
169
+ );
170
+ } else {
171
+ // 🎯 FIX: max-age=0 prevents local browser caching. s-maxage handles Cloudflare CDN!
172
+ Astro.response.headers.set(
173
+ "Cache-Control",
174
+ `public, max-age=0, s-maxage=${cacheTtl}, must-revalidate`,
175
+ );
176
+ // Tell middleware how long to store in KV
177
+ Astro.response.headers.set("X-KV-Expiration", cacheTtl.toString());
178
+ }
179
+
180
+ const rootTreeSlug =
181
+ navTree && navTree.length > 0
182
+ ? navTree[0].slug || (navTree[0] as any).key
183
+ : "";
184
+
185
+ const isHomePage =
186
+ targetSlug === homeSlug ||
187
+ (rootTreeSlug && targetSlug === rootTreeSlug) ||
188
+ node?.path === "/" ||
189
+ pathParts.length === 0;
190
+
161
191
  const normalizedSlug = isHomePage ? "" : targetSlug;
162
192
 
163
193
  const headerBlocks = content?.header || [];