@brightsideoy/kama 0.1.0-alpha.113 → 0.1.0-alpha.115

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 (60) hide show
  1. package/bin/cli.js +172 -379
  2. package/content/definitions/blocks/button.yaml +7 -8
  3. package/content/definitions/blocks/header.yaml +7 -9
  4. package/content/definitions/blocks/header_image.yaml +14 -15
  5. package/content/definitions/blocks/hero.yaml +22 -21
  6. package/content/definitions/blocks/image_card.yaml +46 -47
  7. package/content/definitions/blocks/single_image.yaml +19 -20
  8. package/content/definitions/blocks/text.yaml +8 -9
  9. package/content/definitions/channels/ecommerce.yaml +14 -16
  10. package/content/definitions/channels/website.yaml +20 -21
  11. package/content/definitions/field-sets/access.yaml +3 -5
  12. package/content/definitions/field-sets/channel-base.yaml +25 -25
  13. package/content/definitions/field-sets/content-block.yaml +36 -41
  14. package/content/definitions/field-sets/page-base.yaml +40 -45
  15. package/content/definitions/field-sets/page-meta.yaml +1 -4
  16. package/content/definitions/field-sets/page-social.yaml +6 -8
  17. package/content/definitions/field-sets/tree-base.yaml +25 -25
  18. package/content/definitions/nodes/address.yaml +29 -21
  19. package/content/definitions/nodes/article.yaml +24 -19
  20. package/content/definitions/nodes/event.yaml +32 -26
  21. package/content/definitions/nodes/external-link.yaml +20 -17
  22. package/content/definitions/nodes/page.yaml +38 -35
  23. package/content/definitions/nodes/person.yaml +29 -27
  24. package/content/definitions/nodes/product.yaml +52 -49
  25. package/content/definitions/trees/navigation.yaml +26 -21
  26. package/content/definitions/trees/social-links.yaml +5 -2
  27. package/dist/assets/index-8T39DEP3.js +162 -0
  28. package/dist/assets/index-B9oh2_TL.css +1 -0
  29. package/dist/index.html +2 -2
  30. package/dist/index.js +6492 -4249
  31. package/package.json +5 -6
  32. package/schema.sql +5 -13
  33. package/templates/astro-starter/astro.config.mjs +40 -0
  34. package/templates/astro-starter/package.json.template +20 -0
  35. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +125 -0
  36. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
  37. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
  38. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  39. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
  40. package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
  41. package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
  42. package/templates/astro-starter/src/components/global/Footer.astro +48 -0
  43. package/templates/astro-starter/src/components/global/Header.astro +92 -0
  44. package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
  45. package/templates/astro-starter/src/components/ui/KamaImage.astro +148 -0
  46. package/templates/astro-starter/src/kama-image-service.ts +41 -0
  47. package/templates/astro-starter/src/layouts/Layout.astro +280 -0
  48. package/templates/astro-starter/src/lib/cms.ts +149 -0
  49. package/templates/astro-starter/src/lib/media-url.ts +34 -0
  50. package/templates/astro-starter/src/middleware.ts +306 -0
  51. package/templates/astro-starter/src/pages/404.astro +46 -0
  52. package/templates/astro-starter/src/pages/[...slug].astro +234 -0
  53. package/templates/astro-starter/src/pages/media/[...path].ts +113 -0
  54. package/templates/astro-starter/tsconfig.json +0 -0
  55. package/test-local.sh +76 -0
  56. package/dist/assets/index-10GEw0KY.css +0 -1
  57. package/dist/assets/index-THJ6BqPP.js +0 -1394
  58. package/kama-worker/bundled-definitions.js +0 -2394
  59. package/kama-worker/bundled-definitions.ts +0 -2395
  60. package/scripts/bundle-definitions.js +0 -224
@@ -0,0 +1,280 @@
1
+ ---
2
+ // src/layouts/Layout.astro
3
+ import Header from "@/components/global/Header.astro";
4
+ import Footer from "@/components/global/Footer.astro";
5
+ import type { TreeNode, ContentBlock } from "@/lib/cms";
6
+ // 🎯 FIX: Import from pure TypeScript utility, NOT from KamaImage.astro
7
+ import { buildAgnosticImageUrl } from "@/lib/media-url";
8
+
9
+ interface Props {
10
+ node: Record<string, any>;
11
+ content: Record<string, ContentBlock[]>;
12
+ normalizedSlug: string;
13
+ isHomePage: boolean;
14
+ hasSidebar?: boolean;
15
+ headerBlocks?: ContentBlock[];
16
+ tree?: TreeNode[];
17
+ }
18
+
19
+ const {
20
+ node = {},
21
+ content = {},
22
+ normalizedSlug,
23
+ isHomePage,
24
+ headerBlocks = [],
25
+ tree
26
+ } = Astro.props;
27
+
28
+ const footerBlocks = content?.footer || [];
29
+
30
+ // 🎯 Detect LCP image candidate to preload immediately in <head>
31
+ let lcpImageUrl = "";
32
+ const firstHeaderBlock = headerBlocks && headerBlocks[0];
33
+ const firstContentBlock = content?.content && content.content[0];
34
+
35
+ const candidateBlock = (firstHeaderBlock?.data?.image ? firstHeaderBlock : null)
36
+ || (firstContentBlock?.data?.image ? firstContentBlock : null);
37
+
38
+ if (candidateBlock?.data?.image) {
39
+ const candidateImg = candidateBlock.data.image;
40
+ const rawUrl = candidateImg.variants?.standard?.url || candidateImg.url || candidateImg.path;
41
+ if (rawUrl) {
42
+ lcpImageUrl = buildAgnosticImageUrl(rawUrl, 1352, 75);
43
+ }
44
+ }
45
+
46
+ const siteOrigin = import.meta.env.PUBLIC_SITE_URL || Astro.site?.origin || "https://example.com";
47
+ const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
48
+ const agencyName = import.meta.env.PUBLIC_AGENCY_NAME || "My Company Name";
49
+ const brandLogo = `${siteOrigin}/logo.png`;
50
+
51
+ const prefix = node?.titlePrefix?.trim() ? `${node.titlePrefix.trim()} ` : "";
52
+ const suffix = node?.titleSuffix?.trim() ? ` ${node.titleSuffix.trim()}` : "";
53
+ const baseTitle = node?.title?.trim() || brandName;
54
+ const fullTitle = `${prefix}${baseTitle}${suffix}`;
55
+
56
+ const canonicalURL = new URL(Astro.url.pathname, siteOrigin).href;
57
+
58
+ const breadcrumbListItems = [
59
+ {
60
+ "@type": "ListItem",
61
+ "position": 1,
62
+ "name": "Home",
63
+ "item": siteOrigin,
64
+ },
65
+ ];
66
+
67
+ if (!isHomePage && normalizedSlug) {
68
+ const segments = normalizedSlug.split("/").filter(Boolean);
69
+ let currentPath = "";
70
+
71
+ segments.forEach((segment, index) => {
72
+ currentPath += `/${segment}`;
73
+ const isLast = index === segments.length - 1;
74
+
75
+ const fallbackTitle = segment
76
+ .replace(/-/g, " ")
77
+ .replace(/\b\w/g, (char) => char.toUpperCase());
78
+
79
+ const segmentTitle = isLast && node?.title ? node.title : fallbackTitle;
80
+
81
+ breadcrumbListItems.push({
82
+ "@type": "ListItem",
83
+ "position": index + 2,
84
+ "name": segmentTitle,
85
+ "item": `${siteOrigin}${currentPath}`,
86
+ });
87
+ });
88
+ }
89
+
90
+ const structuredDataGraph = {
91
+ "@context": "https://schema.org",
92
+ "@graph": [
93
+ {
94
+ "@type": "Organization",
95
+ "@id": `${siteOrigin}/#organization`,
96
+ "name": agencyName,
97
+ "url": siteOrigin,
98
+ "logo": {
99
+ "@type": "ImageObject",
100
+ "url": brandLogo,
101
+ "caption": agencyName
102
+ }
103
+ },
104
+ {
105
+ "@type": "SoftwareApplication",
106
+ "@id": `${siteOrigin}/#application`,
107
+ "name": brandName,
108
+ "applicationCategory": "WebApplication",
109
+ "operatingSystem": "Cloudflare Workers",
110
+ "publisher": {
111
+ "@id": `${siteOrigin}/#organization`
112
+ },
113
+ "description": "Corporate Web Management built with KAMA Engine, Astro, and Cloudflare Edge infrastructure."
114
+ },
115
+ {
116
+ "@type": "WebSite",
117
+ "@id": `${siteOrigin}/#website`,
118
+ "url": siteOrigin,
119
+ "name": brandName,
120
+ "publisher": {
121
+ "@id": `${siteOrigin}/#organization`
122
+ },
123
+ "potentialAction": {
124
+ "@type": "SearchAction",
125
+ "target": {
126
+ "@type": "EntryPoint",
127
+ "urlTemplate": `${siteOrigin}/search?q={search_term_string}`
128
+ },
129
+ "query-input": "required name=search_term_string"
130
+ }
131
+ },
132
+ {
133
+ "@type": "BreadcrumbList",
134
+ "@id": `${siteOrigin}${normalizedSlug ? `/${normalizedSlug}` : ''}/#breadcrumb`,
135
+ "itemListElement": breadcrumbListItems
136
+ }
137
+ ]
138
+ };
139
+ ---
140
+
141
+ <html lang="en">
142
+ <head>
143
+ <meta charset="utf-8" />
144
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
145
+
146
+ <title>{fullTitle}</title>
147
+
148
+ <meta
149
+ name="description"
150
+ content={node?.summary || `${brandName} - Corporate Web Application`}
151
+ />
152
+
153
+ <link rel="canonical" href={canonicalURL} />
154
+
155
+ {lcpImageUrl && (
156
+ <link
157
+ rel="preload"
158
+ as="image"
159
+ href={lcpImageUrl}
160
+ fetchpriority="high"
161
+ />
162
+ )}
163
+
164
+ <script
165
+ type="application/ld+json"
166
+ is:inline
167
+ set:html={JSON.stringify(structuredDataGraph)}
168
+ />
169
+ </head>
170
+ <body>
171
+ <Header node={node} tree={tree} />
172
+
173
+ <main class="content-block">
174
+ <div class="content-container">
175
+ <slot />
176
+ </div>
177
+ </main>
178
+
179
+ <Footer blocks={footerBlocks} contentMap={content} />
180
+ </body>
181
+ </html>
182
+
183
+ <style is:global>
184
+ *,
185
+ *::before,
186
+ *::after {
187
+ box-sizing: border-box;
188
+ border-radius: 0 !important;
189
+ }
190
+
191
+ html {
192
+ font-size: 16px;
193
+ -webkit-text-size-adjust: 100%;
194
+ -webkit-font-smoothing: antialiased;
195
+ -moz-osx-font-smoothing: grayscale;
196
+ text-rendering: optimizeLegibility;
197
+ }
198
+
199
+ body {
200
+ margin: 0;
201
+ padding: 0;
202
+ font-family:
203
+ system-ui,
204
+ -apple-system,
205
+ BlinkMacSystemFont,
206
+ "Segoe UI",
207
+ Roboto,
208
+ Helvetica,
209
+ Arial,
210
+ sans-serif;
211
+ font-display: swap;
212
+ background-color: #ffffff;
213
+ color: #1e293b;
214
+ min-height: 100vh;
215
+ display: flex;
216
+ flex-direction: column;
217
+ line-height: 1.65;
218
+ letter-spacing: -0.011em;
219
+ }
220
+
221
+ h1,
222
+ h2,
223
+ h3,
224
+ h4,
225
+ h5,
226
+ h6 {
227
+ color: #0f172a;
228
+ font-weight: 700;
229
+ line-height: 1.25;
230
+ letter-spacing: -0.022em;
231
+ margin-top: 1.6em;
232
+ margin-bottom: 0.6em;
233
+ }
234
+
235
+ h1 {
236
+ font-size: 2.25rem;
237
+ }
238
+ h2 {
239
+ font-size: 1.65rem;
240
+ }
241
+ h3 {
242
+ font-size: 1.35rem;
243
+ }
244
+
245
+ p {
246
+ margin-top: 0;
247
+ margin-bottom: 1.25em;
248
+ color: #334155;
249
+ font-size: 1.03rem;
250
+ }
251
+
252
+ a {
253
+ color: #0284c7;
254
+ text-decoration-thickness: 1px;
255
+ text-underline-offset: 3px;
256
+ transition: color 0.15s ease;
257
+ }
258
+
259
+ a:hover {
260
+ color: #0369a1;
261
+ }
262
+ li>p:last-child {
263
+ margin-bottom: 0;
264
+ }
265
+ </style>
266
+
267
+ <style>
268
+ .content-block {
269
+ flex: 1;
270
+ padding: 40px 0 64px 0;
271
+ }
272
+
273
+ .content-container {
274
+ max-width: 1400px;
275
+ margin: 0 auto;
276
+ padding: 0 24px;
277
+ width: 100%;
278
+ box-sizing: border-box;
279
+ }
280
+ </style>
@@ -0,0 +1,149 @@
1
+ // src/lib/cms.ts
2
+
3
+ export interface TreeNode {
4
+ id: number;
5
+ treeNodeId: number;
6
+ parentId: number | null;
7
+ title: string;
8
+ slug: string;
9
+ path: string;
10
+ nodeType: string;
11
+ hidden?: number;
12
+ summary?: string;
13
+ url?: string;
14
+ target?: string;
15
+ children?: TreeNode[];
16
+ }
17
+
18
+ export interface ContentBlock {
19
+ instanceId: number;
20
+ componentId: string;
21
+ data: Record<string, any>;
22
+ }
23
+
24
+ export interface RegionMeta {
25
+ isInherited: boolean;
26
+ inheritedFromId?: string | number;
27
+ inheritedFromTitle?: string;
28
+ }
29
+
30
+ export interface NodeResponse {
31
+ channelId: string | number;
32
+ treeId: string | number;
33
+ language: string;
34
+ node: {
35
+ id: number;
36
+ title: string;
37
+ slug: string;
38
+ nodeType: string;
39
+ path?: string;
40
+ createdAt?: string;
41
+ updatedAt?: string;
42
+ summary?: string;
43
+ children?: any[];
44
+ [key: string]: any;
45
+ };
46
+ content: Record<string, ContentBlock[]>;
47
+ regionMeta?: Record<string, RegionMeta>;
48
+ }
49
+
50
+ /**
51
+ * Resolves base CMS URL across runtime environments safely via Astro
52
+ */
53
+ export function getCmsUrl(): string {
54
+ const url =
55
+ import.meta.env.PUBLIC_API_URL ||
56
+ import.meta.env.PUBLIC_CMS_URL ||
57
+ 'http://localhost:8787';
58
+ return url.replace(/\/+$/, '');
59
+ }
60
+
61
+ /**
62
+ * Resolves Default Channel ID across environments
63
+ */
64
+ export function getChannelId(): string {
65
+ return import.meta.env.PUBLIC_CHANNEL_ID || 'website';
66
+ }
67
+
68
+ /**
69
+ * Resolves Default Tree ID across environments
70
+ */
71
+ export function getTreeId(): string {
72
+ return import.meta.env.PUBLIC_TREE_ID || 'navigation';
73
+ }
74
+
75
+ /**
76
+ * Fetch tree navigation hierarchy by tree key/ID and channel.
77
+ * Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
78
+ */
79
+ export async function getTree(
80
+ treeKeyOrId: string = getTreeId(),
81
+ channel: string | number = getChannelId(),
82
+ customFetch: typeof fetch = fetch
83
+ ): Promise<TreeNode[]> {
84
+ const baseUrl = getCmsUrl();
85
+ const requestUrl = `${baseUrl}/api/v1/tree/${treeKeyOrId}?channelId=${channel}`;
86
+
87
+ try {
88
+ const res = await customFetch(requestUrl, {
89
+ headers: {
90
+ Accept: 'application/json',
91
+ },
92
+ });
93
+
94
+ if (!res.ok) return [];
95
+
96
+ const data = await res.json();
97
+ return data.items || [];
98
+ } catch (err) {
99
+ console.error('[CMS Client] Failed to fetch public tree:', err);
100
+ return [];
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Fetch single content node payload by its full URL path.
106
+ * Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
107
+ */
108
+ export async function getNodeByPath(
109
+ path: string,
110
+ channel: string | number = getChannelId(),
111
+ preview = false,
112
+ customFetch: typeof fetch = fetch
113
+ ): Promise<NodeResponse | null> {
114
+ const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'home';
115
+ const baseUrl = getCmsUrl();
116
+ const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
117
+
118
+ try {
119
+ const res = await customFetch(requestUrl, {
120
+ headers: {
121
+ Accept: 'application/json',
122
+ },
123
+ });
124
+
125
+ if (!res.ok) return null;
126
+
127
+ return (await res.json()) as NodeResponse;
128
+ } catch (err) {
129
+ console.error('[CMS Client] Failed to fetch node by path:', err);
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Resolves relative media URLs against base CMS URL
136
+ */
137
+ export function getMediaUrl(url?: string): string {
138
+ if (!url) return '';
139
+
140
+ const baseUrl = getCmsUrl();
141
+ let cleanUrl = url.replace(/^http:\/\/localhost:\d+/, baseUrl);
142
+
143
+ if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) {
144
+ return cleanUrl;
145
+ }
146
+
147
+ const cleanPath = cleanUrl.replace(/^\/+/, '');
148
+ return `${baseUrl}/${cleanPath}`;
149
+ }
@@ -0,0 +1,34 @@
1
+ // src/lib/media-url.ts
2
+
3
+ export function toRelativeMediaPath(rawUrl: string): string {
4
+ if (!rawUrl) return "";
5
+
6
+ let clean = String(rawUrl);
7
+ clean = clean.replace(/^https?:\/\/[^/]+/, "");
8
+ clean = clean.split("?")[0].split("#")[0];
9
+
10
+ const mediaIndex = clean.indexOf("/media/");
11
+ if (mediaIndex !== -1) {
12
+ return `/media/${clean
13
+ .slice(mediaIndex + "/media/".length)
14
+ .replace(/^\/+/, "")}`;
15
+ }
16
+
17
+ return clean.startsWith("/") ? clean : `/${clean}`;
18
+ }
19
+
20
+ export function buildAgnosticImageUrl(
21
+ url: string,
22
+ width: number,
23
+ quality = 75,
24
+ ): string {
25
+ const cleanPath = toRelativeMediaPath(url);
26
+ if (!cleanPath) return "";
27
+
28
+ const urlObj = new URL(cleanPath, "http://localhost");
29
+ urlObj.searchParams.set("w", String(width));
30
+ urlObj.searchParams.set("q", String(quality));
31
+ urlObj.searchParams.set("f", "auto");
32
+
33
+ return urlObj.pathname + urlObj.search;
34
+ }