@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.
- package/bin/cli.js +229 -231
- package/dist/assets/index-BesA3e7A.js +164 -0
- package/dist/assets/index-D6yHZ_u1.css +1 -0
- package/dist/{dist-C-WAK7EF.js → dist-mic4UZBb.js} +691 -439
- package/dist/index.html +2 -2
- package/dist/index.js +693 -439
- package/dist/schema-compiler.js +1 -1
- package/package.json +1 -1
- package/schema.sql +2 -2
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +1 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +1 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +1 -0
- package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +2 -1
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +39 -71
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +2 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +2 -0
- package/templates/astro-starter/src/components/global/Footer.astro +2 -0
- package/templates/astro-starter/src/components/global/Header.astro +100 -28
- package/templates/astro-starter/src/components/{seo/SeoHead.astro → global/MetaHead.astro} +101 -49
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +1 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +114 -67
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +5 -3
- package/templates/astro-starter/src/layouts/StandardLayout.astro +14 -5
- package/templates/astro-starter/src/lib/cms.ts +48 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +16 -1
- package/templates/astro-starter/src/lib/media/media-proxy.ts +8 -6
- package/templates/astro-starter/src/lib/media/media-url.ts +65 -1
- package/templates/astro-starter/src/middleware.ts +131 -18
- package/templates/astro-starter/src/pages/[...slug].astro +10 -9
- package/definitions/_page.yaml +0 -52
- package/dist/assets/index-DGHRgyms.js +0 -164
- package/dist/assets/index-EaDn3OUJ.css +0 -1
- /package/templates/astro-starter/{public/css → src/styles}/standard-theme.css +0 -0
package/dist/schema-compiler.js
CHANGED
|
@@ -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-
|
|
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
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
|
|
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
|
|
92
|
+
language TEXT,
|
|
93
93
|
isHidden INTEGER NOT NULL DEFAULT 0,
|
|
94
94
|
status TEXT DEFAULT 'draft',
|
|
95
95
|
hasUnpublishedChanges INTEGER DEFAULT 0,
|
|
@@ -1,43 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
// src/components/blocks/ImageBlock.astro
|
|
3
|
-
import KamaImage from "@/components/ui/KamaImage.astro";
|
|
4
3
|
|
|
5
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
<div class="cms-block cms-block-image"
|
|
63
|
+
img && (
|
|
64
|
+
<div class="cms-block cms-block-image">
|
|
97
65
|
<KamaImage
|
|
98
66
|
image={img}
|
|
99
|
-
sizes={sizes}
|
|
100
|
-
desktopWidths={
|
|
101
|
-
idealWidth={
|
|
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,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
//
|
|
2
|
+
// src/components/global/Header.astro
|
|
3
|
+
|
|
3
4
|
import NavigationMenu from "@/components/global/NavigationMenu.astro";
|
|
4
|
-
import
|
|
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
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
if (!
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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/
|
|
2
|
+
// src/components/global/MetaHead.astro
|
|
3
|
+
|
|
3
4
|
import type { ContentBlock } from "@/lib/cms";
|
|
4
|
-
import {
|
|
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
|
-
// 🎯
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
}
|
|
66
|
+
// 🎯 2. Favicon Resolution (Channel -> Fallback Static)
|
|
67
|
+
const faviconUrl = getImageUri(channel?.favicon, {
|
|
68
|
+
fallback: "/favicon.png",
|
|
69
|
+
})!;
|
|
34
70
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
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
|
-
// 🎯
|
|
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
|
-
|
|
48
|
-
|
|
84
|
+
const lcpImageUrl = getImageUri(candidateBlock?.data?.image, {
|
|
85
|
+
width: 1352,
|
|
86
|
+
quality: 75,
|
|
87
|
+
});
|
|
49
88
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 🎯
|
|
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
|
-
// 🎯
|
|
77
|
-
const
|
|
78
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
:
|
|
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
|
-
|
|
87
|
-
:
|
|
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
|
-
// 🎯
|
|
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
|
-
// 🎯
|
|
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":
|
|
179
|
+
"name": agencyName || brandName,
|
|
180
|
+
"url": siteOrigin,
|
|
131
181
|
"logo": {
|
|
132
182
|
"@type": "ImageObject",
|
|
133
183
|
"url": brandLogo,
|
|
134
|
-
"caption":
|
|
184
|
+
"caption": brandName
|
|
135
185
|
},
|
|
136
186
|
"address": {
|
|
137
187
|
"@type": "PostalAddress",
|
|
138
|
-
"addressLocality": "
|
|
139
|
-
"addressCountry": "
|
|
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":
|
|
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=
|
|
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" />
|