@brightsideoy/kama 0.1.0-alpha.99 → 1.0.0-alpha.2

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.
Files changed (116) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +646 -240
  3. package/content/definitions/blocks/button.yaml +24 -0
  4. package/content/definitions/blocks/header.yaml +13 -0
  5. package/content/definitions/blocks/header_image.yaml +21 -0
  6. package/content/definitions/blocks/hero.yaml +28 -0
  7. package/content/definitions/blocks/html.yaml +13 -0
  8. package/content/definitions/blocks/image_card.yaml +56 -0
  9. package/content/definitions/blocks/single_image.yaml +27 -0
  10. package/content/definitions/blocks/text.yaml +15 -0
  11. package/content/definitions/channels/ecommerce.yaml +28 -0
  12. package/content/definitions/channels/website.yaml +41 -0
  13. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  14. package/content/definitions/field-sets/channel-base.yaml +50 -0
  15. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  16. package/content/definitions/field-sets/content-block.yaml +41 -0
  17. package/content/definitions/field-sets/page-base.yaml +44 -0
  18. package/content/definitions/field-sets/page-seo.yaml +34 -0
  19. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  20. package/content/definitions/field-sets/schema-type.yaml +43 -0
  21. package/content/definitions/field-sets/tree-base.yaml +31 -0
  22. package/content/definitions/node-layouts/content.yml +12 -0
  23. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  24. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  25. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  26. package/content/definitions/nodes/address.yaml +40 -0
  27. package/content/definitions/nodes/article.yaml +49 -0
  28. package/content/definitions/nodes/event.yaml +55 -0
  29. package/content/definitions/nodes/external-link.yaml +29 -0
  30. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  31. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  32. package/content/definitions/nodes/person.yaml +37 -0
  33. package/content/definitions/nodes/product.yaml +89 -0
  34. package/content/definitions/taxonomies/categories.yaml +15 -0
  35. package/content/definitions/taxonomies/tags.yaml +12 -0
  36. package/content/definitions/trees/navigation.yaml +24 -0
  37. package/definitions/editor/default.ts +1 -0
  38. package/dist/assets/index-F2rfUymo.css +1 -0
  39. package/dist/assets/index-lXxH3VFK.js +164 -0
  40. package/dist/dist-Dy7-HUEG.js +26302 -0
  41. package/dist/index.html +4 -2
  42. package/dist/index.js +25205 -8461
  43. package/dist/schema-compiler.js +48 -0
  44. package/documentation/SCHEMAS.md +182 -0
  45. package/package.json +19 -24
  46. package/schema.sql +36 -22
  47. package/templates/astro-starter/astro.config.mjs +27 -0
  48. package/templates/astro-starter/package.json.template +20 -0
  49. package/templates/astro-starter/public/robots.txt +23 -0
  50. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +134 -0
  51. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  52. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +132 -0
  53. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  54. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  55. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +92 -0
  56. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  57. package/templates/astro-starter/src/components/global/Breadcrumb.astro +67 -0
  58. package/templates/astro-starter/src/components/global/Footer.astro +89 -0
  59. package/templates/astro-starter/src/components/global/Header.astro +177 -0
  60. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  61. package/templates/astro-starter/src/components/global/NavigationMenu.astro +402 -0
  62. package/templates/astro-starter/src/components/ui/KamaImage.astro +172 -0
  63. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  64. package/templates/astro-starter/src/layouts/StandardLayout.astro +62 -0
  65. package/templates/astro-starter/src/lib/cms.ts +197 -0
  66. package/templates/astro-starter/src/lib/media/kama-image-service.ts +56 -0
  67. package/templates/astro-starter/src/lib/media/media-proxy.ts +129 -0
  68. package/templates/astro-starter/src/lib/media/media-url.ts +98 -0
  69. package/templates/astro-starter/src/middleware.ts +478 -0
  70. package/templates/astro-starter/src/pages/404.astro +46 -0
  71. package/templates/astro-starter/src/pages/[...slug].astro +237 -0
  72. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  73. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  74. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  75. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  76. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  77. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  78. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  79. package/templates/astro-starter/tsconfig.json +0 -0
  80. package/templates/kama-worker/index.js +19 -0
  81. package/templates/kama-worker/server.js +98 -0
  82. package/wrangler.json +22 -0
  83. package/defaults/definitions/blocks/button.yaml +0 -15
  84. package/defaults/definitions/blocks/header.yaml +0 -15
  85. package/defaults/definitions/blocks/header_image.yaml +0 -22
  86. package/defaults/definitions/blocks/hero.yaml +0 -27
  87. package/defaults/definitions/blocks/image_card.yaml +0 -57
  88. package/defaults/definitions/blocks/single_image.yaml +0 -28
  89. package/defaults/definitions/blocks/text.yaml +0 -16
  90. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  91. package/defaults/definitions/channels/website.yaml +0 -32
  92. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  93. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  94. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  95. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  96. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  97. package/defaults/definitions/nodes/address.yaml +0 -32
  98. package/defaults/definitions/nodes/article.yaml +0 -43
  99. package/defaults/definitions/nodes/event.yaml +0 -49
  100. package/defaults/definitions/nodes/external-link.yaml +0 -26
  101. package/defaults/definitions/nodes/person.yaml +0 -37
  102. package/defaults/definitions/nodes/product.yaml +0 -86
  103. package/defaults/definitions/trees/navigation.yaml +0 -29
  104. package/defaults/definitions/trees/social-links.yaml +0 -9
  105. package/definitions/_page.yaml +0 -52
  106. package/dist/assets/index-D6qQsWOj.js +0 -1392
  107. package/dist/assets/index-TdW5F0pX.css +0 -1
  108. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  109. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  110. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  111. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  112. package/scripts/bundle-definitions.js +0 -176
  113. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  114. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  115. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  116. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
@@ -0,0 +1,478 @@
1
+ // src/middleware.ts
2
+
3
+ import { defineMiddleware } from "astro:middleware";
4
+
5
+ /**
6
+ * Safely extracts environment variables across Astro 7, Node.js, and Cloudflare Workers
7
+ * without triggering the `@astrojs/cloudflare` runtime.env getter exception.
8
+ */
9
+ async function getSafeEnv(locals: any): Promise<Record<string, any>> {
10
+ // 1. Try native Cloudflare Workers module (Astro 5+ / workerd)
11
+ try {
12
+ // @ts-ignore
13
+ const cf = await import("cloudflare:workers");
14
+ if (cf?.env) return cf.env;
15
+ } catch (e) {
16
+ // Not running inside a Cloudflare Worker container
17
+ }
18
+
19
+ // 2. Safe check for locals.runtime / cfContext (Local Node.js / Hono)
20
+ const runtime = locals?.runtime || locals?.cfContext;
21
+ if (runtime && typeof runtime === "object") {
22
+ if ("CMS_WORKER" in runtime || "PUBLIC_API_URL" in runtime) {
23
+ return runtime;
24
+ }
25
+
26
+ // Safely check property descriptor to avoid triggering throwing getters
27
+ try {
28
+ const desc = Object.getOwnPropertyDescriptor(runtime, "env");
29
+ if (desc && !desc.get) {
30
+ return runtime.env;
31
+ }
32
+ } catch (e) {
33
+ // Ignore getter throws
34
+ }
35
+ }
36
+
37
+ // 3. Fallback for standalone Node.js mode
38
+ return typeof process !== "undefined" ? process.env || {} : {};
39
+ }
40
+
41
+ export const onRequest = defineMiddleware(async (context, next) => {
42
+ const pathname = context.url.pathname;
43
+ const pathLower = pathname.toLowerCase();
44
+
45
+ // 🎯 Detect if we are previewing a draft or looking at a snapshot revision
46
+ const isPreview = context.url.searchParams.get("preview") === "true";
47
+ const hasRevision = context.url.searchParams.has("revisionId");
48
+ const isTimeTraveling = isPreview || hasRevision;
49
+
50
+ // 🎯 Detect Local Vite Dev Mode to bypass edge caching locally
51
+ const isDev = import.meta.env.DEV;
52
+
53
+ // 🎯 Safe dual-environment context extraction
54
+ const workerEnv = await getSafeEnv(context.locals);
55
+
56
+ // Attach to locals so Astro components (like Header.astro) don't have to re-evaluate it
57
+ (context.locals as any).safeEnv = workerEnv;
58
+
59
+ // 🎯 Safe ctx extraction (Fixing the runtime.ctx crash in Astro 6/7)
60
+ let cfContext = (context.locals as any).cfContext;
61
+ if (!cfContext) {
62
+ const runtime = (context.locals as any).runtime;
63
+ if (runtime && typeof runtime === "object") {
64
+ try {
65
+ const desc = Object.getOwnPropertyDescriptor(runtime, "ctx");
66
+ if (desc && !desc.get) {
67
+ cfContext = runtime.ctx;
68
+ }
69
+ } catch (e) { }
70
+ }
71
+ }
72
+ cfContext = cfContext || {};
73
+
74
+ const HTML_CACHE = workerEnv?.HTML_CACHE; // KV Namespace binding
75
+
76
+ // 🎯 0. INSTANT EDGE SPAM DROP (Blocks bots before hit to CMS worker/database)
77
+ const isSpam =
78
+ pathLower.includes("wp-") ||
79
+ pathLower.includes("wordpress") ||
80
+ pathLower.endsWith(".php") ||
81
+ pathLower === "/wp";
82
+
83
+ if (isSpam) {
84
+ return new Response("Not Found", { status: 404 });
85
+ }
86
+
87
+ // 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING
88
+ if (pathname.startsWith("/media/")) {
89
+ const cmsWorker = workerEnv?.CMS_WORKER;
90
+ const envApiUrl = import.meta.env?.PUBLIC_API_URL;
91
+ const workerApiUrl = workerEnv?.PUBLIC_API_URL;
92
+ const baseApiUrl = String(envApiUrl || workerApiUrl || "https://kama.kamaengine.com");
93
+ const cleanApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl.slice(0, -1) : baseApiUrl;
94
+
95
+ const mediaUrl = `${cleanApiUrl}${pathname}${context.url.search}`;
96
+ const mediaReq = new Request(mediaUrl, {
97
+ headers: context.request.headers
98
+ });
99
+
100
+ let mediaResponse: Response;
101
+
102
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
103
+ mediaResponse = await cmsWorker.fetch(mediaReq);
104
+ } else {
105
+ mediaResponse = await fetch(mediaReq);
106
+ }
107
+
108
+ const newHeaders = new Headers(mediaResponse.headers);
109
+
110
+ // 🎯 DEV FIX: Only apply 1-year cache on media in Production
111
+ if (!isDev) {
112
+ newHeaders.set("Cache-Control", "public, max-age=31536000, s-maxage=31536000, immutable");
113
+ }
114
+ newHeaders.set("Access-Control-Allow-Origin", "*");
115
+
116
+ return new Response(mediaResponse.body, {
117
+ status: mediaResponse.status,
118
+ statusText: mediaResponse.statusText,
119
+ headers: newHeaders
120
+ });
121
+ }
122
+
123
+ // 🎯 0C. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
124
+ if (pathname === "/api/purge" || pathname === "/api/purge/") {
125
+ if (context.request.method !== "POST") {
126
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
127
+ status: 405,
128
+ headers: { "Content-Type": "application/json" },
129
+ });
130
+ }
131
+
132
+ const authHeader = context.request.headers.get("Authorization");
133
+ const expectedToken = workerEnv.CLOUDFLARE_API_TOKEN;
134
+
135
+ if (!expectedToken || authHeader !== `Bearer ${expectedToken}`) {
136
+ return new Response(JSON.stringify({ error: "Unauthorized Token Mismatch" }), {
137
+ status: 401,
138
+ headers: { "Content-Type": "application/json" },
139
+ });
140
+ }
141
+
142
+ try {
143
+ const body = await context.request.json();
144
+ const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
145
+ const origin = context.url.origin;
146
+
147
+ // A. Purge Workers Caching Layer (Astro cfContext)
148
+ if (typeof cfContext?.cache?.purge === 'function') {
149
+ try {
150
+ await cfContext.cache.purge({ purgeEverything: true });
151
+ console.log("✅ New Cloudflare Workers Cache Engine Purged");
152
+ } catch (e) {
153
+ console.error("❌ Failed to purge new Workers Cache Engine", e);
154
+ }
155
+ }
156
+
157
+ // B. Version-Based Instant Invalidation (0ms KV Write)
158
+ if (HTML_CACHE) {
159
+ try {
160
+ const now = Date.now().toString();
161
+
162
+ if (body.purge_everything) {
163
+ // 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
164
+ await HTML_CACHE.put("site_version", now);
165
+
166
+ // 🎯 FIX: Force clear alias keys during global purge
167
+ await HTML_CACHE.put("v:/", now);
168
+ await HTML_CACHE.put("v:/index", now);
169
+
170
+ console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
171
+ } else if (Array.isArray(body.urls) && body.urls.length > 0) {
172
+ // 🎯 SURGICAL PURGE: Parallel execution for page version bumps
173
+ const purgeTasks = [];
174
+ for (const targetUrl of body.urls) {
175
+ const urlPath = new URL(targetUrl).pathname;
176
+ const cleanPath = urlPath.replace(/\/$/, "") || "/";
177
+
178
+ purgeTasks.push(HTML_CACHE.put(`v:${cleanPath}`, now));
179
+
180
+ // 🎯 FIX: Always sync / and /index aliases together
181
+ if (cleanPath === "/" || cleanPath === "/index") {
182
+ purgeTasks.push(HTML_CACHE.put("v:/", now));
183
+ purgeTasks.push(HTML_CACHE.put("v:/index", now));
184
+ }
185
+ }
186
+ await Promise.all(purgeTasks);
187
+ console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
188
+ }
189
+ } catch (kvErr) {
190
+ console.error("❌ KV Version Tagging Error:", kvErr);
191
+ }
192
+ }
193
+
194
+ // C. Global Legacy Worker Cache Purge
195
+ if (body.purge_everything) {
196
+ if (cache) {
197
+ const rootUrls = [
198
+ origin,
199
+ `${origin}/`,
200
+ `${origin}/index`,
201
+ `${origin}/index/`
202
+ ];
203
+
204
+ for (const url of rootUrls) {
205
+ await cache.delete(new Request(url));
206
+ }
207
+ }
208
+
209
+ return new Response(JSON.stringify({
210
+ success: true,
211
+ message: "All Worker Caches Purged"
212
+ }), {
213
+ status: 200,
214
+ headers: { "Content-Type": "application/json" },
215
+ });
216
+ }
217
+
218
+ // D. Surgical Legacy Worker Cache Purge (For Updated Pages)
219
+ if (Array.isArray(body.urls) && body.urls.length > 0) {
220
+ const results = [];
221
+
222
+ for (const targetUrl of body.urls) {
223
+ const cleanUrl = targetUrl.split('?')[0];
224
+ const urlNoSlash = cleanUrl.replace(/\/$/, "");
225
+ const urlWithSlash = urlNoSlash + "/";
226
+
227
+ const pathsToClear = [urlNoSlash, urlWithSlash];
228
+ const urlPath = new URL(targetUrl).pathname;
229
+
230
+ // 🎯 FIX: Clear legacy cache alias if it's the home page
231
+ if (urlPath === "/" || urlPath === "/index") {
232
+ pathsToClear.push(`${origin}/`);
233
+ pathsToClear.push(`${origin}/index`);
234
+ pathsToClear.push(`${origin}/index/`);
235
+ pathsToClear.push(`${origin}`);
236
+ }
237
+
238
+ let purged = false;
239
+ if (cache) {
240
+ for (const p of pathsToClear) {
241
+ const res = await cache.delete(new Request(p));
242
+ if (res) purged = true;
243
+ }
244
+ }
245
+
246
+ results.push({ url: targetUrl, purged });
247
+ }
248
+
249
+ return new Response(JSON.stringify({
250
+ success: true,
251
+ message: "URLs processed and version tags updated",
252
+ results
253
+ }), {
254
+ status: 200,
255
+ headers: { "Content-Type": "application/json" },
256
+ });
257
+ }
258
+
259
+ // 🎯 E. SURGICAL DELETION FOR DELETED PAGES
260
+ if (Array.isArray(body.deleted_urls) && body.deleted_urls.length > 0) {
261
+ const results: any[] = [];
262
+
263
+ const deleteTasks = body.deleted_urls.map(async (targetUrl: string) => {
264
+ const urlPath = new URL(targetUrl).pathname;
265
+ const cleanPath = urlPath.replace(/\/$/, "") || "/";
266
+
267
+ let purgedKv = false;
268
+
269
+ if (HTML_CACHE) {
270
+ const [siteVer, pageVer] = await Promise.all([
271
+ HTML_CACHE.get("site_version"),
272
+ HTML_CACHE.get(`v:${cleanPath}`)
273
+ ]);
274
+
275
+ if (siteVer || pageVer) {
276
+ const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
277
+ await HTML_CACHE.delete(activeCacheKey);
278
+ purgedKv = true;
279
+ }
280
+
281
+ await HTML_CACHE.delete(`v:${cleanPath}`);
282
+
283
+ // 🎯 FIX: Delete aliases from KV if applicable
284
+ if (cleanPath === "/" || cleanPath === "/index") {
285
+ await HTML_CACHE.delete("v:/");
286
+ await HTML_CACHE.delete("v:/index");
287
+ }
288
+ }
289
+
290
+ const cleanUrl = targetUrl.split('?')[0];
291
+ const urlNoSlash = cleanUrl.replace(/\/$/, "");
292
+ const urlWithSlash = urlNoSlash + "/";
293
+
294
+ const pathsToClear = [urlNoSlash, urlWithSlash];
295
+ if (urlPath === "/" || urlPath === "/index") {
296
+ pathsToClear.push(`${origin}/`);
297
+ pathsToClear.push(`${origin}/index`);
298
+ pathsToClear.push(`${origin}/index/`);
299
+ pathsToClear.push(`${origin}`);
300
+ }
301
+
302
+ let purgedLegacy = false;
303
+ if (cache) {
304
+ for (const p of pathsToClear) {
305
+ const res = await cache.delete(new Request(p));
306
+ if (res) purgedLegacy = true;
307
+ }
308
+ }
309
+
310
+ results.push({ url: targetUrl, purgedKv, purgedLegacy });
311
+ });
312
+
313
+ await Promise.all(deleteTasks);
314
+
315
+ return new Response(JSON.stringify({
316
+ success: true,
317
+ message: "Deleted URLs processed and KV keys permanently removed",
318
+ results
319
+ }), {
320
+ status: 200,
321
+ headers: { "Content-Type": "application/json" },
322
+ });
323
+ }
324
+
325
+ return new Response(JSON.stringify({ error: "Invalid payload format" }), {
326
+ status: 400,
327
+ headers: { "Content-Type": "application/json" },
328
+ });
329
+
330
+ } catch (err: any) {
331
+ return new Response(JSON.stringify({ error: err.message }), {
332
+ status: 500,
333
+ headers: { "Content-Type": "application/json" },
334
+ });
335
+ }
336
+ }
337
+
338
+ // 🎯 1. KV STATIC "FILE" CACHE READ WITH VERSION TAGGING
339
+ const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
340
+ const isHtmlPage = !pathname.includes('.') && !pathname.startsWith('/api/');
341
+ const cleanLookupPath = pathname.replace(/\/$/, "") || "/";
342
+
343
+ let activeCacheKey = cleanLookupPath;
344
+
345
+ // 🎯 FIX: Bypass KV Read in local dev OR if we are time traveling (previewing drafts/snapshots)
346
+ if (!isDev && isHtmlPage && HTML_CACHE && !isWarmer && !isTimeTraveling) {
347
+ try {
348
+ const [siteVer, pageVer] = await Promise.all([
349
+ HTML_CACHE.get("site_version"),
350
+ HTML_CACHE.get(`v:${cleanLookupPath}`)
351
+ ]);
352
+
353
+ activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
354
+
355
+ const cachedHtml = await HTML_CACHE.get(activeCacheKey);
356
+ if (cachedHtml) {
357
+ return new Response(cachedHtml, {
358
+ status: 200,
359
+ headers: {
360
+ "Content-Type": "text/html; charset=utf-8",
361
+ "X-Cache-Status": "HIT-KV-STATICFILE",
362
+ "X-Cache-Key": activeCacheKey,
363
+ "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate"
364
+ }
365
+ });
366
+ }
367
+ } catch (e) {
368
+ console.warn("[Middleware] KV lookup bypass:", e);
369
+ }
370
+ }
371
+
372
+ // Process downstream SSR routes
373
+ const response = await next();
374
+
375
+ // 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
376
+ // 🎯 FIX: Bypass KV Write in local dev OR if we are time traveling (never save drafts to public cache)
377
+ if (
378
+ !isDev &&
379
+ isHtmlPage &&
380
+ HTML_CACHE &&
381
+ !isTimeTraveling &&
382
+ response.status === 200 &&
383
+ response.headers.get("content-type")?.includes("text/html")
384
+ ) {
385
+ const htmlText = await response.clone().text();
386
+
387
+ const writeTask = (async () => {
388
+ if (activeCacheKey === cleanLookupPath || isWarmer) {
389
+ const forcedVersion = Date.now().toString();
390
+
391
+ await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
392
+
393
+ // 🎯 FIX: Sync both aliases when saving the homepage
394
+ if (cleanLookupPath === "/" || cleanLookupPath === "/index") {
395
+ await HTML_CACHE.put(`v:/`, forcedVersion);
396
+ await HTML_CACHE.put(`v:/index`, forcedVersion);
397
+ }
398
+
399
+ const siteVer = await HTML_CACHE.get("site_version") || "1";
400
+
401
+ activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
402
+ }
403
+
404
+ await HTML_CACHE.put(activeCacheKey, htmlText);
405
+ console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey}`);
406
+ })();
407
+
408
+ if (cfContext && typeof cfContext.waitUntil === 'function') {
409
+ cfContext.waitUntil(writeTask);
410
+ } else {
411
+ writeTask.catch((e: any) => console.error("KV write error:", e));
412
+ }
413
+ }
414
+
415
+ // 🎯 3. SERVE LLMS & LLXML ENDPOINTS
416
+ if (pathname === "/llms.txt" || pathname === "/llms-full.txt") {
417
+ response.headers.set("Content-Type", "text/plain; charset=utf-8");
418
+ response.headers.set("Cache-Control", "public, max-age=3600");
419
+ response.headers.set("Access-Control-Allow-Origin", "*");
420
+ return response;
421
+ }
422
+
423
+ if (pathname === "/llxml.txt") {
424
+ response.headers.set("Content-Type", "application/xml; charset=utf-8");
425
+ response.headers.set("Cache-Control", "public, max-age=3600");
426
+ response.headers.set("Access-Control-Allow-Origin", "*");
427
+ return response;
428
+ }
429
+
430
+ // 🎯 4. STATIC ASSET CACHING FOR COMPRESSED BUNDLES / ASSETS
431
+ // 🎯 DEV FIX: Do not apply max-age=31536000 locally to prevent breaking Vite HMR
432
+ if (!isDev) {
433
+ if (
434
+ pathname.startsWith("/_image") ||
435
+ pathname.startsWith("/cdn-cgi/image/") ||
436
+ pathname.match(/\.(png|jpg|jpeg|webp|avif|svg|gif|ico|woff2?|css|js)$/i)
437
+ ) {
438
+ response.headers.set(
439
+ "Cache-Control",
440
+ "public, max-age=31536000, s-maxage=31536000, immutable"
441
+ );
442
+ response.headers.set("Access-Control-Allow-Origin", "*");
443
+ return response;
444
+ }
445
+ }
446
+
447
+ // 🎯 5. SECURITY HEADERS FOR HTML PAGES
448
+ if (response.headers.get("content-type")?.includes("text/html")) {
449
+ response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
450
+ response.headers.set("X-Content-Type-Options", "nosniff");
451
+
452
+ // 🎯 Extract origin from env dynamically
453
+ const cmsUrl = workerEnv?.PUBLIC_CMS_URL || workerEnv?.PUBLIC_API_URL || "http://localhost:8787";
454
+ let cmsOrigin = "";
455
+ try {
456
+ cmsOrigin = new URL(cmsUrl).origin;
457
+ } catch (e) { }
458
+
459
+ if (isDev) {
460
+ response.headers.set("X-Frame-Options", "ALLOWALL");
461
+ response.headers.set(
462
+ "Content-Security-Policy",
463
+ "default-src 'self' http: https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; frame-ancestors *;"
464
+ );
465
+ } else {
466
+ // Production headers
467
+ response.headers.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
468
+ // 🎯 FIX: Remove X-Frame-Options entirely since it breaks cross-subdomain framing.
469
+ // Rely completely on CSP frame-ancestors to whitelist ALL kamaengine subdomains + your env URL.
470
+ response.headers.set(
471
+ "Content-Security-Policy",
472
+ `default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data: blob:; connect-src 'self' https:; frame-ancestors 'self' https://*.kamaengine.com https://kama.kamaengine.com ${cmsOrigin};`
473
+ );
474
+ }
475
+ }
476
+
477
+ return response;
478
+ });
@@ -0,0 +1,46 @@
1
+ ---
2
+ // src/pages/404.astro
3
+ import StandardLayout from "../layouts/StandardLayout.astro";
4
+
5
+ const node = {
6
+ title: "Page Not Found",
7
+ summary: "We couldn't find the page you were looking for.",
8
+ };
9
+ ---
10
+
11
+ <StandardLayout
12
+ node={node}
13
+ isHomePage={false}
14
+ hasSidebar={false}
15
+ content={{}}
16
+ normalizedSlug="404"
17
+ headerBlocks={[]}
18
+ >
19
+ <div class="error-page">
20
+ <h1>404</h1>
21
+ <h2>Page Not Found</h2>
22
+ <p>Sorry, the page you are looking for doesn't exist or has been moved.</p>
23
+ <a href="/" class="button">Go back home</a>
24
+ </div>
25
+ </StandardLayout>
26
+
27
+ <style>
28
+ .error-page {
29
+ text-align: center;
30
+ padding: 100px 20px;
31
+ }
32
+ .error-page h1 {
33
+ font-size: 6rem;
34
+ margin-bottom: 0;
35
+ color: #0284c7;
36
+ }
37
+ .button {
38
+ display: inline-block;
39
+ margin-top: 24px;
40
+ padding: 12px 24px;
41
+ background: #0f172a;
42
+ color: white;
43
+ text-decoration: none;
44
+ border-radius: 4px;
45
+ }
46
+ </style>