@brightsideoy/kama 0.1.0-alpha.99 → 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-D6qQsWOj.js +0 -1392
  107. package/dist/assets/index-TdW5F0pX.css +0 -1
  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,67 @@
1
+ ---
2
+ // src/components/blocks/ColumnLayout.astro
3
+
4
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
+
6
+ interface Props {
7
+ blockId: string;
8
+ contentMap: Record<string, any[]>;
9
+ }
10
+
11
+ const { blockId, contentMap } = Astro.props;
12
+
13
+ const order = ["left", "middle", "right", "col1", "col2", "col3", "col4"];
14
+
15
+ // 1. Find all sub-regions belonging to this specific layout instance
16
+ const regionKeys = Object.keys(contentMap).filter(
17
+ key => blockId && key.startsWith(`${blockId}_`),
18
+ );
19
+
20
+ // 2. Sort keys logically (left -> middle -> right or col1 -> col2 -> col3)
21
+ const sortedKeys = regionKeys.sort((a, b) => {
22
+ const suffixA = a.replace(`${blockId}_`, "");
23
+ const suffixB = b.replace(`${blockId}_`, "");
24
+ const idxA = order.indexOf(suffixA);
25
+ const idxB = order.indexOf(suffixB);
26
+
27
+ if (idxA === -1 && idxB === -1) return a.localeCompare(b);
28
+ if (idxA === -1) return 1;
29
+ if (idxB === -1) return -1;
30
+ return idxA - idxB;
31
+ });
32
+
33
+ const columnCount = Math.max(sortedKeys.length, 1);
34
+ ---
35
+
36
+ <div class="cms-column-layout" style={`--col-count: ${columnCount}`}>
37
+ {
38
+ sortedKeys.map(key => {
39
+ const columnBlocks = contentMap[key] || [];
40
+ return (
41
+ <div class="column">
42
+ <!-- 🎯 Passes region={key} down to BlockRenderer -->
43
+ <BlockRenderer
44
+ blocks={columnBlocks}
45
+ contentMap={contentMap}
46
+ region={key}
47
+ />
48
+ </div>
49
+ );
50
+ })
51
+ }
52
+ </div>
53
+
54
+ <style>
55
+ .cms-column-layout {
56
+ display: grid;
57
+ gap: 2rem;
58
+ margin-bottom: 1.5rem;
59
+ grid-template-columns: 1fr;
60
+ }
61
+
62
+ @media (min-width: 768px) {
63
+ .cms-column-layout {
64
+ grid-template-columns: repeat(var(--col-count, 1), 1fr);
65
+ }
66
+ }
67
+ </style>
@@ -0,0 +1,132 @@
1
+ ---
2
+ // src/components/blocks/HeaderImage.astro
3
+
4
+ import KamaImage from "@/components/ui/KamaImage.astro";
5
+
6
+ interface Props {
7
+ data: {
8
+ image?: any;
9
+ image_meta?: any;
10
+ width?: number | string;
11
+ height?: number | string;
12
+ };
13
+ region?: string;
14
+ isFirst?: boolean;
15
+ }
16
+
17
+ const { data, region = "main", isFirst = false } = Astro.props;
18
+
19
+ const img = data?.image;
20
+
21
+ if (!img) {
22
+ return null;
23
+ }
24
+
25
+ /**
26
+ * Determine if this image is likely to be the Largest Contentful Paint.
27
+ */
28
+ const isLcp = region === "header" || (region === "main" && isFirst);
29
+
30
+ /**
31
+ * Determine grid column count from the region name.
32
+ */
33
+ let columnCount = 1;
34
+
35
+ if (region.includes("_")) {
36
+ columnCount = 2;
37
+
38
+ if (region.includes("_middle") || region.includes("_col3")) {
39
+ columnCount = 3;
40
+ }
41
+
42
+ if (region.includes("_col4")) {
43
+ columnCount = 4;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Default single-column layout.
49
+ */
50
+ let maxWidth = "1352";
51
+ let targetColumnWidth = 1352;
52
+
53
+ // 1-col: Base mobile (~400), 2x mobile/tablet (~800), 1x desktop (1352), 2x desktop (2704)
54
+ let optimizedWidths = [400, 800, 1352, 2048, 2704];
55
+ let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
56
+
57
+ /**
58
+ * Two-column layout.
59
+ */
60
+ if (columnCount === 2) {
61
+ maxWidth = "660";
62
+ targetColumnWidth = 660;
63
+
64
+ // 2-col: 1x desktop (660), 2x mobile stacked (~800), 2x desktop (1320)
65
+ optimizedWidths = [400, 660, 800, 1320];
66
+ sizes =
67
+ "(max-width: 767px) calc(100vw - 48px), " +
68
+ "(max-width: 1399px) calc((100vw - 80px) / 2), " +
69
+ "660px";
70
+ }
71
+
72
+ /**
73
+ * Three-column layout.
74
+ */
75
+ else if (columnCount === 3) {
76
+ maxWidth = "429";
77
+ targetColumnWidth = 429;
78
+
79
+ // 3-col: 1x desktop (429), 2x desktop/tablet (~858), 2x/3x mobile stacked (~1000)
80
+ optimizedWidths = [300, 429, 858, 1024];
81
+ sizes =
82
+ "(max-width: 767px) calc(100vw - 48px), " +
83
+ "(max-width: 1399px) calc((100vw - 112px) / 3), " +
84
+ "429px";
85
+ }
86
+
87
+ /**
88
+ * Four-column layout.
89
+ */
90
+ else if (columnCount === 4) {
91
+ maxWidth = "314";
92
+ targetColumnWidth = 314;
93
+
94
+ // 4-col: 1x desktop (314), 2x desktop (628), 2x/3x mobile stacked (~1000)
95
+ optimizedWidths = [264, 314, 628, 1024];
96
+ sizes =
97
+ "(max-width: 767px) calc(100vw - 48px), " +
98
+ "(max-width: 1399px) calc((100vw - 144px) / 4), " +
99
+ "314px";
100
+ }
101
+ ---
102
+
103
+ <div class="cms-block cms-block-header-image" style={`--max-w: ${maxWidth}px;`}>
104
+ <KamaImage
105
+ image={img}
106
+ sizes={sizes}
107
+ desktopWidths={optimizedWidths}
108
+ idealWidth={targetColumnWidth}
109
+ width={data.width || data.image_meta?.width}
110
+ height={data.height || data.image_meta?.height}
111
+ class="responsive-header-image"
112
+ loading={isLcp ? "eager" : "lazy"}
113
+ fetchpriority={isLcp ? "high" : "auto"}
114
+ decoding={isLcp ? "sync" : "async"}
115
+ />
116
+ </div>
117
+
118
+ <style>
119
+ .cms-block-header-image {
120
+ width: 100%;
121
+ max-width: var(--max-w, 1352px);
122
+ margin: 0 auto 1.5rem auto;
123
+ }
124
+
125
+ :global(.responsive-header-image) {
126
+ width: 100%;
127
+ height: auto;
128
+ display: block;
129
+ border-radius: var(--cms-radius-md, 0.5rem);
130
+ max-width: 100%;
131
+ }
132
+ </style>
@@ -0,0 +1,218 @@
1
+ ---
2
+ // src/components/blocks/Hero.astro
3
+ ---
4
+
5
+ <section class="hero-container">
6
+ <div class="hero-content">
7
+ <!-- Sub-badge -->
8
+ <div class="hero-badge">
9
+ <span class="badge-dot"></span>
10
+ Freeware for Education & NGOs
11
+ </div>
12
+
13
+ <!-- H1 Title with Slogan -->
14
+ <h1 class="hero-title">KAMA – Headless CMS on Edge</h1>
15
+
16
+ <!-- Subtitle -->
17
+ <p class="hero-subtitle">
18
+ Deploy sub-millisecond corporate web experiences with zero infrastructure
19
+ overhead. Built natively on Astro, Hono, and Cloudflare Workers (D1/R2).
20
+ </p>
21
+
22
+ <!-- Call to Action Buttons -->
23
+ <div class="hero-actions">
24
+ <a href="/docs/quick-start" class="btn btn-primary">
25
+ Get Started
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ class="btn-icon"
37
+ ><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
38
+ >
39
+ </a>
40
+ <a
41
+ href="https://github.com/brightside-ou/kama"
42
+ target="_blank"
43
+ rel="noopener noreferrer"
44
+ class="btn btn-secondary"
45
+ >
46
+ View on GitHub
47
+ </a>
48
+ </div>
49
+
50
+ <!-- Tech Stack Highlights -->
51
+ <div class="hero-stack">
52
+ <span class="stack-label">Powered by edge infrastructure:</span>
53
+ <div class="stack-tags">
54
+ <span class="tag">Cloudflare Workers</span>
55
+ <span class="tag">D1 SQL</span>
56
+ <span class="tag">R2 Storage</span>
57
+ <span class="tag">Astro</span>
58
+ <span class="tag">Hono</span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ <style>
65
+ .hero-container {
66
+ padding: 64px 0 48px;
67
+ background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
68
+ border-bottom: 1px solid #e2e8f0;
69
+ }
70
+
71
+ .hero-content {
72
+ max-width: 860px;
73
+ margin: 0 auto;
74
+ text-align: center;
75
+ }
76
+
77
+ .hero-badge {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 6px 14px;
82
+ background-color: #f0f9ff;
83
+ border: 1px solid #bae6fd;
84
+ color: #0369a1;
85
+ font-size: 0.875rem;
86
+ font-weight: 600;
87
+ margin-bottom: 24px;
88
+ }
89
+
90
+ .badge-dot {
91
+ width: 8px;
92
+ height: 8px;
93
+ background-color: #0284c7;
94
+ border-radius: 50% !important;
95
+ }
96
+
97
+ .hero-title {
98
+ font-size: 3rem;
99
+ font-weight: 800;
100
+ line-height: 1.15;
101
+ color: #0f172a;
102
+ letter-spacing: -0.03em;
103
+ margin-top: 0;
104
+ margin-bottom: 20px;
105
+ }
106
+
107
+ .hero-subtitle {
108
+ font-size: 1.25rem;
109
+ line-height: 1.6;
110
+ color: #475569;
111
+ max-width: 720px;
112
+ margin: 0 auto 36px;
113
+ }
114
+
115
+ .hero-actions {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 16px;
120
+ margin-bottom: 48px;
121
+ }
122
+
123
+ .btn {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ gap: 8px;
128
+ padding: 12px 28px;
129
+ font-size: 1rem;
130
+ font-weight: 600;
131
+ text-decoration: none;
132
+ transition: all 0.15s ease-in-out;
133
+ }
134
+
135
+ .btn-primary {
136
+ background-color: #0284c7;
137
+ color: #ffffff;
138
+ }
139
+
140
+ .btn-primary:hover {
141
+ background-color: #0369a1;
142
+ color: #ffffff;
143
+ }
144
+
145
+ .btn-secondary {
146
+ background-color: #ffffff;
147
+ color: #334155;
148
+ border: 1px solid #cbd5e1;
149
+ }
150
+
151
+ .btn-secondary:hover {
152
+ background-color: #f8fafc;
153
+ color: #0f172a;
154
+ border-color: #94a3b8;
155
+ }
156
+
157
+ .btn-icon {
158
+ transition: transform 0.15s ease;
159
+ }
160
+
161
+ .btn-primary:hover .btn-icon {
162
+ transform: translateX(4px);
163
+ }
164
+
165
+ .hero-stack {
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: center;
169
+ gap: 12px;
170
+ }
171
+
172
+ .stack-label {
173
+ font-size: 0.8125rem;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.05em;
176
+ color: #64748b;
177
+ font-weight: 600;
178
+ }
179
+
180
+ .stack-tags {
181
+ display: flex;
182
+ flex-wrap: wrap;
183
+ justify-content: center;
184
+ gap: 8px;
185
+ }
186
+
187
+ .tag {
188
+ padding: 4px 10px;
189
+ background-color: #f1f5f9;
190
+ color: #334155;
191
+ font-size: 0.8125rem;
192
+ font-weight: 500;
193
+ border: 1px solid #e2e8f0;
194
+ }
195
+
196
+ @media (max-width: 768px) {
197
+ .hero-container {
198
+ padding: 40px 0 32px;
199
+ }
200
+
201
+ .hero-title {
202
+ font-size: 2.125rem;
203
+ }
204
+
205
+ .hero-subtitle {
206
+ font-size: 1.05rem;
207
+ }
208
+
209
+ .hero-actions {
210
+ flex-direction: column;
211
+ width: 100%;
212
+ }
213
+
214
+ .btn {
215
+ width: 100%;
216
+ }
217
+ }
218
+ </style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ // src/components/blocks/HtmlBlock.astro
3
+
4
+ interface Props {
5
+ data: {
6
+ body?: string;
7
+ html?: string;
8
+ content?: string;
9
+ text?: string;
10
+ };
11
+ }
12
+
13
+ const { data } = Astro.props;
14
+
15
+ // Fallback chain matching your schema's HTML field key ('body') and legacy aliases
16
+ const rawHtml = data?.body || data?.html || data?.content || data?.text || "";
17
+ ---
18
+
19
+ {
20
+ rawHtml && (
21
+ <div class="cms-block cms-block-html">
22
+ <Fragment set:html={rawHtml} />
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,92 @@
1
+ ---
2
+ // src/components/blocks/ImageBlock.astro
3
+
4
+ import KamaImage from "@/components/ui/KamaImage.astro";
5
+
6
+ interface Props {
7
+ data: {
8
+ image?: any;
9
+ width?: number | string;
10
+ height?: number | string;
11
+ };
12
+ region?: string;
13
+ isFirst?: boolean;
14
+ }
15
+
16
+ const { data, region = "main", isFirst = false } = Astro.props;
17
+
18
+ const img = data?.image;
19
+ const isLcp = region === "header" || (region === "main" && isFirst);
20
+
21
+ // 🎯 Detect column layout based on CMS region names
22
+ let columnCount = 1;
23
+ if (region.includes("_")) {
24
+ columnCount = 2;
25
+ if (region.includes("_middle") || region.includes("_col3")) columnCount = 3;
26
+ if (region.includes("_col4")) columnCount = 4;
27
+ }
28
+
29
+ // 🎯 Column Config Lookup (Max width, breakpoint widths, sizes)
30
+ const COLUMN_CONFIGS: Record<
31
+ number,
32
+ { targetWidth: number; widths: number[]; sizes: string }
33
+ > = {
34
+ 1: {
35
+ targetWidth: 1352,
36
+ widths: [768, 1352],
37
+ sizes: "(max-width: 1399px) calc(100vw - 48px), 1352px",
38
+ },
39
+ 2: {
40
+ targetWidth: 660,
41
+ widths: [400, 660],
42
+ sizes:
43
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 80px) / 2), 660px",
44
+ },
45
+ 3: {
46
+ targetWidth: 429,
47
+ widths: [300, 429],
48
+ sizes:
49
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 112px) / 3), 429px",
50
+ },
51
+ 4: {
52
+ targetWidth: 314,
53
+ widths: [264, 314],
54
+ sizes:
55
+ "(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 144px) / 4), 314px",
56
+ },
57
+ };
58
+
59
+ const layout = COLUMN_CONFIGS[columnCount] || COLUMN_CONFIGS[1];
60
+ ---
61
+
62
+ {
63
+ img && (
64
+ <div class="cms-block cms-block-image">
65
+ <KamaImage
66
+ image={img}
67
+ sizes={layout.sizes}
68
+ desktopWidths={layout.widths}
69
+ idealWidth={layout.targetWidth}
70
+ maxWidth={layout.targetWidth}
71
+ class="free-crop-image"
72
+ loading={isLcp ? "eager" : "lazy"}
73
+ fetchpriority={isLcp ? "high" : "auto"}
74
+ />
75
+ </div>
76
+ )
77
+ }
78
+
79
+ <style>
80
+ .cms-block-image {
81
+ width: 100%;
82
+ margin: 0 auto 1.5rem auto;
83
+ }
84
+
85
+ :global(.free-crop-image) {
86
+ width: 100%;
87
+ height: auto;
88
+ object-fit: contain;
89
+ display: block;
90
+ border-radius: var(--cms-radius-md, 0.5rem);
91
+ }
92
+ </style>
@@ -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,67 @@
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, slug, isHomePage } = Astro.props;
11
+
12
+ // 🎯 Detect home page via URL pathname or explicit flag
13
+ const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
14
+ ---
15
+
16
+ <nav class="breadcrumb-nav" aria-label="Breadcrumb">
17
+ <ol class="breadcrumb-list">
18
+ <li class="breadcrumb-item">
19
+ <a href="/">Home</a>
20
+ </li>
21
+ {
22
+ !isHome && (
23
+ <li class="breadcrumb-item active" aria-current="page">
24
+ <span class="separator">/</span>
25
+ <span>{title}</span>
26
+ </li>
27
+ )
28
+ }
29
+ </ol>
30
+ </nav>
31
+
32
+ <style>
33
+ .breadcrumb-nav {
34
+ margin-bottom: 16px;
35
+ }
36
+
37
+ .breadcrumb-list {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ list-style: none;
42
+ margin: 0;
43
+ padding: 0;
44
+ font-size: 13px;
45
+ color: #64748b;
46
+ }
47
+
48
+ .breadcrumb-item a {
49
+ color: #334155;
50
+ text-decoration: none;
51
+ }
52
+
53
+ .breadcrumb-item a:hover {
54
+ color: #0f172a;
55
+ text-decoration: underline;
56
+ }
57
+
58
+ .breadcrumb-item.active {
59
+ color: #0f172a;
60
+ font-weight: 500;
61
+ }
62
+
63
+ .separator {
64
+ margin-right: 8px;
65
+ color: #94a3b8;
66
+ }
67
+ </style>
@@ -0,0 +1,89 @@
1
+ ---
2
+ // src/components/global/Footer.astro
3
+
4
+ import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
5
+ import { getChannelId } from "@/lib/cms";
6
+
7
+ // 🎯 Fetch data natively during the server request
8
+ const safeEnv = (Astro.locals as any).safeEnv || {};
9
+ const cmsWorker = safeEnv.CMS_WORKER;
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);
50
+ }
51
+ ---
52
+
53
+ <footer class="site-footer">
54
+ <div class="footer-container">
55
+ {
56
+ footerBlocks && footerBlocks.length > 0 ? (
57
+ <BlockRenderer
58
+ blocks={footerBlocks}
59
+ contentMap={contentMap}
60
+ region="footer"
61
+ />
62
+ ) : (
63
+ <div class="footer-default">
64
+ <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
65
+ </div>
66
+ )
67
+ }
68
+ </div>
69
+ </footer>
70
+
71
+ <style>
72
+ .site-footer {
73
+ background: #ffffff;
74
+ border-top: 1px solid #e2e8f0;
75
+ margin-top: auto;
76
+ }
77
+
78
+ .footer-container {
79
+ max-width: 1400px;
80
+ margin: 0 auto;
81
+ padding: 24px 20px;
82
+ }
83
+
84
+ .footer-default p {
85
+ margin: 0;
86
+ font-size: 14px;
87
+ color: #64748b;
88
+ }
89
+ </style>