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

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 (101) hide show
  1. package/bin/cli.js +604 -364
  2. package/content/definitions/block-kama-forms.yaml +5 -0
  3. package/content/definitions/blocks/button.yaml +15 -5
  4. package/content/definitions/blocks/image-gallery.yaml +34 -0
  5. package/content/definitions/blocks/node-list.yaml +75 -0
  6. package/content/definitions/blocks/youtube.yaml +8 -0
  7. package/content/definitions/field-sets/access.yaml +10 -0
  8. package/content/definitions/field-sets/channel-base.yaml +7 -0
  9. package/content/definitions/field-sets/page-base.yaml +16 -1
  10. package/content/definitions/nodes/article.yaml +17 -1
  11. package/content/definitions/taxonomies/categories.yaml +5 -2
  12. package/content/definitions/taxonomies/media-tag.yaml +15 -0
  13. package/content/definitions/taxonomies/tags.yaml +5 -2
  14. package/content/forms/contact.yaml +44 -0
  15. package/content/forms/subscription.yaml +32 -0
  16. package/definitions/editor/default.ts +15 -1
  17. package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
  18. package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
  19. package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
  20. package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
  21. package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
  22. package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
  23. package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
  24. package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
  25. package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
  26. package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
  27. package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
  28. package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
  29. package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
  30. package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
  31. package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
  32. package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
  33. package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
  34. package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
  35. package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
  36. package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
  37. package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
  38. package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
  39. package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
  40. package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
  41. package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
  42. package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
  43. package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
  44. package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
  45. package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
  46. package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
  47. package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
  48. package/dist/assets/circle-check-A-QeFqt3.js +1 -0
  49. package/dist/assets/database-hg5SqKLG.js +1 -0
  50. package/dist/assets/folder-tree-BWYot72H.js +1 -0
  51. package/dist/assets/globe-BraoljaN.js +1 -0
  52. package/dist/assets/history-pqIbKK9u.js +1 -0
  53. package/dist/assets/index-CY0VYSyt.css +1 -0
  54. package/dist/assets/index-DdG9Pozx.js +14 -0
  55. package/dist/assets/layers-B6RgfpPR.js +1 -0
  56. package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
  57. package/dist/assets/monitor-CrpvInqG.js +1 -0
  58. package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
  59. package/dist/assets/plus-CX_GHzXs.js +1 -0
  60. package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
  61. package/dist/assets/save-Ctv8vW9H.js +1 -0
  62. package/dist/assets/schema-helpers-DvficKko.js +1 -0
  63. package/dist/assets/send-F-mJeL54.js +1 -0
  64. package/dist/assets/smartphone-EPce6VdP.js +1 -0
  65. package/dist/assets/trash-2-CAUF1KtI.js +1 -0
  66. package/dist/assets/useLocalStorage-C805uplI.js +1 -0
  67. package/dist/assets/useNodes-CqFlc72m.js +1 -0
  68. package/dist/assets/user-IFlT6mYq.js +1 -0
  69. package/dist/build-seed.js +495 -0
  70. package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
  71. package/dist/index.html +34 -2
  72. package/dist/index.js +3116 -1315
  73. package/dist/schema-compiler.js +5 -5
  74. package/documentation/EXTENSIONS.md +204 -0
  75. package/extension-registry.js +5 -0
  76. package/package.json +13 -6
  77. package/schema.sql +44 -1
  78. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
  79. package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
  80. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
  81. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
  82. package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
  83. package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
  84. package/templates/astro-starter/src/components/global/Footer.astro +7 -46
  85. package/templates/astro-starter/src/components/global/Header.astro +10 -16
  86. package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
  87. package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
  88. package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
  89. package/templates/astro-starter/src/lib/cms.ts +33 -11
  90. package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
  91. package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
  92. package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
  93. package/templates/astro-starter/src/middleware.ts +158 -100
  94. package/templates/astro-starter/src/pages/[...slug].astro +77 -3
  95. package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
  96. package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
  97. package/templates/kama-worker/index.js +38 -15
  98. package/templates/kama-worker/server.js +13 -5
  99. package/templates/kama-worker/setup-local-db.mjs +17 -0
  100. package/dist/assets/index-F2rfUymo.css +0 -1
  101. package/dist/assets/index-lXxH3VFK.js +0 -164
@@ -20,7 +20,7 @@ interface Props {
20
20
  const {
21
21
  image = {},
22
22
  sizes = "100vw",
23
- desktopWidths = [768, 1352],
23
+ desktopWidths = [768, 1352, 1920, 2560],
24
24
  idealWidth = 1352,
25
25
  class: className = "",
26
26
  loading = "lazy",
@@ -90,18 +90,26 @@ function buildAgnosticSrcset(
90
90
  .join(", ");
91
91
  }
92
92
 
93
- // Generate srcsets only for raster images
93
+ // 🎯 High-DPI / Retina Optimization:
94
+ // - Mobile: Includes 320, 640, 768 (covers 2x and 3x Retina mobile displays)
95
+ // - Tablet: Includes 768, 1024, 1352 (covers iPad Pro Retina density)
96
+ // - Desktop: Uses desktopWidths (defaults to 768, 1352, 1920, 2560 for 1440p and 4K)
94
97
  const mobileSrcset =
95
- !isSvg && rawMobileUrl ? buildAgnosticSrcset(rawMobileUrl, [400, 768]) : null;
98
+ !isSvg && rawMobileUrl
99
+ ? buildAgnosticSrcset(rawMobileUrl, [320, 640, 768])
100
+ : null;
96
101
 
97
102
  const tabletSrcset =
98
103
  !isSvg && rawTabletUrl
99
- ? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
104
+ ? buildAgnosticSrcset(rawTabletUrl, [768, 1024, 1352])
100
105
  : null;
101
106
 
102
107
  const desktopSrcset =
103
108
  !isSvg && rawDesktopUrl
104
- ? buildAgnosticSrcset(rawDesktopUrl, desktopWidths)
109
+ ? buildAgnosticSrcset(
110
+ rawDesktopUrl,
111
+ desktopWidths.length ? desktopWidths : [768, 1352, 1920, 2560],
112
+ )
105
113
  : null;
106
114
 
107
115
  const fallbackUrl =
@@ -32,8 +32,8 @@ const {
32
32
  // (MetaHead will resolve channel.defaultLanguage if this remains undefined)
33
33
  const pageLang = language || node?.language || undefined;
34
34
 
35
- // ❌ Removed: const footerBlocks = content?.footer || [];
36
- // (The new Footer component handles its own data now!)
35
+ // 🎯 FIX: Extract the footer blocks from the payload again
36
+ const footerBlocks = content?.footer || [];
37
37
  ---
38
38
 
39
39
  <!doctype html>
@@ -57,6 +57,7 @@ const pageLang = language || node?.language || undefined;
57
57
  </div>
58
58
  </main>
59
59
 
60
- <Footer />
60
+ {/* 🎯 FIX: Pass the blocks and contentMap into the Footer */}
61
+ <Footer blocks={footerBlocks} contentMap={content} />
61
62
  </body>
62
63
  </html>
@@ -93,6 +93,14 @@ export function getTreeId(): string {
93
93
  return import.meta.env.PUBLIC_TREE_ID || 'navigation';
94
94
  }
95
95
 
96
+ // 🎯 Safely resolve global authorization header for default CMS fetches
97
+ function getAuthHeaders(): Record<string, string> {
98
+ const secret = import.meta.env.FRONTEND_API_SECRET;
99
+ const headers: Record<string, string> = { Accept: 'application/json' };
100
+ if (secret) headers['Authorization'] = `Bearer ${secret}`;
101
+ return headers;
102
+ }
103
+
96
104
  /**
97
105
  * Fetch channel metadata by key/ID.
98
106
  * Serves global site branding, logos, favicons, and schema-defined public fields.
@@ -106,13 +114,17 @@ export async function getChannel(
106
114
 
107
115
  try {
108
116
  const res = await customFetch(requestUrl, {
109
- headers: {
110
- Accept: 'application/json',
111
- },
117
+ headers: getAuthHeaders(),
112
118
  });
113
119
 
114
120
  if (!res.ok) return null;
115
121
 
122
+ // 🎯 Defensive HTML check
123
+ if (res.headers.get("content-type")?.includes("text/html")) {
124
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Channel: ${requestUrl}`);
125
+ return null;
126
+ }
127
+
116
128
  return (await res.json()) as ChannelResponse;
117
129
  } catch (err) {
118
130
  console.error('[CMS Client] Failed to fetch public channel:', err);
@@ -134,13 +146,17 @@ export async function getTree(
134
146
 
135
147
  try {
136
148
  const res = await customFetch(requestUrl, {
137
- headers: {
138
- Accept: 'application/json',
139
- },
149
+ headers: getAuthHeaders(),
140
150
  });
141
151
 
142
152
  if (!res.ok) return [];
143
153
 
154
+ // 🎯 Defensive HTML check
155
+ if (res.headers.get("content-type")?.includes("text/html")) {
156
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Tree: ${requestUrl}`);
157
+ return [];
158
+ }
159
+
144
160
  const data = await res.json();
145
161
  return data.items || [];
146
162
  } catch (err) {
@@ -159,22 +175,28 @@ export async function getNodeByPath(
159
175
  preview = false,
160
176
  customFetch: typeof fetch = fetch
161
177
  ): Promise<NodeResponse | null> {
162
- const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'home';
178
+ // 🎯 We keep 'index' as the fallback string for Hono routing compatibility,
179
+ // because the backend dynamically intercepts 'index' and returns the channelRoot!
180
+ const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'index';
163
181
  const baseUrl = getCmsUrl();
164
182
  const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
165
183
 
166
184
  try {
167
185
  const res = await customFetch(requestUrl, {
168
- headers: {
169
- Accept: 'application/json',
170
- },
186
+ headers: getAuthHeaders(),
171
187
  });
172
188
 
173
189
  if (!res.ok) return null;
174
190
 
191
+ // 🎯 Defensive HTML check (Prevents the dreaded "<!DOCTYPE html" JSON parse crash)
192
+ if (res.headers.get("content-type")?.includes("text/html")) {
193
+ console.error(`❌ [CMS Client] API returned HTML instead of JSON for Node: ${requestUrl}`);
194
+ return null;
195
+ }
196
+
175
197
  return (await res.json()) as NodeResponse;
176
198
  } catch (err) {
177
- console.error('[CMS Client] Failed to fetch node by path:', err);
199
+ console.error(`[CMS Client] Failed to fetch node by path (${cleanPath}):`, err);
178
200
  return null;
179
201
  }
180
202
  }
@@ -28,10 +28,21 @@ const kamaImageService: LocalImageService = {
28
28
 
29
29
  const url = new URL(src, 'http://localhost');
30
30
 
31
- if (options.width) url.searchParams.set('w', Math.round(options.width).toString());
32
- if (options.height) url.searchParams.set('h', Math.round(options.height).toString());
33
- if (options.quality) url.searchParams.set('q', options.quality.toString());
34
- if (options.format) url.searchParams.set('f', options.format.toString());
31
+ // 🎯 Pass width if defined (snapped on backend by assets.ts)
32
+ if (options.width && !isNaN(options.width)) {
33
+ url.searchParams.set('w', Math.round(options.width).toString());
34
+ }
35
+
36
+ // 🎯 Pass height only if explicitly provided to maintain intrinsic aspect ratios
37
+ if (options.height && !isNaN(options.height)) {
38
+ url.searchParams.set('h', Math.round(options.height).toString());
39
+ }
40
+
41
+ // 🎯 Default quality to 75 if unsupplied
42
+ url.searchParams.set('q', (options.quality || 75).toString());
43
+
44
+ // 🎯 Default format to 'auto' for backend WebP/AVIF negotiation
45
+ url.searchParams.set('f', options.format ? options.format.toString() : 'auto');
35
46
 
36
47
  return src.startsWith('http') ? url.toString() : (url.pathname + url.search);
37
48
  } catch (e) {
@@ -28,12 +28,31 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
28
28
  // 🎯 Detect if Astro is requesting a resized variant
29
29
  const hasResizeParams = url.searchParams.has('w') || url.searchParams.has('h') || url.searchParams.has('q') || url.searchParams.has('f');
30
30
 
31
+ // 🎯 Resolve Negotiated Format for Edge Cache Key
32
+ const acceptHeader = request.headers.get('Accept') || '';
33
+ const fParam = url.searchParams.get('f');
34
+ let resolvedFormat = fParam && fParam !== 'auto' ? fParam : '';
35
+
36
+ if (!resolvedFormat && hasResizeParams) {
37
+ if (acceptHeader.includes('image/avif')) resolvedFormat = 'avif';
38
+ else if (acceptHeader.includes('image/webp')) resolvedFormat = 'webp';
39
+ }
40
+
41
+ // 🎯 Construct a safe Cache Key that accounts for format negotiation
42
+ let cacheKeyUrl = request.url;
43
+ if (hasResizeParams && resolvedFormat && !fParam) {
44
+ const cacheUrlObj = new URL(cacheKeyUrl);
45
+ cacheUrlObj.searchParams.set('_resolvedFormat', resolvedFormat);
46
+ cacheKeyUrl = cacheUrlObj.toString();
47
+ }
48
+ const cacheKey = new Request(cacheKeyUrl, request);
49
+
31
50
  // 1. Cloudflare Edge Cache API
32
51
  let edgeCache: Cache | null = null;
33
52
  try {
34
53
  if (typeof caches !== "undefined" && caches.default) {
35
54
  edgeCache = caches.default;
36
- const cachedResponse = await edgeCache.match(request);
55
+ const cachedResponse = await edgeCache.match(cacheKey);
37
56
  if (cachedResponse) return cachedResponse;
38
57
  }
39
58
  } catch (err) {
@@ -67,7 +86,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
67
86
  const r2Response = new Response(r2Object.body, { status: 200, headers });
68
87
 
69
88
  if (edgeCache) {
70
- const cachePromise = edgeCache.put(request, r2Response.clone());
89
+ const cachePromise = edgeCache.put(cacheKey, r2Response.clone());
71
90
  if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
72
91
  else cachePromise.catch(e => console.warn(e));
73
92
  }
@@ -112,7 +131,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
112
131
  });
113
132
 
114
133
  if (edgeCache) {
115
- const cachePromise = edgeCache.put(request, proxyResponse.clone());
134
+ const cachePromise = edgeCache.put(cacheKey, proxyResponse.clone());
116
135
  if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
117
136
  else cachePromise.catch(e => console.warn(e));
118
137
  }
@@ -1,11 +1,22 @@
1
1
  // src/lib/media/media-url.ts
2
2
 
3
+ /**
4
+ * Normalizes absolute CMS URLs into clean relative /media/... paths.
5
+ * Ensures CSP compliance by routing requests through the Astro frontend proxy.
6
+ */
3
7
  export function toRelativeMediaPath(rawUrl: string): string {
4
8
  if (!rawUrl) return "";
5
9
 
6
- let clean = String(rawUrl);
7
- clean = clean.replace(/^https?:\/\/[^/]+/, "");
8
- clean = clean.split("?")[0].split("#")[0];
10
+ let clean = String(rawUrl).trim();
11
+
12
+ try {
13
+ // Parse absolute or relative URLs safely using a dummy origin fallback
14
+ const urlObj = new URL(clean, "http://localhost");
15
+ clean = urlObj.pathname;
16
+ } catch {
17
+ // Fallback cleanup for malformed strings
18
+ clean = clean.replace(/^https?:\/\/[^/]+/, "").split("?")[0].split("#")[0];
19
+ }
9
20
 
10
21
  const mediaIndex = clean.indexOf("/media/");
11
22
  if (mediaIndex !== -1) {
@@ -26,6 +37,9 @@ export function isSvgImage(urlOrMime: string): boolean {
26
37
  return lower.endsWith('.svg') || lower.includes('image/svg+xml') || lower.includes('.svg?');
27
38
  }
28
39
 
40
+ /**
41
+ * Constructs an agnostic /media/... URL with standard optimization query parameters.
42
+ */
29
43
  export function buildAgnosticImageUrl(
30
44
  url: string,
31
45
  width: number,