@brightsideoy/kama 0.1.0-alpha.98 → 1.0.0-alpha.10

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 (119) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +726 -261
  3. package/content/definitions/blocks/button.yaml +34 -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/node-list.yaml +61 -0
  10. package/content/definitions/blocks/single_image.yaml +27 -0
  11. package/content/definitions/blocks/text.yaml +15 -0
  12. package/content/definitions/channels/ecommerce.yaml +28 -0
  13. package/content/definitions/channels/website.yaml +41 -0
  14. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  15. package/content/definitions/field-sets/channel-base.yaml +57 -0
  16. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  17. package/content/definitions/field-sets/content-block.yaml +41 -0
  18. package/content/definitions/field-sets/page-base.yaml +44 -0
  19. package/content/definitions/field-sets/page-seo.yaml +34 -0
  20. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  21. package/content/definitions/field-sets/schema-type.yaml +43 -0
  22. package/content/definitions/field-sets/tree-base.yaml +31 -0
  23. package/content/definitions/node-layouts/content.yml +12 -0
  24. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  25. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  26. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  27. package/content/definitions/nodes/address.yaml +40 -0
  28. package/content/definitions/nodes/article.yaml +65 -0
  29. package/content/definitions/nodes/event.yaml +55 -0
  30. package/content/definitions/nodes/external-link.yaml +29 -0
  31. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  32. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  33. package/content/definitions/nodes/person.yaml +37 -0
  34. package/content/definitions/nodes/product.yaml +89 -0
  35. package/content/definitions/taxonomies/categories.yaml +15 -0
  36. package/content/definitions/taxonomies/tags.yaml +12 -0
  37. package/content/definitions/trees/navigation.yaml +24 -0
  38. package/definitions/editor/default.ts +1 -0
  39. package/dist/assets/index-Bzbme3Ob.css +1 -0
  40. package/dist/assets/index-DX98CiK-.js +164 -0
  41. package/dist/dist-CErcWOwp.js +27136 -0
  42. package/dist/index.html +4 -2
  43. package/dist/index.js +26159 -8575
  44. package/dist/schema-compiler.js +48 -0
  45. package/documentation/SCHEMAS.md +182 -0
  46. package/package.json +23 -25
  47. package/schema.sql +67 -23
  48. package/templates/astro-starter/astro.config.mjs +27 -0
  49. package/templates/astro-starter/package.json.template +20 -0
  50. package/templates/astro-starter/public/robots.txt +23 -0
  51. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
  52. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  53. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
  54. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  55. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  56. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
  57. package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
  58. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  59. package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
  60. package/templates/astro-starter/src/components/global/Footer.astro +50 -0
  61. package/templates/astro-starter/src/components/global/Header.astro +171 -0
  62. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  63. package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
  64. package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
  65. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  66. package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
  67. package/templates/astro-starter/src/lib/cms.ts +219 -0
  68. package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
  69. package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
  70. package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
  71. package/templates/astro-starter/src/middleware.ts +508 -0
  72. package/templates/astro-starter/src/pages/404.astro +46 -0
  73. package/templates/astro-starter/src/pages/[...slug].astro +318 -0
  74. package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
  75. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  76. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  77. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  78. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  79. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  80. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  81. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  82. package/templates/astro-starter/tsconfig.json +0 -0
  83. package/templates/kama-worker/index.js +19 -0
  84. package/templates/kama-worker/server.js +104 -0
  85. package/wrangler.json +22 -0
  86. package/defaults/definitions/blocks/button.yaml +0 -15
  87. package/defaults/definitions/blocks/header.yaml +0 -15
  88. package/defaults/definitions/blocks/header_image.yaml +0 -22
  89. package/defaults/definitions/blocks/hero.yaml +0 -27
  90. package/defaults/definitions/blocks/image_card.yaml +0 -57
  91. package/defaults/definitions/blocks/single_image.yaml +0 -28
  92. package/defaults/definitions/blocks/text.yaml +0 -16
  93. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  94. package/defaults/definitions/channels/website.yaml +0 -32
  95. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  96. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  97. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  98. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  99. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  100. package/defaults/definitions/nodes/address.yaml +0 -32
  101. package/defaults/definitions/nodes/article.yaml +0 -43
  102. package/defaults/definitions/nodes/event.yaml +0 -49
  103. package/defaults/definitions/nodes/external-link.yaml +0 -26
  104. package/defaults/definitions/nodes/person.yaml +0 -37
  105. package/defaults/definitions/nodes/product.yaml +0 -86
  106. package/defaults/definitions/trees/navigation.yaml +0 -29
  107. package/defaults/definitions/trees/social-links.yaml +0 -9
  108. package/definitions/_page.yaml +0 -52
  109. package/dist/assets/index-TdW5F0pX.css +0 -1
  110. package/dist/assets/index-u9OkBauM.js +0 -1392
  111. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  112. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  113. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  114. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  115. package/scripts/bundle-definitions.js +0 -176
  116. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  117. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  118. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  119. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
@@ -0,0 +1,278 @@
1
+ ---
2
+ // src/components/blocks/NodeList.astro
3
+
4
+ interface Props {
5
+ data: Record<string, any>;
6
+ }
7
+
8
+ const { data } = Astro.props;
9
+ const items = data.initialItems || [];
10
+ const listTitle = data.listTitle || "";
11
+ ---
12
+
13
+ <div class="cms-block node-list-wrapper">
14
+ {listTitle && <h2 class="node-list-title">{listTitle}</h2>}
15
+
16
+ {
17
+ items.length === 0 ? (
18
+ <p class="empty-state">No items found.</p>
19
+ ) : (
20
+ <div class="node-grid" id={`grid-${data.blockId}`}>
21
+ {items.map((item: any) => (
22
+ <article class="node-card">
23
+ <a href={`/${item.slug}`} class="node-card-link">
24
+ {item.image ? (
25
+ <img
26
+ src={item.image.url}
27
+ alt={item.image.alt || item.title}
28
+ loading="lazy"
29
+ />
30
+ ) : (
31
+ <div class="image-placeholder" />
32
+ )}
33
+ <div class="node-card-content">
34
+ <h3 class="node-card-title">{item.title}</h3>
35
+ {item.createdAt && (
36
+ <time datetime={item.createdAt} class="node-card-date">
37
+ {new Date(item.createdAt).toLocaleDateString("en-GB", {
38
+ day: "numeric",
39
+ month: "short",
40
+ year: "numeric",
41
+ })}
42
+ </time>
43
+ )}
44
+ {item.excerpt && (
45
+ <p class="node-card-excerpt">{item.excerpt}</p>
46
+ )}
47
+ </div>
48
+ </a>
49
+ </article>
50
+ ))}
51
+ </div>
52
+ )
53
+ }
54
+
55
+ {
56
+ data.loadMoreUrl && (
57
+ <div class="load-more-container">
58
+ <button
59
+ class="load-more-btn"
60
+ data-url={data.loadMoreUrl}
61
+ data-target={`grid-${data.blockId}`}
62
+ >
63
+ Load More
64
+ </button>
65
+ </div>
66
+ )
67
+ }
68
+ </div>
69
+ <style is:global>
70
+ .node-list-wrapper {
71
+ margin: 2rem 0;
72
+ }
73
+
74
+ .node-list-title {
75
+ font-size: 1.5rem;
76
+ font-weight: 700;
77
+ margin-bottom: 1.5rem;
78
+ color: #1e293b;
79
+ }
80
+
81
+ .node-list-wrapper .empty-state {
82
+ color: #64748b;
83
+ font-style: italic;
84
+ }
85
+
86
+ .node-grid {
87
+ display: grid;
88
+ grid-template-columns: repeat(1, 1fr);
89
+ gap: 1.5rem;
90
+ }
91
+
92
+ @media (min-width: 640px) {
93
+ .node-grid {
94
+ grid-template-columns: repeat(2, 1fr);
95
+ }
96
+ }
97
+
98
+ @media (min-width: 1024px) {
99
+ .node-grid {
100
+ grid-template-columns: repeat(3, 1fr);
101
+ }
102
+ }
103
+
104
+ .node-card {
105
+ border: 1px solid #e2e8f0;
106
+ border-radius: 8px;
107
+ overflow: hidden;
108
+ background: #fff;
109
+ transition:
110
+ transform 0.2s ease,
111
+ box-shadow 0.2s ease;
112
+ }
113
+
114
+ .node-card:hover {
115
+ transform: translateY(-2px);
116
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
117
+ }
118
+
119
+ .node-card-link {
120
+ text-decoration: none;
121
+ color: inherit;
122
+ display: flex;
123
+ flex-direction: column;
124
+ height: 100%;
125
+ }
126
+
127
+ .node-card img,
128
+ .node-card .image-placeholder {
129
+ width: 100%;
130
+ aspect-ratio: 16 / 9;
131
+ object-fit: cover;
132
+ background: #f1f5f9;
133
+ }
134
+
135
+ .node-card-content {
136
+ padding: 1.25rem;
137
+ display: flex;
138
+ flex-direction: column;
139
+ flex-grow: 1;
140
+ }
141
+
142
+ .node-card-title {
143
+ font-size: 1.125rem;
144
+ font-weight: 600;
145
+ margin: 0 0 0.5rem 0;
146
+ color: #0f172a;
147
+ line-height: 1.4;
148
+ }
149
+
150
+ .node-card-date {
151
+ font-size: 0.875rem;
152
+ color: #64748b;
153
+ margin-bottom: 0.75rem;
154
+ }
155
+
156
+ .node-card-excerpt {
157
+ font-size: 0.9375rem;
158
+ color: #475569;
159
+ margin: 0;
160
+ line-height: 1.5;
161
+ display: -webkit-box;
162
+ -webkit-line-clamp: 3;
163
+ -webkit-box-orient: vertical;
164
+ overflow: hidden;
165
+ }
166
+
167
+ .load-more-container {
168
+ margin-top: 2rem;
169
+ text-align: center;
170
+ }
171
+
172
+ .load-more-btn {
173
+ padding: 0.75rem 1.5rem;
174
+ background: #0f172a;
175
+ color: #fff;
176
+ border: none;
177
+ border-radius: 6px;
178
+ font-weight: 600;
179
+ cursor: pointer;
180
+ transition: background 0.2s;
181
+ }
182
+
183
+ .load-more-btn:hover {
184
+ background: #334155;
185
+ }
186
+ </style>
187
+
188
+ <script>
189
+ document.addEventListener("DOMContentLoaded", () => {
190
+ const buttons = document.querySelectorAll(".load-more-btn");
191
+
192
+ buttons.forEach(btn => {
193
+ btn.addEventListener("click", async e => {
194
+ const targetBtn = e.currentTarget as HTMLButtonElement;
195
+ const urlString = targetBtn.getAttribute("data-url");
196
+ const targetId = targetBtn.getAttribute("data-target");
197
+
198
+ if (!urlString || !targetId) return;
199
+
200
+ const grid = document.getElementById(targetId);
201
+ if (!grid) return;
202
+
203
+ // Change the original backend URL path to our local Astro proxy path
204
+ const urlObj = new URL(urlString, window.location.origin);
205
+ const proxyUrl = `/api/load-nodes${urlObj.search}`;
206
+
207
+ const originalText = targetBtn.textContent;
208
+ targetBtn.textContent = "Loading...";
209
+ targetBtn.disabled = true;
210
+
211
+ try {
212
+ const res = await fetch(proxyUrl);
213
+ const data = await res.json();
214
+
215
+ if (data.items && data.items.length > 0) {
216
+ // Append each new item to the grid
217
+ data.items.forEach((item: any) => {
218
+ const dateStr = item.createdAt
219
+ ? new Date(item.createdAt).toLocaleDateString("en-GB", {
220
+ day: "numeric",
221
+ month: "short",
222
+ year: "numeric",
223
+ })
224
+ : "";
225
+
226
+ const imgHtml = item.image
227
+ ? `<img src="${item.image.url}" alt="${item.image.alt || item.title}" loading="lazy" />`
228
+ : `<div class="image-placeholder"></div>`;
229
+
230
+ const articleHtml = `
231
+ <article class="node-card">
232
+ <a href="/${item.slug}" class="node-card-link">
233
+ ${imgHtml}
234
+ <div class="node-card-content">
235
+ <h3 class="node-card-title">${item.title}</h3>
236
+ ${dateStr ? `<time datetime="${item.createdAt}" class="node-card-date">${dateStr}</time>` : ""}
237
+ ${item.excerpt ? `<p class="node-card-excerpt">${item.excerpt}</p>` : ""}
238
+ </div>
239
+ </a>
240
+ </article>
241
+ `;
242
+ grid.insertAdjacentHTML("beforeend", articleHtml);
243
+ });
244
+
245
+ // If we received a full batch, there might be more. Update the offset.
246
+ if (data.items.length === data.limit) {
247
+ const currentOffset = parseInt(
248
+ urlObj.searchParams.get("offset") || "0",
249
+ 10,
250
+ );
251
+ urlObj.searchParams.set(
252
+ "offset",
253
+ (currentOffset + data.limit).toString(),
254
+ );
255
+ targetBtn.setAttribute(
256
+ "data-url",
257
+ urlObj.pathname + urlObj.search,
258
+ );
259
+ targetBtn.textContent = "Load More";
260
+ targetBtn.disabled = false;
261
+ } else {
262
+ // Hide the button container if we've reached the end
263
+ if (targetBtn.parentElement)
264
+ targetBtn.parentElement.style.display = "none";
265
+ }
266
+ } else {
267
+ if (targetBtn.parentElement)
268
+ targetBtn.parentElement.style.display = "none";
269
+ }
270
+ } catch (err) {
271
+ console.error("Failed to load more items:", err);
272
+ targetBtn.textContent = "Error. Try again";
273
+ targetBtn.disabled = false;
274
+ }
275
+ });
276
+ });
277
+ });
278
+ </script>
@@ -0,0 +1,15 @@
1
+ ---
2
+ // src/components/blocks/TextBlock.astro
3
+
4
+ interface Props {
5
+ data: {
6
+ text?: string;
7
+ content?: string;
8
+ };
9
+ }
10
+ const { data } = Astro.props;
11
+ ---
12
+
13
+ <div class="block-text">
14
+ <Fragment set:html={data.text || data.content || ""} />
15
+ </div>
@@ -0,0 +1,108 @@
1
+ ---
2
+ // src/components/global/Breadcrumb.astro
3
+
4
+ interface Props {
5
+ title: string;
6
+ slug?: string;
7
+ isHomePage?: boolean;
8
+ }
9
+
10
+ const { title, isHomePage } = Astro.props;
11
+
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
+ });
39
+ ---
40
+
41
+ <nav class="breadcrumb-nav" aria-label="Breadcrumb">
42
+ <ol class="breadcrumb-list">
43
+ <!-- Home Link -->
44
+ <li class="breadcrumb-item">
45
+ <a href="/">Home</a>
46
+ </li>
47
+
48
+ <!-- Subpage Breadcrumbs -->
49
+ {
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
+ ))
64
+ }
65
+ </ol>
66
+ </nav>
67
+
68
+ <style>
69
+ .breadcrumb-nav {
70
+ margin-bottom: 16px;
71
+ }
72
+
73
+ .breadcrumb-list {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ list-style: none;
78
+ margin: 0;
79
+ padding: 0;
80
+ font-size: 13px;
81
+ color: #64748b;
82
+ }
83
+
84
+ .breadcrumb-item {
85
+ display: flex;
86
+ align-items: center;
87
+ }
88
+
89
+ .breadcrumb-item a {
90
+ color: #334155;
91
+ text-decoration: none;
92
+ }
93
+
94
+ .breadcrumb-item a:hover {
95
+ color: #0f172a;
96
+ text-decoration: underline;
97
+ }
98
+
99
+ .breadcrumb-item.active {
100
+ color: #0f172a;
101
+ font-weight: 500;
102
+ }
103
+
104
+ .separator {
105
+ margin-right: 8px;
106
+ color: #94a3b8;
107
+ }
108
+ </style>
@@ -0,0 +1,50 @@
1
+ ---
2
+ // src/components/global/Footer.astro
3
+
4
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
+
6
+ interface Props {
7
+ blocks?: any[];
8
+ contentMap?: Record<string, any[]>;
9
+ }
10
+
11
+ const { blocks = [], contentMap = {} } = Astro.props;
12
+ ---
13
+
14
+ <footer class="site-footer">
15
+ <div class="footer-container">
16
+ {
17
+ blocks && blocks.length > 0 ? (
18
+ <BlockRenderer
19
+ blocks={blocks}
20
+ contentMap={contentMap}
21
+ region="footer"
22
+ />
23
+ ) : (
24
+ <div class="footer-default">
25
+ <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
26
+ </div>
27
+ )
28
+ }
29
+ </div>
30
+ </footer>
31
+
32
+ <style>
33
+ .site-footer {
34
+ background: #ffffff;
35
+ border-top: 1px solid #e2e8f0;
36
+ margin-top: auto;
37
+ }
38
+
39
+ .footer-container {
40
+ max-width: 1400px;
41
+ margin: 0 auto;
42
+ padding: 24px 20px;
43
+ }
44
+
45
+ .footer-default p {
46
+ margin: 0;
47
+ font-size: 14px;
48
+ color: #64748b;
49
+ }
50
+ </style>
@@ -0,0 +1,171 @@
1
+ ---
2
+ // src/components/global/Header.astro
3
+
4
+ import NavigationMenu from "@/components/global/NavigationMenu.astro";
5
+ import KamaImage from "@/components/ui/KamaImage.astro";
6
+ import { getTree, getTreeId, getChannel, getChannelId } from "@/lib/cms";
7
+ import type { TreeNode } from "@/lib/cms";
8
+
9
+ interface Props {
10
+ node?: Record<string, any>;
11
+ tree?: TreeNode[];
12
+ }
13
+
14
+ const props = Astro.props as Props;
15
+ const currentPropsNode = props.node || {};
16
+ let rawTree: TreeNode[] = props.tree || [];
17
+
18
+ const safeEnv = (Astro.locals as any).safeEnv || {};
19
+ const cmsWorker = safeEnv.CMS_WORKER;
20
+
21
+ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
22
+ const reqInit = { ...init };
23
+ delete reqInit.cache;
24
+
25
+ let urlStr = input instanceof URL ? input.toString() : String(input);
26
+
27
+ if (urlStr.startsWith("/")) {
28
+ const baseUrl =
29
+ import.meta.env.PUBLIC_API_URL ||
30
+ safeEnv.PUBLIC_API_URL ||
31
+ "http://127.0.0.1:8787";
32
+ urlStr = `${baseUrl}${urlStr}`;
33
+ }
34
+
35
+ const req = new Request(urlStr, reqInit);
36
+
37
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
38
+ return cmsWorker.fetch(req);
39
+ }
40
+ return fetch(req);
41
+ };
42
+
43
+ // 🎯 Resolve Channel ID dynamically from env/lib without hardcoding "website"
44
+ const channelId = getChannelId();
45
+ const treeId = getTreeId();
46
+
47
+ let channelData: Record<string, any> | null = null;
48
+
49
+ try {
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;
56
+ rawTree = fetchedTree || [];
57
+ } else {
58
+ channelData = await getChannel(channelId, customFetch);
59
+ }
60
+ } catch (e) {
61
+ console.error("[Header] Failed to load layout data:", e);
62
+ }
63
+
64
+ const rawLogoText = channelData?.logoText;
65
+ const rawSiteTitle = channelData?.siteTitle;
66
+ const rawLabel = channelData?.label;
67
+
68
+ let logoText = "";
69
+
70
+ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
71
+ logoText = rawLogoText;
72
+ } else if (typeof rawSiteTitle === "string" && rawSiteTitle.trim() !== "") {
73
+ logoText = rawSiteTitle;
74
+ } else if (typeof rawLabel === "string" && rawLabel.trim() !== "") {
75
+ logoText = rawLabel;
76
+ } else if (currentPropsNode.logoText) {
77
+ logoText = currentPropsNode.logoText;
78
+ } else {
79
+ logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
80
+ }
81
+
82
+ const rawLogoAsset = channelData?.logoImage || channelData?.logo;
83
+ const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
84
+
85
+ const hasValidLogoImage = Boolean(
86
+ targetLogo &&
87
+ ((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
88
+ targetLogo.url ||
89
+ targetLogo.path ||
90
+ targetLogo.variants),
91
+ );
92
+ ---
93
+
94
+ <header class="site-header">
95
+ <div class="header-container main-header-row">
96
+ <a href="/" class="logo-block">
97
+ {
98
+ hasValidLogoImage ? (
99
+ <KamaImage
100
+ image={targetLogo}
101
+ idealWidth={300}
102
+ class="logo-image"
103
+ loading="eager"
104
+ fetchpriority="high"
105
+ />
106
+ ) : (
107
+ <span class="logo-text">{logoText}</span>
108
+ )
109
+ }
110
+ </a>
111
+ <NavigationMenu tree={rawTree} />
112
+ </div>
113
+ </header>
114
+
115
+ <style>
116
+ .site-header {
117
+ background: #ffffff;
118
+ color: #1e293b;
119
+ position: sticky;
120
+ top: 0;
121
+ z-index: 100;
122
+ border-bottom: 1px solid #e2e8f0;
123
+ }
124
+
125
+ .header-container {
126
+ max-width: 1400px;
127
+ margin: 0 auto;
128
+ padding: 0 20px;
129
+ width: 100%;
130
+ box-sizing: border-box;
131
+ }
132
+
133
+ .main-header-row {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ height: 64px;
138
+ }
139
+
140
+ .logo-block {
141
+ text-decoration: none;
142
+ color: #0f172a;
143
+ font-weight: 700;
144
+ font-size: 20px;
145
+ display: inline-flex;
146
+ align-items: center;
147
+ z-index: 1001;
148
+ }
149
+
150
+ :global(.logo-image) {
151
+ height: 16px !important;
152
+ max-height: 16px !important;
153
+ width: auto !important;
154
+ object-fit: contain;
155
+ display: block;
156
+ }
157
+
158
+ .logo-text {
159
+ white-space: nowrap;
160
+ font-size: 16px;
161
+ line-height: 16px;
162
+ }
163
+
164
+ @media (min-width: 768px) {
165
+ .main-header-row {
166
+ height: 64px;
167
+ justify-content: space-between;
168
+ gap: 32px;
169
+ }
170
+ }
171
+ </style>