@brightsideoy/kama 0.1.0-alpha.155 → 0.1.0-alpha.157

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 (33) hide show
  1. package/bin/cli.js +229 -231
  2. package/dist/assets/index-BesA3e7A.js +164 -0
  3. package/dist/assets/index-D6yHZ_u1.css +1 -0
  4. package/dist/{dist-C-WAK7EF.js → dist-mic4UZBb.js} +691 -439
  5. package/dist/index.html +2 -2
  6. package/dist/index.js +693 -439
  7. package/dist/schema-compiler.js +1 -1
  8. package/package.json +1 -1
  9. package/schema.sql +2 -2
  10. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +1 -0
  11. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +1 -0
  12. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +1 -0
  13. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +2 -1
  14. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +39 -71
  15. package/templates/astro-starter/src/components/blocks/TextBlock.astro +2 -0
  16. package/templates/astro-starter/src/components/global/Breadcrumb.astro +2 -0
  17. package/templates/astro-starter/src/components/global/Footer.astro +2 -0
  18. package/templates/astro-starter/src/components/global/Header.astro +100 -28
  19. package/templates/astro-starter/src/components/{seo/SeoHead.astro → global/MetaHead.astro} +101 -49
  20. package/templates/astro-starter/src/components/global/NavigationMenu.astro +1 -0
  21. package/templates/astro-starter/src/components/ui/KamaImage.astro +114 -67
  22. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +5 -3
  23. package/templates/astro-starter/src/layouts/StandardLayout.astro +14 -5
  24. package/templates/astro-starter/src/lib/cms.ts +48 -0
  25. package/templates/astro-starter/src/lib/media/kama-image-service.ts +16 -1
  26. package/templates/astro-starter/src/lib/media/media-proxy.ts +8 -6
  27. package/templates/astro-starter/src/lib/media/media-url.ts +65 -1
  28. package/templates/astro-starter/src/middleware.ts +131 -18
  29. package/templates/astro-starter/src/pages/[...slug].astro +10 -9
  30. package/definitions/_page.yaml +0 -52
  31. package/dist/assets/index-DGHRgyms.js +0 -164
  32. package/dist/assets/index-EaDn3OUJ.css +0 -1
  33. /package/templates/astro-starter/{public/css → src/styles}/standard-theme.css +0 -0
@@ -20,7 +20,7 @@ async function u() {
20
20
  if (n(d)) u = (await import(t(d).href)).compileDefinitions;
21
21
  else if (n(f)) u = (await import(t(f).href)).compileDefinitions;
22
22
  else try {
23
- u = (await import("./dist-C-WAK7EF.js")).compileDefinitions;
23
+ u = (await import("./dist-mic4UZBb.js")).compileDefinitions;
24
24
  } catch (e) {
25
25
  console.error("❌ Could not resolve Kama Schema Engine:", e), process.exit(1);
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightsideoy/kama",
3
- "version": "0.1.0-alpha.155",
3
+ "version": "0.1.0-alpha.157",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/schema.sql CHANGED
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS "channels" (
46
46
  channelType TEXT,
47
47
  domain TEXT,
48
48
  baseUrl TEXT,
49
- defaultLanguage TEXT DEFAULT 'en',
49
+ defaultLanguage TEXT,
50
50
  isHidden INTEGER NOT NULL DEFAULT 0,
51
51
  dataJson TEXT,
52
52
  createdById TEXT,
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS "content_nodes" (
89
89
  contentType TEXT NOT NULL,
90
90
  title TEXT NOT NULL,
91
91
  slug TEXT,
92
- language TEXT DEFAULT 'en',
92
+ language TEXT,
93
93
  isHidden INTEGER NOT NULL DEFAULT 0,
94
94
  status TEXT DEFAULT 'draft',
95
95
  hasUnpublishedChanges INTEGER DEFAULT 0,
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  // src/components/blocks/BlockRenderer.astro
3
+
3
4
  import ImageBlock from "@/components/blocks/ImageBlock.astro";
4
5
  import HeaderImage from "@/components/blocks/HeaderImage.astro";
5
6
  import ColumnLayout from "@/components/blocks/ColumnLayout.astro";
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  // src/components/blocks/ColumnLayout.astro
3
+
3
4
  import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
4
5
 
5
6
  interface Props {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  // src/components/blocks/HeaderImage.astro
3
+
3
4
  import KamaImage from "@/components/ui/KamaImage.astro";
4
5
 
5
6
  interface Props {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  // src/components/blocks/HtmlBlock.astro
3
+
3
4
  interface Props {
4
5
  data: {
5
6
  body?: string;
@@ -21,4 +22,4 @@ const rawHtml = data?.body || data?.html || data?.content || data?.text || "";
21
22
  <Fragment set:html={rawHtml} />
22
23
  </div>
23
24
  )
24
- }
25
+ }
@@ -1,43 +1,11 @@
1
1
  ---
2
2
  // src/components/blocks/ImageBlock.astro
3
- import KamaImage from "@/components/ui/KamaImage.astro";
4
3
 
5
- interface CropVariant {
6
- id?: string;
7
- url?: string;
8
- cropCoordinates?: {
9
- width?: number;
10
- height?: number;
11
- };
12
- }
4
+ import KamaImage from "@/components/ui/KamaImage.astro";
13
5
 
14
6
  interface Props {
15
7
  data: {
16
- image?: {
17
- id?: string;
18
- url?: string;
19
- path?: string;
20
- filename?: string;
21
- alt?: string;
22
- width?: number | string;
23
- height?: number | string;
24
- cropCoordinates?: {
25
- width?: number;
26
- height?: number;
27
- };
28
- variants?: {
29
- standard?: CropVariant;
30
- tablet?: CropVariant;
31
- mobile?: CropVariant;
32
- [key: string]: CropVariant | undefined;
33
- };
34
- };
35
- image_meta?: {
36
- cropCoordinates?: {
37
- width?: number;
38
- height?: number;
39
- };
40
- };
8
+ image?: any;
41
9
  width?: number | string;
42
10
  height?: number | string;
43
11
  };
@@ -48,10 +16,9 @@ interface Props {
48
16
  const { data, region = "main", isFirst = false } = Astro.props;
49
17
 
50
18
  const img = data?.image;
51
- const isValid = Boolean(img && (img.url || img.path || img.variants));
52
-
53
19
  const isLcp = region === "header" || (region === "main" && isFirst);
54
20
 
21
+ // 🎯 Detect column layout based on CMS region names
55
22
  let columnCount = 1;
56
23
  if (region.includes("_")) {
57
24
  columnCount = 2;
@@ -59,46 +26,48 @@ if (region.includes("_")) {
59
26
  if (region.includes("_col4")) columnCount = 4;
60
27
  }
61
28
 
62
- let maxWidth = "1352";
63
- let targetColumnWidth = 1352;
64
- let optimizedWidths = [768, 1352];
65
- let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
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
+ };
66
58
 
67
- if (columnCount === 2) {
68
- maxWidth = "660";
69
- targetColumnWidth = 660;
70
- optimizedWidths = [400, 660];
71
- sizes =
72
- "(max-width: 767px) calc(100vw - 48px), " +
73
- "(max-width: 1399px) calc((100vw - 80px) / 2), " +
74
- "660px";
75
- } else if (columnCount === 3) {
76
- maxWidth = "429";
77
- targetColumnWidth = 429;
78
- optimizedWidths = [300, 429];
79
- sizes =
80
- "(max-width: 767px) calc(100vw - 48px), " +
81
- "(max-width: 1399px) calc((100vw - 112px) / 3), " +
82
- "429px";
83
- } else if (columnCount === 4) {
84
- maxWidth = "314";
85
- targetColumnWidth = 314;
86
- optimizedWidths = [264, 314];
87
- sizes =
88
- "(max-width: 767px) calc(100vw - 48px), " +
89
- "(max-width: 1399px) calc((100vw - 144px) / 4), " +
90
- "314px";
91
- }
59
+ const layout = COLUMN_CONFIGS[columnCount] || COLUMN_CONFIGS[1];
92
60
  ---
93
61
 
94
62
  {
95
- isValid && (
96
- <div class="cms-block cms-block-image" style={`--max-w: ${maxWidth}px;`}>
63
+ img && (
64
+ <div class="cms-block cms-block-image">
97
65
  <KamaImage
98
66
  image={img}
99
- sizes={sizes}
100
- desktopWidths={optimizedWidths}
101
- idealWidth={targetColumnWidth}
67
+ sizes={layout.sizes}
68
+ desktopWidths={layout.widths}
69
+ idealWidth={layout.targetWidth}
70
+ maxWidth={layout.targetWidth}
102
71
  class="free-crop-image"
103
72
  loading={isLcp ? "eager" : "lazy"}
104
73
  fetchpriority={isLcp ? "high" : "auto"}
@@ -110,7 +79,6 @@ if (columnCount === 2) {
110
79
  <style>
111
80
  .cms-block-image {
112
81
  width: 100%;
113
- max-width: var(--max-w, 1352px);
114
82
  margin: 0 auto 1.5rem auto;
115
83
  }
116
84
 
@@ -1,4 +1,6 @@
1
1
  ---
2
+ // src/components/blocks/TextBlock.astro
3
+
2
4
  interface Props {
3
5
  data: {
4
6
  text?: string;
@@ -1,4 +1,6 @@
1
1
  ---
2
+ // src/components/global/Breadcrumb.astro
3
+
2
4
  interface Props {
3
5
  title: string;
4
6
  slug?: string;
@@ -1,4 +1,6 @@
1
1
  ---
2
+ // src/components/global/Footer.astro
3
+
2
4
  import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
3
5
 
4
6
  interface Props {
@@ -1,7 +1,9 @@
1
1
  ---
2
- // templates/astro-starter/src/components/global/Header.astro
2
+ // src/components/global/Header.astro
3
+
3
4
  import NavigationMenu from "@/components/global/NavigationMenu.astro";
4
- import { getTree, getTreeId, getChannelId } from "@/lib/cms";
5
+ import KamaImage from "@/components/ui/KamaImage.astro";
6
+ import { getTree, getTreeId, getChannel, getChannelId } from "@/lib/cms";
5
7
  import type { TreeNode } from "@/lib/cms";
6
8
 
7
9
  interface Props {
@@ -13,44 +15,103 @@ const props = Astro.props as Props;
13
15
  const currentPropsNode = props.node || {};
14
16
  let rawTree: TreeNode[] = props.tree || [];
15
17
 
16
- // Fallback: If the layout didn't pass the tree, fetch it directly
17
- if (!rawTree || rawTree.length === 0) {
18
- const locals = Astro.locals as Record<string, any>;
19
- const runtimeEnv = locals?.runtime?.env || {};
20
- const cmsWorker = runtimeEnv.CMS_WORKER;
18
+ // 🎯 Read the safely extracted environment from middleware (Fixes Astro 7 crash)
19
+ const safeEnv = (Astro.locals as any).safeEnv || {};
20
+ const cmsWorker = safeEnv.CMS_WORKER;
21
21
 
22
- // Safely wrap the fetcher for Service Bindings
23
- const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
24
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
25
- return cmsWorker.fetch(new Request(input, init));
26
- }
27
- return fetch(input, init);
28
- };
22
+ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
23
+ const reqInit = { ...init };
24
+ delete reqInit.cache;
25
+
26
+ let urlStr = input instanceof URL ? input.toString() : String(input);
29
27
 
30
- try {
31
- rawTree = await getTree(getTreeId(), getChannelId(), customFetch);
32
- } catch (e) {
33
- rawTree = [];
28
+ if (urlStr.startsWith("/")) {
29
+ const baseUrl =
30
+ import.meta.env.PUBLIC_API_URL ||
31
+ safeEnv.PUBLIC_API_URL ||
32
+ "http://127.0.0.1:8787";
33
+ urlStr = `${baseUrl}${urlStr}`;
34
34
  }
35
- }
36
35
 
37
- const homeSlug = import.meta.env.PUBLIC_HOME_SLUG || "home";
38
- const homeNodeInTree =
39
- rawTree.find(n => n.slug === homeSlug) || rawTree[0] || {};
36
+ const req = new Request(urlStr, reqInit);
37
+
38
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
39
+ return cmsWorker.fetch(req);
40
+ }
41
+ return fetch(req);
42
+ };
43
+
44
+ // 🎯 Fetch Global Channel Metadata and Nav Tree in Parallel
45
+ const channelId = getChannelId() || "website";
46
+ const treeId = getTreeId() || "main-nav";
47
+
48
+ let channelData: Record<string, any> | null = null;
49
+
50
+ try {
51
+ const [fetchedChannel, fetchedTree] = await Promise.all([
52
+ getChannel(channelId, customFetch),
53
+ !rawTree || rawTree.length === 0
54
+ ? getTree(treeId, channelId, customFetch)
55
+ : Promise.resolve(rawTree),
56
+ ]);
40
57
 
41
- let siteName = currentPropsNode.logoText;
42
- if (!siteName && homeNodeInTree && "logoText" in homeNodeInTree) {
43
- siteName = homeNodeInTree.logoText as string;
58
+ channelData = fetchedChannel;
59
+ if (!rawTree || rawTree.length === 0) {
60
+ rawTree = fetchedTree || [];
61
+ }
62
+ } catch (e) {
63
+ console.error("[Header] Failed to load layout data:", e);
44
64
  }
45
- if (!siteName) {
46
- siteName = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
65
+
66
+ // 🎯 1. Resolve Logo Text (Strict waterfall prioritizing explicit strings)
67
+ const rawLogoText = channelData?.logoText;
68
+ const rawSiteTitle = channelData?.siteTitle;
69
+ const rawLabel = channelData?.label;
70
+
71
+ let logoText = "";
72
+
73
+ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
74
+ logoText = rawLogoText;
75
+ } else if (typeof rawSiteTitle === "string" && rawSiteTitle.trim() !== "") {
76
+ logoText = rawSiteTitle;
77
+ } else if (typeof rawLabel === "string" && rawLabel.trim() !== "") {
78
+ logoText = rawLabel;
79
+ } else if (currentPropsNode.logoText) {
80
+ logoText = currentPropsNode.logoText;
81
+ } else {
82
+ logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
47
83
  }
84
+
85
+ // 🎯 2. Extract Raw Logo Asset (Unwraps array wrappers if present)
86
+ const rawLogoAsset = channelData?.logoImage || channelData?.logo;
87
+ const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
88
+
89
+ // 🎯 3. Strict Image Validation Guard (Guarantees non-empty string or populated object)
90
+ const hasValidLogoImage = Boolean(
91
+ targetLogo &&
92
+ ((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
93
+ targetLogo.url ||
94
+ targetLogo.path ||
95
+ targetLogo.variants),
96
+ );
48
97
  ---
49
98
 
50
99
  <header class="site-header">
51
100
  <div class="header-container main-header-row">
52
101
  <a href="/" class="logo-block">
53
- <span class="logo-text">{siteName}</span>
102
+ {
103
+ hasValidLogoImage ? (
104
+ <KamaImage
105
+ image={targetLogo}
106
+ idealWidth={300}
107
+ class="logo-image"
108
+ loading="eager"
109
+ fetchpriority="high"
110
+ />
111
+ ) : (
112
+ <span class="logo-text">{logoText}</span>
113
+ )
114
+ }
54
115
  </a>
55
116
  <NavigationMenu tree={rawTree} />
56
117
  </div>
@@ -91,8 +152,19 @@ if (!siteName) {
91
152
  z-index: 1001;
92
153
  }
93
154
 
155
+ /* 🎯 Strict 16px Height Constraint for KamaImage output */
156
+ :global(.logo-image) {
157
+ height: 16px !important;
158
+ max-height: 16px !important;
159
+ width: auto !important;
160
+ object-fit: contain;
161
+ display: block;
162
+ }
163
+
94
164
  .logo-text {
95
165
  white-space: nowrap;
166
+ font-size: 16px;
167
+ line-height: 16px;
96
168
  }
97
169
 
98
170
  @media (min-width: 768px) {
@@ -1,7 +1,9 @@
1
1
  ---
2
- // src/components/seo/SeoHead.astro
2
+ // src/components/global/MetaHead.astro
3
+
3
4
  import type { ContentBlock } from "@/lib/cms";
4
- import { buildAgnosticImageUrl } from "@/lib/media/media-url";
5
+ import { getChannel, getChannelId } from "@/lib/cms";
6
+ import { getImageUri } from "@/lib/media/media-url";
5
7
 
6
8
  interface Props {
7
9
  node: Record<string, any>;
@@ -9,6 +11,7 @@ interface Props {
9
11
  normalizedSlug: string;
10
12
  isHomePage: boolean;
11
13
  headerBlocks?: ContentBlock[];
14
+ resolvedLanguage?: string;
12
15
  }
13
16
 
14
17
  const {
@@ -16,77 +19,124 @@ const {
16
19
  content = {},
17
20
  normalizedSlug,
18
21
  isHomePage,
19
- headerBlocks = []
22
+ headerBlocks = [],
23
+ resolvedLanguage
20
24
  } = Astro.props;
21
25
 
26
+ // 🎯 0. Read the safely extracted environment from middleware (Fixes Astro 7 crash)
27
+ const safeEnv = (Astro.locals as any).safeEnv || {};
28
+ const cmsWorker = safeEnv.CMS_WORKER;
29
+
30
+ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
31
+ const reqInit = { ...init };
32
+ delete reqInit.cache;
33
+
34
+ let urlStr = input instanceof URL ? input.toString() : String(input);
35
+
36
+ if (urlStr.startsWith("/")) {
37
+ const baseUrl = import.meta.env.PUBLIC_API_URL || safeEnv.PUBLIC_API_URL || "http://127.0.0.1:8787";
38
+ urlStr = `${baseUrl}${urlStr}`;
39
+ }
40
+
41
+ const req = new Request(urlStr, reqInit);
42
+
43
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
44
+ return cmsWorker.fetch(req);
45
+ }
46
+ return fetch(req);
47
+ };
48
+
49
+ // 🎯 Fetch Global Channel Metadata
50
+ const channel = await getChannel(getChannelId(), customFetch);
51
+
52
+ // 🎯 Language Cascade Final Resolution:
53
+ // Prop / Node Language -> Channel Default -> Undefined
54
+ const activeLanguage = resolvedLanguage || node?.language || channel?.defaultLanguage || channel?.language || undefined;
55
+
56
+ // Convert BCP 47 (e.g. et-EE, zh-CN) to Open Graph locale format (et_EE, zh_CN)
57
+ const ogLocale = activeLanguage ? activeLanguage.replace("-", "_") : undefined;
58
+
22
59
  // 🎯 1. Resolve Origin Safely
23
- let siteOrigin = import.meta.env.PUBLIC_FRONTEND_URL || Astro.site?.origin || Astro.url.origin;
60
+ let siteOrigin = import.meta.env.PUBLIC_FRONTEND_URL || safeEnv.PUBLIC_FRONTEND_URL || Astro.site?.origin || Astro.url.origin;
24
61
  if (siteOrigin === "https://example.com") {
25
62
  siteOrigin = Astro.url.origin;
26
63
  }
27
- // Remove trailing slash for consistent concatenation
28
64
  siteOrigin = siteOrigin.replace(/\/$/, "");
29
65
 
30
- // 🎯 Helper: Get raw URL from a CMS Image Object
31
- const getRawImageUrl = (imgObj: any) => {
32
- return imgObj?.variants?.standard?.url || imgObj?.url || imgObj?.path || null;
33
- };
66
+ // 🎯 2. Favicon Resolution (Channel -> Fallback Static)
67
+ const faviconUrl = getImageUri(channel?.favicon, {
68
+ fallback: "/favicon.png",
69
+ })!;
34
70
 
35
- // 🎯 Helper: Make URL Absolute for Social Tags
36
- const makeAbsolute = (pathStr: string) => {
37
- if (!pathStr) return "";
38
- return pathStr.startsWith("http") ? pathStr : `${siteOrigin}${pathStr.startsWith("/") ? "" : "/"}${pathStr}`;
71
+ // Detect MIME type dynamically for the favicon link tag
72
+ const getFaviconType = (url: string) => {
73
+ if (url.endsWith(".svg")) return "image/svg+xml";
74
+ if (url.endsWith(".ico")) return "image/x-icon";
75
+ return "image/png";
39
76
  };
40
77
 
41
- // 🎯 2. Detect Block-level LCP Image Candidate (Fallback)
78
+ // 🎯 3. Detect Block-level LCP Image Candidate
42
79
  const firstHeaderBlock = headerBlocks && headerBlocks[0];
43
80
  const firstContentBlock = content?.content && content.content[0];
44
81
  const candidateBlock = (firstHeaderBlock?.data?.image ? firstHeaderBlock : null)
45
82
  || (firstContentBlock?.data?.image ? firstContentBlock : null);
46
83
 
47
- let lcpImageUrl = "";
48
- let fallbackRawImage = "";
84
+ const lcpImageUrl = getImageUri(candidateBlock?.data?.image, {
85
+ width: 1352,
86
+ quality: 75,
87
+ });
49
88
 
50
- if (candidateBlock?.data?.image) {
51
- fallbackRawImage = getRawImageUrl(candidateBlock.data.image);
52
- if (fallbackRawImage) {
53
- lcpImageUrl = buildAgnosticImageUrl(fallbackRawImage, 1352, 75);
54
- }
55
- }
89
+ // 🎯 4. Text Metadata Variables
90
+ const brandName = channel?.siteTitle || channel?.label || import.meta.env.PUBLIC_SITE_NAME || safeEnv.PUBLIC_SITE_NAME || "KAMA";
91
+ const agencyName = import.meta.env.PUBLIC_AGENCY_NAME || safeEnv.PUBLIC_AGENCY_NAME || "";
56
92
 
57
- // 🎯 3. Text Metadata Variables
58
- const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
59
- const agencyName = import.meta.env.PUBLIC_AGENCY_NAME || "Brightside OÜ";
60
- const brandLogo = `${siteOrigin}/logo.png`;
93
+ const brandLogo = getImageUri(channel?.logoImage || channel?.logo, {
94
+ width: 512,
95
+ absoluteOrigin: siteOrigin,
96
+ fallback: `${siteOrigin}/logo.png`,
97
+ })!;
61
98
 
62
99
  const prefix = node?.titlePrefix?.trim() ? `${node.titlePrefix.trim()} ` : "";
63
- const suffix = node?.titleSuffix?.trim() ? ` ${node.titleSuffix.trim()}` : "";
100
+
101
+ // Suffix Waterfall: Page node -> Channel level (e.g. "- Suffix")
102
+ const channelSuffix = channel?.titleSuffix?.trim() || "";
103
+ const rawSuffix = node?.titleSuffix?.trim() || channelSuffix;
104
+ const suffix = rawSuffix ? ` ${rawSuffix}` : "";
105
+
64
106
  const baseTitle = node?.title?.trim() || brandName;
65
107
  const fullTitle = `${prefix}${baseTitle}${suffix}`;
66
- const metaDescription = node?.summary || "KAMA – The headless content engine built for omnichannel delivery and Cloudflare Edge computing.";
108
+
109
+ // Description Waterfall: Page summary -> Channel metaDescription
110
+ const metaDescription = node?.summary || channel?.metaDescription || "KAMA – The headless content engine built for omnichannel delivery and Cloudflare Edge computing.";
67
111
 
68
112
  const canonicalURL = new URL(Astro.url.pathname, siteOrigin).href;
69
113
 
70
- // 🎯 4. Social Text Waterfall (Uses YAML fields)
114
+ // 🎯 5. Social Text Waterfall
71
115
  const fbTitle = node?.fbTitle?.trim() || fullTitle;
72
116
  const xTitle = node?.xTitle?.trim() || fbTitle;
73
117
  const fbDescription = node?.fbDescription?.trim() || metaDescription;
74
118
  const xDescription = node?.xDescription?.trim() || fbDescription;
75
119
 
76
- // 🎯 5. Social Image Waterfall (Updated for nested JSON payload)
77
- const rawFbImage = node?.socialImages?.facebook?.url || fallbackRawImage;
78
- const rawXImage = node?.socialImages?.x?.url || fallbackRawImage;
120
+ // 🎯 6. Social Image Waterfall (Page -> LCP Candidate -> Channel Default -> Fallback)
121
+ const channelDefaultOg = channel?.defaultSeoImage;
122
+ const rawFbImage = node?.socialImages?.facebook?.url || candidateBlock?.data?.image || channelDefaultOg;
123
+ const rawXImage = node?.socialImages?.x?.url || candidateBlock?.data?.image || channelDefaultOg;
79
124
 
80
- // Build the optimized, absolute URLs
81
- const fbImageUrl = rawFbImage
82
- ? makeAbsolute(buildAgnosticImageUrl(rawFbImage, 1200, 80))
83
- : `${siteOrigin}/default-og-image.png`;
125
+ const fbImageUrl = getImageUri(rawFbImage, {
126
+ width: 1200,
127
+ quality: 80,
128
+ absoluteOrigin: siteOrigin,
129
+ fallback: `${siteOrigin}/default-og-image.png`,
130
+ })!;
84
131
 
85
- const xImageUrl = rawXImage
86
- ? makeAbsolute(buildAgnosticImageUrl(rawXImage, 1200, 80))
87
- : `${siteOrigin}/default-og-image.png`;
132
+ const xImageUrl = getImageUri(rawXImage, {
133
+ width: 1200,
134
+ quality: 80,
135
+ absoluteOrigin: siteOrigin,
136
+ fallback: `${siteOrigin}/default-og-image.png`,
137
+ })!;
88
138
 
89
- // 🎯 6. Structured Data Breadcrumbs
139
+ // 🎯 7. Structured Data Breadcrumbs
90
140
  const breadcrumbListItems = [
91
141
  {
92
142
  "@type": "ListItem",
@@ -119,24 +169,24 @@ if (!isHomePage && normalizedSlug) {
119
169
  });
120
170
  }
121
171
 
122
- // 🎯 7. JSON-LD Schema
172
+ // 🎯 8. JSON-LD Schema
123
173
  const structuredDataGraph = {
124
174
  "@context": "https://schema.org",
125
175
  "@graph": [
126
176
  {
127
177
  "@type": "Organization",
128
178
  "@id": `${siteOrigin}/#organization`,
129
- "name": agencyName,
130
- "url": "https://brightside.ee",
179
+ "name": agencyName || brandName,
180
+ "url": siteOrigin,
131
181
  "logo": {
132
182
  "@type": "ImageObject",
133
183
  "url": brandLogo,
134
- "caption": agencyName
184
+ "caption": brandName
135
185
  },
136
186
  "address": {
137
187
  "@type": "PostalAddress",
138
- "addressLocality": "Kuressaare",
139
- "addressCountry": "EE"
188
+ "addressLocality": "",
189
+ "addressCountry": ""
140
190
  }
141
191
  },
142
192
  {
@@ -146,7 +196,7 @@ const structuredDataGraph = {
146
196
  "applicationCategory": "DeveloperApplication",
147
197
  "softwareVersion": "v0.1.0",
148
198
  "operatingSystem": "Cloudflare Workers, Node.js",
149
- "description": "A headless content engine and visual block editor architected for edge networks.",
199
+ "description": metaDescription,
150
200
  "url": siteOrigin,
151
201
  "publisher": {
152
202
  "@id": `${siteOrigin}/#organization`
@@ -158,6 +208,7 @@ const structuredDataGraph = {
158
208
  "url": siteOrigin,
159
209
  "name": brandName,
160
210
  "description": metaDescription,
211
+ "inLanguage": activeLanguage,
161
212
  "publisher": {
162
213
  "@id": `${siteOrigin}/#organization`
163
214
  }
@@ -177,8 +228,8 @@ const structuredDataGraph = {
177
228
  <title>{fullTitle}</title>
178
229
  <meta name="description" content={metaDescription} />
179
230
 
180
- <!-- 🎯 Favicon -->
181
- <link rel="icon" type="image/png" href="/favicon.png" />
231
+ <!-- 🎯 Channel Favicon -->
232
+ <link rel="icon" type={getFaviconType(faviconUrl)} href={faviconUrl} />
182
233
 
183
234
  <link rel="canonical" href={canonicalURL} />
184
235
  <link rel="author" type="text/plain" href="/llms.txt" title="LLM Directory" />
@@ -190,6 +241,7 @@ const structuredDataGraph = {
190
241
  <meta property="og:description" content={fbDescription} />
191
242
  <meta property="og:image" content={fbImageUrl} />
192
243
  <meta property="og:site_name" content={brandName} />
244
+ {ogLocale && <meta property="og:locale" content={ogLocale} />}
193
245
 
194
246
  <!-- 🎯 X / Twitter -->
195
247
  <meta name="twitter:card" content="summary_large_image" />
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  // src/components/global/NavigationMenu.astro
3
+
3
4
  import type { TreeNode } from "@/lib/cms";
4
5
 
5
6
  interface Props {