@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.
- package/README.md +169 -0
- package/bin/cli.js +726 -261
- package/content/definitions/blocks/button.yaml +34 -0
- package/content/definitions/blocks/header.yaml +13 -0
- package/content/definitions/blocks/header_image.yaml +21 -0
- package/content/definitions/blocks/hero.yaml +28 -0
- package/content/definitions/blocks/html.yaml +13 -0
- package/content/definitions/blocks/image_card.yaml +56 -0
- package/content/definitions/blocks/node-list.yaml +61 -0
- package/content/definitions/blocks/single_image.yaml +27 -0
- package/content/definitions/blocks/text.yaml +15 -0
- package/content/definitions/channels/ecommerce.yaml +28 -0
- package/content/definitions/channels/website.yaml +41 -0
- package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
- package/content/definitions/field-sets/channel-base.yaml +57 -0
- package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
- package/content/definitions/field-sets/content-block.yaml +41 -0
- package/content/definitions/field-sets/page-base.yaml +44 -0
- package/content/definitions/field-sets/page-seo.yaml +34 -0
- package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
- package/content/definitions/field-sets/schema-type.yaml +43 -0
- package/content/definitions/field-sets/tree-base.yaml +31 -0
- package/content/definitions/node-layouts/content.yml +12 -0
- package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
- package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
- package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
- package/content/definitions/nodes/address.yaml +40 -0
- package/content/definitions/nodes/article.yaml +65 -0
- package/content/definitions/nodes/event.yaml +55 -0
- package/content/definitions/nodes/external-link.yaml +29 -0
- package/content/definitions/nodes/page-skeleton.yaml +48 -0
- package/{defaults → content}/definitions/nodes/page.yaml +9 -20
- package/content/definitions/nodes/person.yaml +37 -0
- package/content/definitions/nodes/product.yaml +89 -0
- package/content/definitions/taxonomies/categories.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +12 -0
- package/content/definitions/trees/navigation.yaml +24 -0
- package/definitions/editor/default.ts +1 -0
- package/dist/assets/index-Bzbme3Ob.css +1 -0
- package/dist/assets/index-DX98CiK-.js +164 -0
- package/dist/dist-CErcWOwp.js +27136 -0
- package/dist/index.html +4 -2
- package/dist/index.js +26159 -8575
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +23 -25
- package/schema.sql +67 -23
- package/templates/astro-starter/astro.config.mjs +27 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/public/robots.txt +23 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
- package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
- package/templates/astro-starter/src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/src/components/global/Header.astro +171 -0
- package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
- package/templates/astro-starter/src/lib/cms.ts +219 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
- package/templates/astro-starter/src/middleware.ts +508 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +318 -0
- package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
- package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
- package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
- package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
- package/templates/astro-starter/src/styles/standard-theme.css +157 -0
- package/templates/astro-starter/src/utils/tiptap.ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/templates/kama-worker/index.js +19 -0
- package/templates/kama-worker/server.js +104 -0
- package/wrangler.json +22 -0
- package/defaults/definitions/blocks/button.yaml +0 -15
- package/defaults/definitions/blocks/header.yaml +0 -15
- package/defaults/definitions/blocks/header_image.yaml +0 -22
- package/defaults/definitions/blocks/hero.yaml +0 -27
- package/defaults/definitions/blocks/image_card.yaml +0 -57
- package/defaults/definitions/blocks/single_image.yaml +0 -28
- package/defaults/definitions/blocks/text.yaml +0 -16
- package/defaults/definitions/channels/ecommerce.yaml +0 -24
- package/defaults/definitions/channels/website.yaml +0 -32
- package/defaults/definitions/field-sets/channel-base.yaml +0 -31
- package/defaults/definitions/field-sets/content-block.yaml +0 -46
- package/defaults/definitions/field-sets/page-base.yaml +0 -49
- package/defaults/definitions/field-sets/tree-base.yaml +0 -31
- package/defaults/definitions/node-layouts/three-column.yml +0 -41
- package/defaults/definitions/nodes/address.yaml +0 -32
- package/defaults/definitions/nodes/article.yaml +0 -43
- package/defaults/definitions/nodes/event.yaml +0 -49
- package/defaults/definitions/nodes/external-link.yaml +0 -26
- package/defaults/definitions/nodes/person.yaml +0 -37
- package/defaults/definitions/nodes/product.yaml +0 -86
- package/defaults/definitions/trees/navigation.yaml +0 -29
- package/defaults/definitions/trees/social-links.yaml +0 -9
- package/definitions/_page.yaml +0 -52
- package/dist/assets/index-TdW5F0pX.css +0 -1
- package/dist/assets/index-u9OkBauM.js +0 -1392
- package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
- package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
- package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
- package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
- package/scripts/bundle-definitions.js +0 -176
- /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
- /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
- /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
- /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
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, 1920, 2560],
|
|
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
|
+
// 🎯 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)
|
|
97
|
+
const mobileSrcset =
|
|
98
|
+
!isSvg && rawMobileUrl
|
|
99
|
+
? buildAgnosticSrcset(rawMobileUrl, [320, 640, 768])
|
|
100
|
+
: null;
|
|
101
|
+
|
|
102
|
+
const tabletSrcset =
|
|
103
|
+
!isSvg && rawTabletUrl
|
|
104
|
+
? buildAgnosticSrcset(rawTabletUrl, [768, 1024, 1352])
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
const desktopSrcset =
|
|
108
|
+
!isSvg && rawDesktopUrl
|
|
109
|
+
? buildAgnosticSrcset(
|
|
110
|
+
rawDesktopUrl,
|
|
111
|
+
desktopWidths.length ? desktopWidths : [768, 1352, 1920, 2560],
|
|
112
|
+
)
|
|
113
|
+
: null;
|
|
114
|
+
|
|
115
|
+
const fallbackUrl =
|
|
116
|
+
getImageUri(rawDesktopUrl, {
|
|
117
|
+
width: isSvg ? undefined : baseWidth,
|
|
118
|
+
quality: 75,
|
|
119
|
+
}) || "";
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
isValidImage &&
|
|
124
|
+
(isSvg ? (
|
|
125
|
+
/* 🎯 Clean single <img> for SVG Vectors */
|
|
126
|
+
<img
|
|
127
|
+
src={fallbackUrl}
|
|
128
|
+
alt={targetImage.alt || targetImage.filename || ""}
|
|
129
|
+
width={rawWidth > 0 ? rawWidth : undefined}
|
|
130
|
+
height={rawHeight > 0 ? rawHeight : undefined}
|
|
131
|
+
loading={loading}
|
|
132
|
+
fetchpriority={fetchpriority}
|
|
133
|
+
decoding={loading === "eager" ? "sync" : "async"}
|
|
134
|
+
class={className}
|
|
135
|
+
/>
|
|
136
|
+
) : (
|
|
137
|
+
/* 🎯 Responsive <picture> for Raster Images (.png, .jpg, .webp) */
|
|
138
|
+
<picture>
|
|
139
|
+
{Boolean(mobileSrcset) && (
|
|
140
|
+
<source
|
|
141
|
+
media="(max-width: 767px)"
|
|
142
|
+
srcset={mobileSrcset as string}
|
|
143
|
+
sizes="(max-width: 767px) calc(100vw - 48px), 768px"
|
|
144
|
+
width={mobileW}
|
|
145
|
+
height={mobileH}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{Boolean(tabletSrcset) && (
|
|
150
|
+
<source
|
|
151
|
+
media="(min-width: 768px) and (max-width: 1024px)"
|
|
152
|
+
srcset={tabletSrcset as string}
|
|
153
|
+
sizes="(max-width: 1024px) calc(100vw - 48px), 1024px"
|
|
154
|
+
width={tabletW}
|
|
155
|
+
height={tabletH}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{Boolean(desktopSrcset) && (
|
|
160
|
+
<source
|
|
161
|
+
srcset={desktopSrcset as string}
|
|
162
|
+
sizes={sizes}
|
|
163
|
+
width={baseWidth}
|
|
164
|
+
height={baseHeight}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<img
|
|
169
|
+
src={fallbackUrl}
|
|
170
|
+
alt={targetImage.alt || targetImage.filename || ""}
|
|
171
|
+
width={baseWidth}
|
|
172
|
+
height={baseHeight}
|
|
173
|
+
loading={loading}
|
|
174
|
+
fetchpriority={fetchpriority}
|
|
175
|
+
decoding={loading === "eager" ? "sync" : "async"}
|
|
176
|
+
class={className}
|
|
177
|
+
/>
|
|
178
|
+
</picture>
|
|
179
|
+
))
|
|
180
|
+
}
|
|
@@ -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,63 @@
|
|
|
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
|
+
// 🎯 FIX: Extract the footer blocks from the payload again
|
|
36
|
+
const footerBlocks = content?.footer || [];
|
|
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
|
+
{/* 🎯 FIX: Pass the blocks and contentMap into the Footer */}
|
|
61
|
+
<Footer blocks={footerBlocks} contentMap={content} />
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ContentBlock {
|
|
20
|
+
instanceId: number;
|
|
21
|
+
componentId: string;
|
|
22
|
+
data: Record<string, any>;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RegionMeta {
|
|
27
|
+
isInherited: boolean;
|
|
28
|
+
inheritedFromId?: string | number;
|
|
29
|
+
inheritedFromTitle?: string;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface NodeResponse {
|
|
34
|
+
channelId: string | number;
|
|
35
|
+
treeId: string | number;
|
|
36
|
+
language: string;
|
|
37
|
+
node: {
|
|
38
|
+
id: number;
|
|
39
|
+
title: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
nodeType: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
createdAt?: string;
|
|
44
|
+
updatedAt?: string;
|
|
45
|
+
summary?: string;
|
|
46
|
+
children?: any[];
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
};
|
|
49
|
+
content: Record<string, ContentBlock[]>;
|
|
50
|
+
regionMeta?: Record<string, RegionMeta>;
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChannelResponse {
|
|
55
|
+
id: number;
|
|
56
|
+
key: string;
|
|
57
|
+
label: string;
|
|
58
|
+
channelType: string;
|
|
59
|
+
defaultLanguage: string;
|
|
60
|
+
// Core branding & SEO properties typed for IDE auto-complete:
|
|
61
|
+
logo?: string;
|
|
62
|
+
logoText?: string;
|
|
63
|
+
favicon?: string;
|
|
64
|
+
siteTitle?: string;
|
|
65
|
+
titleSuffix?: string;
|
|
66
|
+
defaultSeoImage?: string;
|
|
67
|
+
// 🎯 Dynamically captures all public fields defined in YAML channel schemas:
|
|
68
|
+
[key: string]: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolves base CMS URL across runtime environments safely via Astro
|
|
73
|
+
*/
|
|
74
|
+
export function getCmsUrl(): string {
|
|
75
|
+
const url =
|
|
76
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
77
|
+
import.meta.env.PUBLIC_CMS_URL ||
|
|
78
|
+
'http://localhost:8787';
|
|
79
|
+
return url.replace(/\/+$/, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves Default Channel ID across environments
|
|
84
|
+
*/
|
|
85
|
+
export function getChannelId(): string {
|
|
86
|
+
return import.meta.env.PUBLIC_CHANNEL_ID || 'website';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves Default Tree ID across environments
|
|
91
|
+
*/
|
|
92
|
+
export function getTreeId(): string {
|
|
93
|
+
return import.meta.env.PUBLIC_TREE_ID || 'navigation';
|
|
94
|
+
}
|
|
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
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch channel metadata by key/ID.
|
|
106
|
+
* Serves global site branding, logos, favicons, and schema-defined public fields.
|
|
107
|
+
*/
|
|
108
|
+
export async function getChannel(
|
|
109
|
+
channelKeyOrId: string = getChannelId(),
|
|
110
|
+
customFetch: typeof fetch = fetch
|
|
111
|
+
): Promise<ChannelResponse | null> {
|
|
112
|
+
const baseUrl = getCmsUrl();
|
|
113
|
+
const requestUrl = `${baseUrl}/api/v1/channel/${channelKeyOrId}`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const res = await customFetch(requestUrl, {
|
|
117
|
+
headers: getAuthHeaders(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!res.ok) return null;
|
|
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
|
+
|
|
128
|
+
return (await res.json()) as ChannelResponse;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[CMS Client] Failed to fetch public channel:', err);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch tree navigation hierarchy by tree key/ID and channel.
|
|
137
|
+
* Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
|
|
138
|
+
*/
|
|
139
|
+
export async function getTree(
|
|
140
|
+
treeKeyOrId: string = getTreeId(),
|
|
141
|
+
channel: string | number = getChannelId(),
|
|
142
|
+
customFetch: typeof fetch = fetch
|
|
143
|
+
): Promise<TreeNode[]> {
|
|
144
|
+
const baseUrl = getCmsUrl();
|
|
145
|
+
const requestUrl = `${baseUrl}/api/v1/tree/${treeKeyOrId}?channelId=${channel}`;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const res = await customFetch(requestUrl, {
|
|
149
|
+
headers: getAuthHeaders(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!res.ok) return [];
|
|
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
|
+
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
return data.items || [];
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('[CMS Client] Failed to fetch public tree:', err);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fetch single content node payload by its full URL path.
|
|
170
|
+
* Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
|
|
171
|
+
*/
|
|
172
|
+
export async function getNodeByPath(
|
|
173
|
+
path: string,
|
|
174
|
+
channel: string | number = getChannelId(),
|
|
175
|
+
preview = false,
|
|
176
|
+
customFetch: typeof fetch = fetch
|
|
177
|
+
): Promise<NodeResponse | null> {
|
|
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';
|
|
181
|
+
const baseUrl = getCmsUrl();
|
|
182
|
+
const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const res = await customFetch(requestUrl, {
|
|
186
|
+
headers: getAuthHeaders(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!res.ok) return null;
|
|
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
|
+
|
|
197
|
+
return (await res.json()) as NodeResponse;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(`[CMS Client] Failed to fetch node by path (${cleanPath}):`, err);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolves relative media URLs against base CMS URL
|
|
206
|
+
*/
|
|
207
|
+
export function getMediaUrl(url?: string): string {
|
|
208
|
+
if (!url) return '';
|
|
209
|
+
|
|
210
|
+
const baseUrl = getCmsUrl();
|
|
211
|
+
let cleanUrl = url.replace(/^http:\/\/localhost:\d+/, baseUrl);
|
|
212
|
+
|
|
213
|
+
if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) {
|
|
214
|
+
return cleanUrl;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const cleanPath = cleanUrl.replace(/^\/+/, '');
|
|
218
|
+
return `${baseUrl}/${cleanPath}`;
|
|
219
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/lib/media/kama-image-service.ts
|
|
2
|
+
|
|
3
|
+
import type { LocalImageService } from "astro";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether a given path or URL represents a vector SVG image.
|
|
7
|
+
*/
|
|
8
|
+
function isSvgImage(src: string): boolean {
|
|
9
|
+
if (!src) return false;
|
|
10
|
+
const lower = src.toLowerCase();
|
|
11
|
+
return lower.endsWith('.svg') || lower.includes('image/svg+xml') || lower.includes('.svg?');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const kamaImageService: LocalImageService = {
|
|
15
|
+
getURL(options) {
|
|
16
|
+
let src = options.src;
|
|
17
|
+
|
|
18
|
+
if (typeof src === 'object' && 'src' in src) {
|
|
19
|
+
src = src.src;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof src === 'string') {
|
|
23
|
+
try {
|
|
24
|
+
// 🎯 SVG Guard: Bypass raster query params for vector graphics
|
|
25
|
+
if (isSvgImage(src)) {
|
|
26
|
+
return src;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const url = new URL(src, 'http://localhost');
|
|
30
|
+
|
|
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');
|
|
46
|
+
|
|
47
|
+
return src.startsWith('http') ? url.toString() : (url.pathname + url.search);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return src;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return String(src);
|
|
53
|
+
},
|
|
54
|
+
parseURL(url) {
|
|
55
|
+
return { src: url.pathname + url.search };
|
|
56
|
+
},
|
|
57
|
+
getHTMLAttributes(options) {
|
|
58
|
+
return {
|
|
59
|
+
width: options.width,
|
|
60
|
+
height: options.height,
|
|
61
|
+
loading: options.loading || 'lazy',
|
|
62
|
+
decoding: options.decoding || 'async',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default kamaImageService;
|