@brightsideoy/kama 0.1.0-alpha.98 → 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-TdW5F0pX.css +0 -1
  107. package/dist/assets/index-u9OkBauM.js +0 -1392
  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,402 @@
1
+ ---
2
+ // src/components/global/NavigationMenu.astro
3
+ import type { TreeNode } from "@/lib/cms";
4
+
5
+ export interface Props {
6
+ tree?: TreeNode[];
7
+ }
8
+
9
+ const { tree = [] } = Astro.props;
10
+
11
+ // 🎯 Grab the current path directly from Astro's server request
12
+ const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
13
+ const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "index").trim();
14
+
15
+ const homeNode = tree.find(
16
+ item =>
17
+ item.slug === homeSlug ||
18
+ item.slug === "" ||
19
+ item.path === "/" ||
20
+ item.path === "",
21
+ );
22
+
23
+ const topLevelNavItems =
24
+ homeNode?.children && homeNode.children.length > 0
25
+ ? homeNode.children
26
+ : tree.filter(
27
+ item =>
28
+ item.slug !== homeSlug &&
29
+ item.slug !== "" &&
30
+ item.path !== "/" &&
31
+ item.path !== "",
32
+ );
33
+
34
+ const isPathActive = (itemPath?: string) => {
35
+ const normItem = (itemPath || "").replace(/\/$/, "") || "/";
36
+ if (normItem === "/") return currentPath === "/";
37
+ return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
38
+ };
39
+
40
+ const isExactActive = (itemPath?: string) => {
41
+ return currentPath === ((itemPath || "").replace(/\/$/, "") || "/");
42
+ };
43
+ ---
44
+
45
+ <!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
46
+ <button
47
+ type="button"
48
+ class="mobile-toggle"
49
+ id="menu-toggle"
50
+ aria-label="Toggle Navigation Menu"
51
+ aria-expanded="false"
52
+ >
53
+ <span class="line line-1"></span>
54
+ <span class="line line-2"></span>
55
+ </button>
56
+
57
+ <!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
58
+ <nav class="nav-block" id="nav-overlay">
59
+ <ul class="nav-list">
60
+ {
61
+ topLevelNavItems.map(item => {
62
+ const hasChildren = item.children && item.children.length > 0;
63
+ const isParentActive = isPathActive(item.path);
64
+
65
+ if (hasChildren) {
66
+ return (
67
+ <li class="nav-item">
68
+ <details class="nav-details">
69
+ <summary
70
+ class:list={[
71
+ "nav-button",
72
+ "dropdown-summary",
73
+ { active: isParentActive },
74
+ ]}
75
+ >
76
+ <span>{item.title}</span>
77
+ <span class="indicator">▼</span>
78
+ </summary>
79
+ <div class="submenu-container">
80
+ <ul class="dropdown-panel">
81
+ <li>
82
+ <a
83
+ href={item.path}
84
+ class:list={[
85
+ "dropdown-item",
86
+ "overview-item",
87
+ { active: isExactActive(item.path) },
88
+ ]}
89
+ >
90
+ {item.title} Overview
91
+ </a>
92
+ </li>
93
+ {item.children?.map(child => (
94
+ <li>
95
+ <a
96
+ href={child.path}
97
+ class:list={[
98
+ "dropdown-item",
99
+ { active: isExactActive(child.path) },
100
+ ]}
101
+ >
102
+ {child.title}
103
+ </a>
104
+ </li>
105
+ ))}
106
+ </ul>
107
+ </div>
108
+ </details>
109
+ </li>
110
+ );
111
+ }
112
+
113
+ return (
114
+ <li class="nav-item">
115
+ <a
116
+ href={item.path}
117
+ class:list={["nav-button", { active: isExactActive(item.path) }]}
118
+ >
119
+ {item.title}
120
+ </a>
121
+ </li>
122
+ );
123
+ })
124
+ }
125
+ </ul>
126
+ </nav>
127
+
128
+ <script>
129
+ // 🎯 Clean, standard event listeners that run when the HTML is parsed
130
+ document.addEventListener("DOMContentLoaded", () => {
131
+ const toggleBtn = document.getElementById("menu-toggle");
132
+ const navOverlay = document.getElementById("nav-overlay");
133
+
134
+ 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
+ }
141
+ };
142
+
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
+ });
151
+
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
+ });
159
+ });
160
+ }
161
+
162
+ window.addEventListener("pageshow", closeMenu);
163
+
164
+ document.addEventListener("click", event => {
165
+ const allDetails = document.querySelectorAll("details.nav-details");
166
+ allDetails.forEach(details => {
167
+ if (
168
+ details.hasAttribute("open") &&
169
+ !details.contains(event.target as Node)
170
+ ) {
171
+ details.removeAttribute("open");
172
+ }
173
+ });
174
+ });
175
+ });
176
+ </script>
177
+
178
+ <style>
179
+ /* Keeps all of your exact original CSS styles intact! */
180
+ .mobile-toggle {
181
+ display: flex;
182
+ flex-direction: column;
183
+ justify-content: center;
184
+ align-items: center;
185
+ gap: 6px;
186
+ width: 44px;
187
+ height: 44px;
188
+ background: transparent;
189
+ border: none;
190
+ cursor: pointer;
191
+ padding: 0;
192
+ z-index: 1001;
193
+ }
194
+ .mobile-toggle .line {
195
+ width: 22px;
196
+ height: 2px;
197
+ background-color: #0f172a;
198
+ transition:
199
+ transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
200
+ opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
201
+ transform-origin: center;
202
+ }
203
+ .mobile-toggle.is-open .line-1 {
204
+ transform: translateY(4px) rotate(45deg);
205
+ }
206
+ .mobile-toggle.is-open .line-2 {
207
+ transform: translateY(-4px) rotate(-45deg);
208
+ }
209
+ .nav-block {
210
+ position: fixed;
211
+ top: 0;
212
+ left: 0;
213
+ width: 100vw;
214
+ height: 100vh;
215
+ background-color: #ffffff;
216
+ padding: 80px 24px 40px 24px;
217
+ box-sizing: border-box;
218
+ display: flex;
219
+ flex-direction: column;
220
+ overflow-y: auto;
221
+ opacity: 0;
222
+ visibility: hidden;
223
+ pointer-events: none;
224
+ transition:
225
+ opacity 0.3s ease,
226
+ visibility 0.3s ease;
227
+ z-index: 1000;
228
+ }
229
+ .nav-block.is-open {
230
+ opacity: 1;
231
+ visibility: visible;
232
+ pointer-events: auto;
233
+ }
234
+
235
+ .nav-list {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 8px;
239
+ list-style: none;
240
+ margin: 0;
241
+ padding: 0;
242
+ width: 100%;
243
+ }
244
+ .nav-item {
245
+ position: relative;
246
+ width: 100%;
247
+ }
248
+ .nav-button {
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: space-between;
252
+ width: 100%;
253
+ height: 52px;
254
+ padding: 0 16px;
255
+ font-size: 18px;
256
+ font-weight: 500;
257
+ color: #334155;
258
+ text-decoration: none;
259
+ background: transparent;
260
+ border: none;
261
+ border-radius: 4px;
262
+ cursor: pointer;
263
+ box-sizing: border-box;
264
+ }
265
+ .nav-button:hover {
266
+ color: #0f172a;
267
+ background: #f8fafc;
268
+ }
269
+ .nav-button.active {
270
+ color: #0c4a6e;
271
+ font-weight: 600;
272
+ }
273
+ .dropdown-summary {
274
+ list-style: none;
275
+ user-select: none;
276
+ }
277
+ .dropdown-summary::-webkit-details-marker {
278
+ display: none;
279
+ }
280
+ .indicator {
281
+ font-size: 11px;
282
+ color: #64748b;
283
+ transition: transform 0.2s ease;
284
+ }
285
+ details[open] .indicator {
286
+ transform: rotate(180deg);
287
+ }
288
+ details.nav-details {
289
+ border-radius: 6px;
290
+ transition: background-color 0.2s ease;
291
+ }
292
+ details[open].nav-details {
293
+ background: #f8fafc;
294
+ padding-bottom: 8px;
295
+ }
296
+ .submenu-container {
297
+ position: relative;
298
+ margin-left: 24px;
299
+ padding-left: 16px;
300
+ border-left: 2px solid #cbd5e1;
301
+ }
302
+ .dropdown-panel {
303
+ list-style: none;
304
+ margin: 4px 0 0 0;
305
+ padding: 0;
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 4px;
309
+ }
310
+ .dropdown-item {
311
+ display: block;
312
+ padding: 10px 12px;
313
+ font-size: 16px;
314
+ font-weight: 400;
315
+ color: #475569;
316
+ text-decoration: none;
317
+ border-radius: 4px;
318
+ transition:
319
+ background 0.15s ease,
320
+ color 0.15s ease;
321
+ }
322
+ .overview-item {
323
+ font-weight: 500;
324
+ color: #334155;
325
+ }
326
+ .dropdown-item:hover {
327
+ background: #e2e8f0;
328
+ color: #0f172a;
329
+ }
330
+ .dropdown-item.active {
331
+ font-weight: 600;
332
+ color: #0c4a6e;
333
+ background: #e0f2fe;
334
+ }
335
+ body.menu-lock-scroll {
336
+ overflow: hidden !important;
337
+ }
338
+
339
+ @media (min-width: 768px) {
340
+ .mobile-toggle {
341
+ display: none;
342
+ }
343
+ .nav-block {
344
+ position: static;
345
+ width: 100%;
346
+ height: auto;
347
+ background: transparent;
348
+ padding: 0;
349
+ opacity: 1;
350
+ visibility: visible;
351
+ pointer-events: auto;
352
+ overflow-y: visible;
353
+ display: block;
354
+ }
355
+ .nav-list {
356
+ flex-direction: row;
357
+ gap: 4px;
358
+ justify-content: flex-end;
359
+ }
360
+ .nav-item {
361
+ width: auto;
362
+ }
363
+ .nav-button {
364
+ display: inline-flex;
365
+ justify-content: flex-start;
366
+ gap: 6px;
367
+ height: 40px;
368
+ padding: 0 16px;
369
+ font-size: 14px;
370
+ border-radius: 4px;
371
+ margin-top: 4px;
372
+ }
373
+ details[open].nav-details {
374
+ background: transparent;
375
+ padding-bottom: 0;
376
+ }
377
+ .submenu-container {
378
+ margin-left: 0;
379
+ padding-left: 0;
380
+ border-left: none;
381
+ }
382
+ .dropdown-panel {
383
+ position: absolute;
384
+ top: 100%;
385
+ left: 0;
386
+ min-width: 220px;
387
+ background: #ffffff;
388
+ border: 1px solid #e2e8f0;
389
+ margin: 0;
390
+ gap: 0;
391
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
392
+ border-radius: 4px;
393
+ padding: 4px;
394
+ }
395
+ .dropdown-item {
396
+ font-size: 14px;
397
+ padding: 8px 16px;
398
+ border-radius: 4px;
399
+ border-bottom: none;
400
+ }
401
+ }
402
+ </style>
@@ -0,0 +1,172 @@
1
+ ---
2
+ // src/components/ui/KamaImage.astro
3
+
4
+ import {
5
+ buildAgnosticImageUrl,
6
+ isSvgImage,
7
+ getImageUri,
8
+ } from "@/lib/media/media-url";
9
+
10
+ interface Props {
11
+ image: any;
12
+ sizes?: string;
13
+ desktopWidths?: number[];
14
+ idealWidth?: number;
15
+ class?: string;
16
+ loading?: "lazy" | "eager";
17
+ fetchpriority?: "auto" | "high" | "low";
18
+ }
19
+
20
+ const {
21
+ image = {},
22
+ sizes = "100vw",
23
+ desktopWidths = [768, 1352],
24
+ idealWidth = 1352,
25
+ class: className = "",
26
+ loading = "lazy",
27
+ fetchpriority = "auto",
28
+ } = Astro.props;
29
+
30
+ // Unpack array wrappers if passed
31
+ const targetImage = Array.isArray(image) ? image[0] : image;
32
+
33
+ // 🎯 FIX 1: Support string paths in addition to object payloads
34
+ const isValidImage = Boolean(
35
+ targetImage &&
36
+ (typeof targetImage === "string" ||
37
+ targetImage.url ||
38
+ targetImage.path ||
39
+ targetImage.variants),
40
+ );
41
+
42
+ // Detect if this asset is a vector SVG
43
+ const isSvg =
44
+ isSvgImage(targetImage?.mimeType || "") ||
45
+ isSvgImage(targetImage?.filename || "") ||
46
+ isSvgImage(targetImage?.url || targetImage?.path || "") ||
47
+ (typeof targetImage === "string" && isSvgImage(targetImage));
48
+
49
+ const variants = targetImage?.variants || {};
50
+
51
+ // Resolve base paths using getImageUri to ensure CSP compliance & relativity
52
+ const rawMobileUrl = getImageUri(variants.mobile?.url);
53
+ const rawTabletUrl = getImageUri(variants.tablet?.url);
54
+ const rawDesktopUrl = getImageUri(
55
+ variants.standard?.url ||
56
+ targetImage?.url ||
57
+ targetImage?.path ||
58
+ targetImage,
59
+ );
60
+
61
+ const mobileW = Number(variants.mobile?.cropCoordinates?.width || 768);
62
+ const mobileH = Number(variants.mobile?.cropCoordinates?.height || 576);
63
+
64
+ const tabletW = Number(variants.tablet?.cropCoordinates?.width || 1024);
65
+ const tabletH = Number(variants.tablet?.cropCoordinates?.height || 512);
66
+
67
+ // 🎯 FIX 2: Omit explicit width/height attributes on SVGs if intrinsic dimensions are missing/0
68
+ const rawWidth = Number(
69
+ variants.standard?.cropCoordinates?.width || targetImage?.width,
70
+ );
71
+ const rawHeight = Number(
72
+ variants.standard?.cropCoordinates?.height || targetImage?.height,
73
+ );
74
+
75
+ const baseWidth =
76
+ rawWidth && !isNaN(rawWidth) && rawWidth > 0 ? rawWidth : idealWidth;
77
+ const baseHeight =
78
+ rawHeight && !isNaN(rawHeight) && rawHeight > 0
79
+ ? rawHeight
80
+ : Math.round(idealWidth * 0.5);
81
+
82
+ function buildAgnosticSrcset(
83
+ url: string,
84
+ targetWidths: number[],
85
+ quality = 75,
86
+ ): string {
87
+ if (!url) return "";
88
+ return targetWidths
89
+ .map(width => `${buildAgnosticImageUrl(url, width, quality)} ${width}w`)
90
+ .join(", ");
91
+ }
92
+
93
+ // Generate srcsets only for raster images
94
+ const mobileSrcset =
95
+ !isSvg && rawMobileUrl ? buildAgnosticSrcset(rawMobileUrl, [400, 768]) : null;
96
+
97
+ const tabletSrcset =
98
+ !isSvg && rawTabletUrl
99
+ ? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
100
+ : null;
101
+
102
+ const desktopSrcset =
103
+ !isSvg && rawDesktopUrl
104
+ ? buildAgnosticSrcset(rawDesktopUrl, desktopWidths)
105
+ : null;
106
+
107
+ const fallbackUrl =
108
+ getImageUri(rawDesktopUrl, {
109
+ width: isSvg ? undefined : baseWidth,
110
+ quality: 75,
111
+ }) || "";
112
+ ---
113
+
114
+ {
115
+ isValidImage &&
116
+ (isSvg ? (
117
+ /* 🎯 Clean single <img> for SVG Vectors */
118
+ <img
119
+ src={fallbackUrl}
120
+ alt={targetImage.alt || targetImage.filename || ""}
121
+ width={rawWidth > 0 ? rawWidth : undefined}
122
+ height={rawHeight > 0 ? rawHeight : undefined}
123
+ loading={loading}
124
+ fetchpriority={fetchpriority}
125
+ decoding={loading === "eager" ? "sync" : "async"}
126
+ class={className}
127
+ />
128
+ ) : (
129
+ /* 🎯 Responsive <picture> for Raster Images (.png, .jpg, .webp) */
130
+ <picture>
131
+ {Boolean(mobileSrcset) && (
132
+ <source
133
+ media="(max-width: 767px)"
134
+ srcset={mobileSrcset as string}
135
+ sizes="(max-width: 767px) calc(100vw - 48px), 768px"
136
+ width={mobileW}
137
+ height={mobileH}
138
+ />
139
+ )}
140
+
141
+ {Boolean(tabletSrcset) && (
142
+ <source
143
+ media="(min-width: 768px) and (max-width: 1024px)"
144
+ srcset={tabletSrcset as string}
145
+ sizes="(max-width: 1024px) calc(100vw - 48px), 1024px"
146
+ width={tabletW}
147
+ height={tabletH}
148
+ />
149
+ )}
150
+
151
+ {Boolean(desktopSrcset) && (
152
+ <source
153
+ srcset={desktopSrcset as string}
154
+ sizes={sizes}
155
+ width={baseWidth}
156
+ height={baseHeight}
157
+ />
158
+ )}
159
+
160
+ <img
161
+ src={fallbackUrl}
162
+ alt={targetImage.alt || targetImage.filename || ""}
163
+ width={baseWidth}
164
+ height={baseHeight}
165
+ loading={loading}
166
+ fetchpriority={fetchpriority}
167
+ decoding={loading === "eager" ? "sync" : "async"}
168
+ class={className}
169
+ />
170
+ </picture>
171
+ ))
172
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ // src/layouts/SkeletonLayout.astro
3
+
4
+ import MetaHead from "@/components/global/MetaHead.astro";
5
+
6
+ interface Props {
7
+ node: Record<string, any>;
8
+ content: Record<string, any>;
9
+ normalizedSlug: string;
10
+ isHomePage: boolean;
11
+ // 🎯 1. Added language to Props
12
+ language?: string;
13
+ }
14
+
15
+ // 🎯 2. Destructure language
16
+ const {
17
+ node = {},
18
+ content = {},
19
+ normalizedSlug,
20
+ isHomePage,
21
+ language,
22
+ } = Astro.props;
23
+
24
+ const customCss = node.css || "";
25
+ const customJsHeader = node.jsHeader || "";
26
+ const customJsFooter = node.jsFooter || "";
27
+
28
+ // Now this works without crashing!
29
+ const pageLang = language || node?.language || "en";
30
+ ---
31
+
32
+ <!doctype html>
33
+ <html lang={pageLang}>
34
+ <head>
35
+ <MetaHead
36
+ node={node}
37
+ content={content}
38
+ normalizedSlug={normalizedSlug}
39
+ isHomePage={isHomePage}
40
+ />
41
+
42
+ <!-- 🎯 3. Added is:inline so Astro actually renders your database CSS -->
43
+ {customCss && <style is:inline set:html={customCss} />}
44
+
45
+ <!-- 🎯 4. Added is:inline so Astro executes your header JS -->
46
+ {customJsHeader && <script is:inline set:html={customJsHeader} />}
47
+ </head>
48
+
49
+ <body>
50
+ <!-- Renders your Blocks (like the HTML Block) natively -->
51
+ <slot />
52
+
53
+ <!-- 🎯 5. Added is:inline to footer JS -->
54
+ {customJsFooter && <script is:inline set:html={customJsFooter} />}
55
+ </body>
56
+ </html>
@@ -0,0 +1,62 @@
1
+ ---
2
+ // src/layouts/StandardLayout.astro
3
+
4
+ import "@/styles/standard-theme.css";
5
+ import Header from "@/components/global/Header.astro";
6
+ import Footer from "@/components/global/Footer.astro";
7
+ import MetaHead from "@/components/global/MetaHead.astro";
8
+ import type { TreeNode, ContentBlock } from "@/lib/cms";
9
+
10
+ interface Props {
11
+ node: Record<string, any>;
12
+ content: Record<string, ContentBlock[]>;
13
+ normalizedSlug: string;
14
+ isHomePage: boolean;
15
+ hasSidebar?: boolean;
16
+ headerBlocks?: ContentBlock[];
17
+ tree?: TreeNode[];
18
+ language?: string;
19
+ }
20
+
21
+ const {
22
+ node = {},
23
+ content = {},
24
+ normalizedSlug,
25
+ isHomePage,
26
+ headerBlocks = [],
27
+ tree,
28
+ language,
29
+ } = Astro.props;
30
+
31
+ // 🎯 Language Cascade: explicit prop -> node language -> fallback undefined
32
+ // (MetaHead will resolve channel.defaultLanguage if this remains undefined)
33
+ const pageLang = language || node?.language || undefined;
34
+
35
+ // ❌ Removed: const footerBlocks = content?.footer || [];
36
+ // (The new Footer component handles its own data now!)
37
+ ---
38
+
39
+ <!doctype html>
40
+ <html lang={pageLang}>
41
+ <head>
42
+ <MetaHead
43
+ node={node}
44
+ content={content}
45
+ normalizedSlug={normalizedSlug}
46
+ isHomePage={isHomePage}
47
+ headerBlocks={headerBlocks}
48
+ resolvedLanguage={pageLang}
49
+ />
50
+ </head>
51
+ <body>
52
+ <Header node={node} tree={tree} />
53
+
54
+ <main class="content-block">
55
+ <div class="content-container">
56
+ <slot />
57
+ </div>
58
+ </main>
59
+
60
+ <Footer />
61
+ </body>
62
+ </html>