@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.
- package/bin/cli.js +263 -145
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/dist/assets/index-C3LzwJjY.css +1 -0
- package/dist/assets/index-DhguVxzs.js +164 -0
- package/dist/{dist-Dy7-HUEG.js → dist-cK3xGnBZ.js} +940 -785
- package/dist/index.html +2 -2
- package/dist/index.js +982 -823
- package/dist/schema-compiler.js +5 -5
- package/package.json +2 -1
- package/schema.sql +19 -1
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +57 -46
- package/templates/astro-starter/src/layouts/StandardLayout.astro +5 -5
- package/templates/astro-starter/src/lib/cms.ts +22 -2
- package/templates/astro-starter/src/middleware.ts +7 -50
- package/templates/astro-starter/src/pages/[...slug].astro +30 -24
- package/templates/kama-worker/index.js +3 -3
- package/templates/kama-worker/server.js +11 -5
- package/dist/assets/index-F2rfUymo.css +0 -1
- package/dist/assets/index-lXxH3VFK.js +0 -164
package/dist/schema-compiler.js
CHANGED
|
@@ -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-
|
|
33
|
+
l = (await import("./dist-cK3xGnBZ.js")).compileDefinitions;
|
|
34
34
|
} catch (e) {
|
|
35
|
-
console.error("
|
|
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("
|
|
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(
|
|
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("
|
|
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.
|
|
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
|
|
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,
|
|
10
|
+
const { title, isHomePage } = Astro.props;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
+
blocks && blocks.length > 0 ? (
|
|
57
18
|
<BlockRenderer
|
|
58
|
-
blocks={
|
|
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
|
-
// 🎯
|
|
45
|
-
const channelId = getChannelId()
|
|
46
|
-
const treeId = getTreeId()
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
item
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
?
|
|
27
|
+
rootNode?.children && rootNode.children.length > 0
|
|
28
|
+
? rootNode.children
|
|
26
29
|
: tree.filter(
|
|
27
30
|
item =>
|
|
28
|
-
item.
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
32
|
-
// (MetaHead will resolve channel.defaultLanguage if this remains undefined)
|
|
31
|
+
// 🎯 Language Cascade
|
|
33
32
|
const pageLang = language || node?.language || undefined;
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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};`
|