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

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,19 +30,19 @@ 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-Dy7-HUEG.js")).compileDefinitions;
33
+ l = (await import("./dist-cK3xGnBZ.js")).compileDefinitions;
34
34
  } catch (e) {
35
- console.error("❌ Could not resolve Kama Schema Engine:", e), process.exit(1);
35
+ console.error("\x1B[31m ✖\x1B[0m \x1B[31mCould not resolve Kama Schema Engine:\x1B[0m", e), process.exit(1);
36
36
  }
37
- l || (console.error("❌ compileDefinitions function missing from resolved module"), process.exit(1));
37
+ l || (console.error("\x1B[31m ✖\x1B[0m \x1B[31mcompileDefinitions function missing from resolved module\x1B[0m"), process.exit(1));
38
38
  let m = l(), h = Object.keys(m?.nodes || {}).length, g = Object.keys(m?.blocks || {}).length, _ = Object.keys(m?.taxonomies || {}).length, v = `// AUTO-GENERATED BUNDLE - DO NOT EDIT
39
39
  const defs = ${JSON.stringify(m, null, 2)};
40
40
  export const BUNDLED_DEFINITIONS = defs;
41
41
  export default defs;
42
42
  `;
43
- a(c, v, "utf-8"), console.log(`✅ Schema securely compiled using engine.ts to ${c} (${h} nodes, ${g} blocks, ${_} taxonomies)`);
43
+ a(c, v, "utf-8"), console.log(`\x1b[32m ✓\x1b[0m \x1b[2mSchemas bundled (${h} nodes, ${g} blocks, ${_} taxonomies)\x1b[0m`);
44
44
  }
45
45
  f().catch((e) => {
46
- console.error("❌ Schema Compiler Runtime Error:", e), process.exit(1);
46
+ console.error("\x1B[31m ✖\x1B[0m \x1B[31mSchema Compiler Runtime Error:\x1B[0m", e), process.exit(1);
47
47
  });
48
48
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightsideoy/kama",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,6 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
+ "@clack/prompts": "^1.8.1",
36
37
  "@heroicons/vue": "^2.2.0",
37
38
  "@hono/node-server": "^2.1.1",
38
39
  "@hono/swagger-ui": "^0.6.1",
package/schema.sql CHANGED
@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS "content_schemas" (
13
13
 
14
14
  CREATE TABLE IF NOT EXISTS "users" (
15
15
  id TEXT PRIMARY KEY,
16
- email TEXT UNIQUE NOT NULL,
16
+ email UNIQUE NOT NULL,
17
17
  name TEXT NOT NULL,
18
18
  globalRole TEXT DEFAULT 'editor',
19
19
  createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -201,6 +201,22 @@ CREATE TABLE IF NOT EXISTS "node_terms" (
201
201
  FOREIGN KEY (termId) REFERENCES taxonomy_terms(id) ON DELETE CASCADE
202
202
  );
203
203
 
204
+ CREATE TABLE IF NOT EXISTS "node_routes" (
205
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
206
+ path TEXT NOT NULL,
207
+ channelId INTEGER NOT NULL,
208
+ nodeId INTEGER,
209
+ treeId INTEGER,
210
+ isCanonical INTEGER DEFAULT 1,
211
+ redirectUrl TEXT,
212
+ createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
213
+ updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
214
+ FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE,
215
+ FOREIGN KEY (nodeId) REFERENCES content_nodes(id) ON DELETE CASCADE,
216
+ FOREIGN KEY (treeId) REFERENCES trees(id) ON DELETE CASCADE,
217
+ UNIQUE(path, channelId)
218
+ );
219
+
204
220
  CREATE INDEX IF NOT EXISTS idx_tree_nodes_tree_parent ON tree_nodes(treeId, parentId);
205
221
  CREATE INDEX IF NOT EXISTS idx_tree_nodes_contentId ON tree_nodes(contentId);
206
222
  CREATE INDEX IF NOT EXISTS idx_content_nodes_slug_status ON content_nodes(slug, status);
@@ -233,3 +249,5 @@ CREATE INDEX IF NOT EXISTS idx_node_terms_node ON node_terms(nodeId);
233
249
  CREATE INDEX IF NOT EXISTS idx_node_terms_term ON node_terms(termId);
234
250
  CREATE INDEX IF NOT EXISTS idx_node_terms_node_term ON node_terms(nodeId, termId);
235
251
  CREATE INDEX IF NOT EXISTS idx_node_terms_term_node ON node_terms(termId, nodeId);
252
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_node_routes_lookup ON node_routes(path, channelId);
253
+ CREATE INDEX IF NOT EXISTS idx_node_routes_node ON node_routes(nodeId);
@@ -7,24 +7,60 @@ interface Props {
7
7
  isHomePage?: boolean;
8
8
  }
9
9
 
10
- const { title, slug, isHomePage } = Astro.props;
10
+ const { title, isHomePage } = Astro.props;
11
11
 
12
- // 🎯 Detect home page via URL pathname or explicit flag
13
- const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
12
+ const currentPath = Astro.url.pathname;
13
+
14
+ // Extract non-empty path segments
15
+ const rawSegments = currentPath.split("/").filter(Boolean);
16
+
17
+ // Detect homepage
18
+ const isHome = isHomePage || currentPath === "/" || rawSegments.length === 0;
19
+
20
+ function formatSegmentTitle(segment: string): string {
21
+ return segment
22
+ .split("-")
23
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
24
+ .join(" ");
25
+ }
26
+
27
+ // Build progressive paths: /some-page, /some-page/child-page, etc.
28
+ let runningPath = "";
29
+ const breadcrumbItems = rawSegments.map((segment, index) => {
30
+ runningPath += `/${segment}`;
31
+ const isLast = index === rawSegments.length - 1;
32
+
33
+ return {
34
+ label: isLast ? title : formatSegmentTitle(segment),
35
+ href: runningPath,
36
+ isCurrent: isLast,
37
+ };
38
+ });
14
39
  ---
15
40
 
16
41
  <nav class="breadcrumb-nav" aria-label="Breadcrumb">
17
42
  <ol class="breadcrumb-list">
43
+ <!-- Home Link -->
18
44
  <li class="breadcrumb-item">
19
45
  <a href="/">Home</a>
20
46
  </li>
47
+
48
+ <!-- Subpage Breadcrumbs -->
21
49
  {
22
- !isHome && (
23
- <li class="breadcrumb-item active" aria-current="page">
24
- <span class="separator">/</span>
25
- <span>{title}</span>
26
- </li>
27
- )
50
+ !isHome &&
51
+ breadcrumbItems.map(item => (
52
+ <li
53
+ class={`breadcrumb-item ${item.isCurrent ? "active" : ""}`}
54
+ aria-current={item.isCurrent ? "page" : undefined}
55
+ >
56
+ <span class="separator">/</span>
57
+ {item.isCurrent ? (
58
+ <span>{item.label}</span>
59
+ ) : (
60
+ <a href={item.href}>{item.label}</a>
61
+ )}
62
+ </li>
63
+ ))
28
64
  }
29
65
  </ol>
30
66
  </nav>
@@ -45,6 +81,11 @@ const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
45
81
  color: #64748b;
46
82
  }
47
83
 
84
+ .breadcrumb-item {
85
+ display: flex;
86
+ align-items: center;
87
+ }
88
+
48
89
  .breadcrumb-item a {
49
90
  color: #334155;
50
91
  text-decoration: none;
@@ -2,60 +2,21 @@
2
2
  // src/components/global/Footer.astro
3
3
 
4
4
  import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
- import { getChannelId } from "@/lib/cms";
6
5
 
7
- // 🎯 Fetch data natively during the server request
8
- const safeEnv = (Astro.locals as any).safeEnv || {};
9
- const cmsWorker = safeEnv.CMS_WORKER;
10
- const channelId = getChannelId() || "website";
11
-
12
- const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
13
- const reqInit = { ...init };
14
- delete reqInit.cache;
15
-
16
- let urlStr = input instanceof URL ? input.toString() : String(input);
17
- if (urlStr.startsWith("/")) {
18
- const baseUrl =
19
- import.meta.env.PUBLIC_API_URL ||
20
- safeEnv.PUBLIC_API_URL ||
21
- "http://127.0.0.1:8787";
22
- urlStr = `${baseUrl}${urlStr}`;
23
- }
24
-
25
- const req = new Request(urlStr, reqInit);
26
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
27
- return cmsWorker.fetch(req);
28
- }
29
- return fetch(req);
30
- };
31
-
32
- // 🎯 Resolve the Home Page slug
33
- const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
34
-
35
- let footerBlocks: any[] = [];
36
- let contentMap: Record<string, any[]> = {};
37
-
38
- try {
39
- const res = await customFetch(
40
- `/api/v1/node/${homeSlug}?channelId=${channelId}`,
41
- );
42
- if (res.ok) {
43
- const data = await res.json();
44
- const allContent = data.content || {};
45
- footerBlocks = allContent.footer || [];
46
- contentMap = allContent;
47
- }
48
- } catch (e) {
49
- console.error("[Footer] Failed to fetch home page blocks:", e);
6
+ interface Props {
7
+ blocks?: any[];
8
+ contentMap?: Record<string, any[]>;
50
9
  }
10
+
11
+ const { blocks = [], contentMap = {} } = Astro.props;
51
12
  ---
52
13
 
53
14
  <footer class="site-footer">
54
15
  <div class="footer-container">
55
16
  {
56
- footerBlocks && footerBlocks.length > 0 ? (
17
+ blocks && blocks.length > 0 ? (
57
18
  <BlockRenderer
58
- blocks={footerBlocks}
19
+ blocks={blocks}
59
20
  contentMap={contentMap}
60
21
  region="footer"
61
22
  />
@@ -15,7 +15,6 @@ const props = Astro.props as Props;
15
15
  const currentPropsNode = props.node || {};
16
16
  let rawTree: TreeNode[] = props.tree || [];
17
17
 
18
- // 🎯 Read the safely extracted environment from middleware (Fixes Astro 7 crash)
19
18
  const safeEnv = (Astro.locals as any).safeEnv || {};
20
19
  const cmsWorker = safeEnv.CMS_WORKER;
21
20
 
@@ -41,29 +40,27 @@ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
41
40
  return fetch(req);
42
41
  };
43
42
 
44
- // 🎯 Fetch Global Channel Metadata and Nav Tree in Parallel
45
- const channelId = getChannelId() || "website";
46
- const treeId = getTreeId() || "main-nav";
43
+ // 🎯 Resolve Channel ID dynamically from env/lib without hardcoding "website"
44
+ const channelId = getChannelId();
45
+ const treeId = getTreeId();
47
46
 
48
47
  let channelData: Record<string, any> | null = null;
49
48
 
50
49
  try {
51
- const [fetchedChannel, fetchedTree] = await Promise.all([
52
- getChannel(channelId, customFetch),
53
- !rawTree || rawTree.length === 0
54
- ? getTree(treeId, channelId, customFetch)
55
- : Promise.resolve(rawTree),
56
- ]);
57
-
58
- channelData = fetchedChannel;
59
50
  if (!rawTree || rawTree.length === 0) {
51
+ const [fetchedChannel, fetchedTree] = await Promise.all([
52
+ getChannel(channelId, customFetch),
53
+ getTree(treeId, channelId, customFetch),
54
+ ]);
55
+ channelData = fetchedChannel;
60
56
  rawTree = fetchedTree || [];
57
+ } else {
58
+ channelData = await getChannel(channelId, customFetch);
61
59
  }
62
60
  } catch (e) {
63
61
  console.error("[Header] Failed to load layout data:", e);
64
62
  }
65
63
 
66
- // 🎯 1. Resolve Logo Text (Strict waterfall prioritizing explicit strings)
67
64
  const rawLogoText = channelData?.logoText;
68
65
  const rawSiteTitle = channelData?.siteTitle;
69
66
  const rawLabel = channelData?.label;
@@ -82,11 +79,9 @@ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
82
79
  logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
83
80
  }
84
81
 
85
- // 🎯 2. Extract Raw Logo Asset (Unwraps array wrappers if present)
86
82
  const rawLogoAsset = channelData?.logoImage || channelData?.logo;
87
83
  const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
88
84
 
89
- // 🎯 3. Strict Image Validation Guard (Guarantees non-empty string or populated object)
90
85
  const hasValidLogoImage = Boolean(
91
86
  targetLogo &&
92
87
  ((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
@@ -152,7 +147,6 @@ const hasValidLogoImage = Boolean(
152
147
  z-index: 1001;
153
148
  }
154
149
 
155
- /* 🎯 Strict 16px Height Constraint for KamaImage output */
156
150
  :global(.logo-image) {
157
151
  height: 16px !important;
158
152
  max-height: 16px !important;
@@ -8,25 +8,28 @@ export interface Props {
8
8
 
9
9
  const { tree = [] } = Astro.props;
10
10
 
11
- // 🎯 Grab the current path directly from Astro's server request
12
11
  const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
13
- const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "index").trim();
14
12
 
15
- const homeNode = tree.find(
16
- item =>
17
- item.slug === homeSlug ||
18
- item.slug === "" ||
19
- item.path === "/" ||
20
- item.path === "",
21
- );
13
+ // 🎯 FIX: Identify the root node in the tree structure
14
+ const rootNode =
15
+ tree.find(
16
+ item =>
17
+ item.path === "/" ||
18
+ item.path === "" ||
19
+ item.slug === "index" ||
20
+ (item as any).isRoot === true ||
21
+ (item as any).isRootNode === 1,
22
+ ) || tree[0];
22
23
 
24
+ // 🎯 FIX: If rootNode has children, display those children as top-level menu items.
25
+ // If not, filter out the root node by ID AND path.
23
26
  const topLevelNavItems =
24
- homeNode?.children && homeNode.children.length > 0
25
- ? homeNode.children
27
+ rootNode?.children && rootNode.children.length > 0
28
+ ? rootNode.children
26
29
  : tree.filter(
27
30
  item =>
28
- item.slug !== homeSlug &&
29
- item.slug !== "" &&
31
+ item.id !== rootNode?.id &&
32
+ item.slug !== rootNode?.slug &&
30
33
  item.path !== "/" &&
31
34
  item.path !== "",
32
35
  );
@@ -125,58 +128,66 @@ const isExactActive = (itemPath?: string) => {
125
128
  </ul>
126
129
  </nav>
127
130
 
128
- <script>
129
- // 🎯 Clean, standard event listeners that run when the HTML is parsed
130
- document.addEventListener("DOMContentLoaded", () => {
131
+ <script is:inline>
132
+ const initMobileMenu = () => {
131
133
  const toggleBtn = document.getElementById("menu-toggle");
132
134
  const navOverlay = document.getElementById("nav-overlay");
133
135
 
136
+ if (!toggleBtn || !navOverlay) return;
137
+
138
+ // Remove old listeners by replacing the nodes (prevents duplicate bindings)
139
+ const newToggleBtn = toggleBtn.cloneNode(true);
140
+ toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
141
+
134
142
  const closeMenu = () => {
135
- if (toggleBtn && navOverlay) {
136
- toggleBtn.setAttribute("aria-expanded", "false");
137
- toggleBtn.classList.remove("is-open");
138
- navOverlay.classList.remove("is-open");
139
- document.body.classList.remove("menu-lock-scroll");
140
- }
143
+ newToggleBtn.setAttribute("aria-expanded", "false");
144
+ newToggleBtn.classList.remove("is-open");
145
+ navOverlay.classList.remove("is-open");
146
+ document.body.classList.remove("menu-lock-scroll");
141
147
  };
142
148
 
143
- if (toggleBtn && navOverlay) {
144
- toggleBtn.addEventListener("click", () => {
145
- const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
146
- toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
147
- toggleBtn.classList.toggle("is-open");
148
- navOverlay.classList.toggle("is-open");
149
- document.body.classList.toggle("menu-lock-scroll");
150
- });
149
+ newToggleBtn.addEventListener("click", e => {
150
+ e.stopPropagation();
151
+ const isOpen = newToggleBtn.getAttribute("aria-expanded") === "true";
152
+ newToggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
153
+ newToggleBtn.classList.toggle("is-open");
154
+ navOverlay.classList.toggle("is-open");
155
+ document.body.classList.toggle("menu-lock-scroll");
156
+ });
151
157
 
152
- const navLinks = navOverlay.querySelectorAll("a");
153
- navLinks.forEach(link => {
154
- link.addEventListener("click", () => {
155
- const targetUrl = new URL(link.href);
156
- const currentUrl = new window.URL(window.location.href);
157
- if (targetUrl.pathname === currentUrl.pathname) closeMenu();
158
- });
158
+ // Close menu when clicking nav links
159
+ const navLinks = navOverlay.querySelectorAll("a");
160
+ navLinks.forEach(link => {
161
+ link.addEventListener("click", () => {
162
+ closeMenu();
159
163
  });
160
- }
161
-
162
- window.addEventListener("pageshow", closeMenu);
164
+ });
163
165
 
166
+ // Close details dropdowns when clicking outside
164
167
  document.addEventListener("click", event => {
165
168
  const allDetails = document.querySelectorAll("details.nav-details");
166
169
  allDetails.forEach(details => {
167
- if (
168
- details.hasAttribute("open") &&
169
- !details.contains(event.target as Node)
170
- ) {
170
+ if (details.hasAttribute("open") && !details.contains(event.target)) {
171
171
  details.removeAttribute("open");
172
172
  }
173
173
  });
174
174
  });
175
- });
175
+
176
+ window.addEventListener("pageshow", closeMenu);
177
+ };
178
+
179
+ // Run on first load
180
+ if (document.readyState === "loading") {
181
+ document.addEventListener("DOMContentLoaded", initMobileMenu);
182
+ } else {
183
+ initMobileMenu();
184
+ }
185
+
186
+ // Run on every Astro View Transition page navigation
187
+ document.addEventListener("astro:page-load", initMobileMenu);
176
188
  </script>
177
189
 
178
190
  <style>
179
- /* Keeps all of your exact original CSS styles intact! */
180
191
  .mobile-toggle {
181
192
  display: flex;
182
193
  flex-direction: column;
@@ -28,12 +28,11 @@ const {
28
28
  language,
29
29
  } = Astro.props;
30
30
 
31
- // 🎯 Language Cascade: explicit prop -> node language -> fallback undefined
32
- // (MetaHead will resolve channel.defaultLanguage if this remains undefined)
31
+ // 🎯 Language Cascade
33
32
  const pageLang = language || node?.language || undefined;
34
33
 
35
- // ❌ Removed: const footerBlocks = content?.footer || [];
36
- // (The new Footer component handles its own data now!)
34
+ // 🎯 Extract the footer blocks the backend already thoughtfully provided
35
+ const footerBlocks = content?.footer || [];
37
36
  ---
38
37
 
39
38
  <!doctype html>
@@ -57,6 +56,7 @@ const pageLang = language || node?.language || undefined;
57
56
  </div>
58
57
  </main>
59
58
 
60
- <Footer />
59
+ <!-- 🎯 Pass the blocks and content map directly -->
60
+ <Footer blocks={footerBlocks} contentMap={content} />
61
61
  </body>
62
62
  </html>
@@ -113,6 +113,12 @@ export async function getChannel(
113
113
 
114
114
  if (!res.ok) return null;
115
115
 
116
+ // 🎯 Defensive HTML check
117
+ if (res.headers.get("content-type")?.includes("text/html")) {
118
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Channel: ${requestUrl}`);
119
+ return null;
120
+ }
121
+
116
122
  return (await res.json()) as ChannelResponse;
117
123
  } catch (err) {
118
124
  console.error('[CMS Client] Failed to fetch public channel:', err);
@@ -141,6 +147,12 @@ export async function getTree(
141
147
 
142
148
  if (!res.ok) return [];
143
149
 
150
+ // 🎯 Defensive HTML check
151
+ if (res.headers.get("content-type")?.includes("text/html")) {
152
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Tree: ${requestUrl}`);
153
+ return [];
154
+ }
155
+
144
156
  const data = await res.json();
145
157
  return data.items || [];
146
158
  } catch (err) {
@@ -159,7 +171,9 @@ export async function getNodeByPath(
159
171
  preview = false,
160
172
  customFetch: typeof fetch = fetch
161
173
  ): Promise<NodeResponse | null> {
162
- const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'home';
174
+ // 🎯 We keep 'index' as the fallback string for Hono routing compatibility,
175
+ // because the backend dynamically intercepts 'index' and returns the channelRoot!
176
+ const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'index';
163
177
  const baseUrl = getCmsUrl();
164
178
  const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
165
179
 
@@ -172,9 +186,15 @@ export async function getNodeByPath(
172
186
 
173
187
  if (!res.ok) return null;
174
188
 
189
+ // 🎯 Defensive HTML check (Prevents the dreaded "<!DOCTYPE html" JSON parse crash)
190
+ if (res.headers.get("content-type")?.includes("text/html")) {
191
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Node: ${requestUrl}`);
192
+ return null;
193
+ }
194
+
175
195
  return (await res.json()) as NodeResponse;
176
196
  } catch (err) {
177
- console.error('[CMS Client] Failed to fetch node by path:', err);
197
+ console.error(`[CMS Client] Failed to fetch node by path (${cleanPath}):`, err);
178
198
  return null;
179
199
  }
180
200
  }
@@ -162,10 +162,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
162
162
  if (body.purge_everything) {
163
163
  // 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
164
164
  await HTML_CACHE.put("site_version", now);
165
-
166
- // 🎯 FIX: Force clear alias keys during global purge
165
+ // Ensure root alias is forced to new version
167
166
  await HTML_CACHE.put("v:/", now);
168
- await HTML_CACHE.put("v:/index", now);
169
167
 
170
168
  console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
171
169
  } else if (Array.isArray(body.urls) && body.urls.length > 0) {
@@ -176,12 +174,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
176
174
  const cleanPath = urlPath.replace(/\/$/, "") || "/";
177
175
 
178
176
  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
177
  }
186
178
  await Promise.all(purgeTasks);
187
179
  console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
@@ -196,11 +188,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
196
188
  if (cache) {
197
189
  const rootUrls = [
198
190
  origin,
199
- `${origin}/`,
200
- `${origin}/index`,
201
- `${origin}/index/`
191
+ `${origin}/`
202
192
  ];
203
-
204
193
  for (const url of rootUrls) {
205
194
  await cache.delete(new Request(url));
206
195
  }
@@ -215,7 +204,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
215
204
  });
216
205
  }
217
206
 
218
- // D. Surgical Legacy Worker Cache Purge (For Updated Pages)
207
+ // D. Surgical Legacy Worker Cache Purge
219
208
  if (Array.isArray(body.urls) && body.urls.length > 0) {
220
209
  const results = [];
221
210
 
@@ -223,17 +212,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
223
212
  const cleanUrl = targetUrl.split('?')[0];
224
213
  const urlNoSlash = cleanUrl.replace(/\/$/, "");
225
214
  const urlWithSlash = urlNoSlash + "/";
226
-
227
215
  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
216
 
238
217
  let purged = false;
239
218
  if (cache) {
@@ -279,25 +258,12 @@ export const onRequest = defineMiddleware(async (context, next) => {
279
258
  }
280
259
 
281
260
  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
261
  }
289
262
 
290
263
  const cleanUrl = targetUrl.split('?')[0];
291
264
  const urlNoSlash = cleanUrl.replace(/\/$/, "");
292
265
  const urlWithSlash = urlNoSlash + "/";
293
-
294
266
  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
267
 
302
268
  let purgedLegacy = false;
303
269
  if (cache) {
@@ -338,11 +304,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
338
304
  // 🎯 1. KV STATIC "FILE" CACHE READ WITH VERSION TAGGING
339
305
  const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
340
306
  const isHtmlPage = !pathname.includes('.') && !pathname.startsWith('/api/');
341
- const cleanLookupPath = pathname.replace(/\/$/, "") || "/";
307
+ // 🎯 The ONLY way we refer to the homepage in KV is "/"
308
+ const cleanLookupPath = pathname === "/index" || pathname === "/index/"
309
+ ? "/"
310
+ : (pathname.replace(/\/$/, "") || "/");
342
311
 
343
312
  let activeCacheKey = cleanLookupPath;
344
313
 
345
- // 🎯 FIX: Bypass KV Read in local dev OR if we are time traveling (previewing drafts/snapshots)
346
314
  if (!isDev && isHtmlPage && HTML_CACHE && !isWarmer && !isTimeTraveling) {
347
315
  try {
348
316
  const [siteVer, pageVer] = await Promise.all([
@@ -373,7 +341,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
373
341
  const response = await next();
374
342
 
375
343
  // 🎯 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
344
  if (
378
345
  !isDev &&
379
346
  isHtmlPage &&
@@ -390,14 +357,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
390
357
 
391
358
  await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
392
359
 
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
360
  const siteVer = await HTML_CACHE.get("site_version") || "1";
400
-
401
361
  activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
402
362
  }
403
363
 
@@ -428,7 +388,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
428
388
  }
429
389
 
430
390
  // 🎯 4. STATIC ASSET CACHING FOR COMPRESSED BUNDLES / ASSETS
431
- // 🎯 DEV FIX: Do not apply max-age=31536000 locally to prevent breaking Vite HMR
432
391
  if (!isDev) {
433
392
  if (
434
393
  pathname.startsWith("/_image") ||
@@ -465,8 +424,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
465
424
  } else {
466
425
  // Production headers
467
426
  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
427
  response.headers.set(
471
428
  "Content-Security-Policy",
472
429
  `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};`