@brightsideoy/kama 0.1.0-alpha.98 → 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.
- package/README.md +169 -0
- package/bin/cli.js +646 -240
- package/content/definitions/blocks/button.yaml +24 -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/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 +50 -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 +49 -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-F2rfUymo.css +1 -0
- package/dist/assets/index-lXxH3VFK.js +164 -0
- package/dist/dist-Dy7-HUEG.js +26302 -0
- package/dist/index.html +4 -2
- package/dist/index.js +25205 -8461
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +19 -24
- package/schema.sql +36 -22
- 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 +134 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +132 -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 +92 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +67 -0
- package/templates/astro-starter/src/components/global/Footer.astro +89 -0
- package/templates/astro-starter/src/components/global/Header.astro +177 -0
- package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +402 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +172 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +62 -0
- package/templates/astro-starter/src/lib/cms.ts +197 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +56 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +129 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +98 -0
- package/templates/astro-starter/src/middleware.ts +478 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +237 -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 +98 -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,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/global/Header.astro
|
|
3
|
+
|
|
4
|
+
import NavigationMenu from "@/components/global/NavigationMenu.astro";
|
|
5
|
+
import KamaImage from "@/components/ui/KamaImage.astro";
|
|
6
|
+
import { getTree, getTreeId, getChannel, getChannelId } from "@/lib/cms";
|
|
7
|
+
import type { TreeNode } from "@/lib/cms";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
node?: Record<string, any>;
|
|
11
|
+
tree?: TreeNode[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = Astro.props as Props;
|
|
15
|
+
const currentPropsNode = props.node || {};
|
|
16
|
+
let rawTree: TreeNode[] = props.tree || [];
|
|
17
|
+
|
|
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
|
+
|
|
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);
|
|
27
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
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
|
+
]);
|
|
57
|
+
|
|
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);
|
|
64
|
+
}
|
|
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";
|
|
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
|
+
);
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
<header class="site-header">
|
|
100
|
+
<div class="header-container main-header-row">
|
|
101
|
+
<a href="/" class="logo-block">
|
|
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
|
+
}
|
|
115
|
+
</a>
|
|
116
|
+
<NavigationMenu tree={rawTree} />
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
.site-header {
|
|
122
|
+
background: #ffffff;
|
|
123
|
+
color: #1e293b;
|
|
124
|
+
position: sticky;
|
|
125
|
+
top: 0;
|
|
126
|
+
z-index: 100;
|
|
127
|
+
border-bottom: 1px solid #e2e8f0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.header-container {
|
|
131
|
+
max-width: 1400px;
|
|
132
|
+
margin: 0 auto;
|
|
133
|
+
padding: 0 20px;
|
|
134
|
+
width: 100%;
|
|
135
|
+
box-sizing: border-box;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.main-header-row {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: space-between;
|
|
142
|
+
height: 64px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.logo-block {
|
|
146
|
+
text-decoration: none;
|
|
147
|
+
color: #0f172a;
|
|
148
|
+
font-weight: 700;
|
|
149
|
+
font-size: 20px;
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
z-index: 1001;
|
|
153
|
+
}
|
|
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
|
+
|
|
164
|
+
.logo-text {
|
|
165
|
+
white-space: nowrap;
|
|
166
|
+
font-size: 16px;
|
|
167
|
+
line-height: 16px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (min-width: 768px) {
|
|
171
|
+
.main-header-row {
|
|
172
|
+
height: 64px;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
gap: 32px;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/global/MetaHead.astro
|
|
3
|
+
|
|
4
|
+
import type { ContentBlock } from "@/lib/cms";
|
|
5
|
+
import { getChannel, getChannelId } from "@/lib/cms";
|
|
6
|
+
import { getImageUri } from "@/lib/media/media-url";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
node: Record<string, any>;
|
|
10
|
+
content: Record<string, ContentBlock[]>;
|
|
11
|
+
normalizedSlug: string;
|
|
12
|
+
isHomePage: boolean;
|
|
13
|
+
headerBlocks?: ContentBlock[];
|
|
14
|
+
resolvedLanguage?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
node = {},
|
|
19
|
+
content = {},
|
|
20
|
+
normalizedSlug,
|
|
21
|
+
isHomePage,
|
|
22
|
+
headerBlocks = [],
|
|
23
|
+
resolvedLanguage
|
|
24
|
+
} = Astro.props;
|
|
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
|
+
|
|
59
|
+
// 🎯 1. Resolve Origin Safely
|
|
60
|
+
let siteOrigin = import.meta.env.PUBLIC_FRONTEND_URL || safeEnv.PUBLIC_FRONTEND_URL || Astro.site?.origin || Astro.url.origin;
|
|
61
|
+
if (siteOrigin === "https://example.com") {
|
|
62
|
+
siteOrigin = Astro.url.origin;
|
|
63
|
+
}
|
|
64
|
+
siteOrigin = siteOrigin.replace(/\/$/, "");
|
|
65
|
+
|
|
66
|
+
// 🎯 2. Favicon Resolution (Channel -> Fallback Static)
|
|
67
|
+
const faviconUrl = getImageUri(channel?.favicon, {
|
|
68
|
+
fallback: "/favicon.png",
|
|
69
|
+
})!;
|
|
70
|
+
|
|
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";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 🎯 3. Detect Block-level LCP Image Candidate
|
|
79
|
+
const firstHeaderBlock = headerBlocks && headerBlocks[0];
|
|
80
|
+
const firstContentBlock = content?.content && content.content[0];
|
|
81
|
+
const candidateBlock = (firstHeaderBlock?.data?.image ? firstHeaderBlock : null)
|
|
82
|
+
|| (firstContentBlock?.data?.image ? firstContentBlock : null);
|
|
83
|
+
|
|
84
|
+
const lcpImageUrl = getImageUri(candidateBlock?.data?.image, {
|
|
85
|
+
width: 1352,
|
|
86
|
+
quality: 75,
|
|
87
|
+
});
|
|
88
|
+
|
|
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 || "";
|
|
92
|
+
|
|
93
|
+
const brandLogo = getImageUri(channel?.logoImage || channel?.logo, {
|
|
94
|
+
width: 512,
|
|
95
|
+
absoluteOrigin: siteOrigin,
|
|
96
|
+
fallback: `${siteOrigin}/logo.png`,
|
|
97
|
+
})!;
|
|
98
|
+
|
|
99
|
+
const prefix = node?.titlePrefix?.trim() ? `${node.titlePrefix.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
|
+
|
|
106
|
+
const baseTitle = node?.title?.trim() || brandName;
|
|
107
|
+
const fullTitle = `${prefix}${baseTitle}${suffix}`;
|
|
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.";
|
|
111
|
+
|
|
112
|
+
const canonicalURL = new URL(Astro.url.pathname, siteOrigin).href;
|
|
113
|
+
|
|
114
|
+
// 🎯 5. Social Text Waterfall
|
|
115
|
+
const fbTitle = node?.fbTitle?.trim() || fullTitle;
|
|
116
|
+
const xTitle = node?.xTitle?.trim() || fbTitle;
|
|
117
|
+
const fbDescription = node?.fbDescription?.trim() || metaDescription;
|
|
118
|
+
const xDescription = node?.xDescription?.trim() || fbDescription;
|
|
119
|
+
|
|
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;
|
|
124
|
+
|
|
125
|
+
const fbImageUrl = getImageUri(rawFbImage, {
|
|
126
|
+
width: 1200,
|
|
127
|
+
quality: 80,
|
|
128
|
+
absoluteOrigin: siteOrigin,
|
|
129
|
+
fallback: `${siteOrigin}/default-og-image.png`,
|
|
130
|
+
})!;
|
|
131
|
+
|
|
132
|
+
const xImageUrl = getImageUri(rawXImage, {
|
|
133
|
+
width: 1200,
|
|
134
|
+
quality: 80,
|
|
135
|
+
absoluteOrigin: siteOrigin,
|
|
136
|
+
fallback: `${siteOrigin}/default-og-image.png`,
|
|
137
|
+
})!;
|
|
138
|
+
|
|
139
|
+
// 🎯 7. Structured Data Breadcrumbs
|
|
140
|
+
const breadcrumbListItems = [
|
|
141
|
+
{
|
|
142
|
+
"@type": "ListItem",
|
|
143
|
+
"position": 1,
|
|
144
|
+
"name": "Home",
|
|
145
|
+
"item": `${siteOrigin}/`,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
if (!isHomePage && normalizedSlug) {
|
|
150
|
+
const segments = normalizedSlug.split("/").filter(Boolean);
|
|
151
|
+
let currentPath = "";
|
|
152
|
+
|
|
153
|
+
segments.forEach((segment, index) => {
|
|
154
|
+
currentPath += `/${segment}`;
|
|
155
|
+
const isLast = index === segments.length - 1;
|
|
156
|
+
|
|
157
|
+
const fallbackTitle = segment
|
|
158
|
+
.replace(/-/g, " ")
|
|
159
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
160
|
+
|
|
161
|
+
const segmentTitle = isLast && node?.title ? node.title : fallbackTitle;
|
|
162
|
+
|
|
163
|
+
breadcrumbListItems.push({
|
|
164
|
+
"@type": "ListItem",
|
|
165
|
+
"position": index + 2,
|
|
166
|
+
"name": segmentTitle,
|
|
167
|
+
"item": `${siteOrigin}${currentPath}`,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 🎯 8. JSON-LD Schema
|
|
173
|
+
const structuredDataGraph = {
|
|
174
|
+
"@context": "https://schema.org",
|
|
175
|
+
"@graph": [
|
|
176
|
+
{
|
|
177
|
+
"@type": "Organization",
|
|
178
|
+
"@id": `${siteOrigin}/#organization`,
|
|
179
|
+
"name": agencyName || brandName,
|
|
180
|
+
"url": siteOrigin,
|
|
181
|
+
"logo": {
|
|
182
|
+
"@type": "ImageObject",
|
|
183
|
+
"url": brandLogo,
|
|
184
|
+
"caption": brandName
|
|
185
|
+
},
|
|
186
|
+
"address": {
|
|
187
|
+
"@type": "PostalAddress",
|
|
188
|
+
"addressLocality": "",
|
|
189
|
+
"addressCountry": ""
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"@type": "SoftwareApplication",
|
|
194
|
+
"@id": `${siteOrigin}/#application`,
|
|
195
|
+
"name": brandName,
|
|
196
|
+
"applicationCategory": "DeveloperApplication",
|
|
197
|
+
"softwareVersion": "v0.1.0",
|
|
198
|
+
"operatingSystem": "Cloudflare Workers, Node.js",
|
|
199
|
+
"description": metaDescription,
|
|
200
|
+
"url": siteOrigin,
|
|
201
|
+
"publisher": {
|
|
202
|
+
"@id": `${siteOrigin}/#organization`
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"@type": "WebSite",
|
|
207
|
+
"@id": `${siteOrigin}/#website`,
|
|
208
|
+
"url": siteOrigin,
|
|
209
|
+
"name": brandName,
|
|
210
|
+
"description": metaDescription,
|
|
211
|
+
"inLanguage": activeLanguage,
|
|
212
|
+
"publisher": {
|
|
213
|
+
"@id": `${siteOrigin}/#organization`
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"@type": "BreadcrumbList",
|
|
218
|
+
"@id": `${siteOrigin}${normalizedSlug ? `/${normalizedSlug}` : ''}/#breadcrumb`,
|
|
219
|
+
"itemListElement": breadcrumbListItems
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
<meta charset="utf-8" />
|
|
226
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
227
|
+
|
|
228
|
+
<title>{fullTitle}</title>
|
|
229
|
+
<meta name="description" content={metaDescription} />
|
|
230
|
+
|
|
231
|
+
<!-- 🎯 Channel Favicon -->
|
|
232
|
+
<link rel="icon" type={getFaviconType(faviconUrl)} href={faviconUrl} />
|
|
233
|
+
|
|
234
|
+
<link rel="canonical" href={canonicalURL} />
|
|
235
|
+
<link rel="author" type="text/plain" href="/llms.txt" title="LLM Directory" />
|
|
236
|
+
|
|
237
|
+
<!-- 🎯 Open Graph / Facebook -->
|
|
238
|
+
<meta property="og:type" content={isHomePage ? "website" : "article"} />
|
|
239
|
+
<meta property="og:url" content={canonicalURL} />
|
|
240
|
+
<meta property="og:title" content={fbTitle} />
|
|
241
|
+
<meta property="og:description" content={fbDescription} />
|
|
242
|
+
<meta property="og:image" content={fbImageUrl} />
|
|
243
|
+
<meta property="og:site_name" content={brandName} />
|
|
244
|
+
{ogLocale && <meta property="og:locale" content={ogLocale} />}
|
|
245
|
+
|
|
246
|
+
<!-- 🎯 X / Twitter -->
|
|
247
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
248
|
+
<meta name="twitter:url" content={canonicalURL} />
|
|
249
|
+
<meta name="twitter:title" content={xTitle} />
|
|
250
|
+
<meta name="twitter:description" content={xDescription} />
|
|
251
|
+
<meta name="twitter:image" content={xImageUrl} />
|
|
252
|
+
|
|
253
|
+
{lcpImageUrl && (
|
|
254
|
+
<link
|
|
255
|
+
rel="preload"
|
|
256
|
+
as="image"
|
|
257
|
+
href={lcpImageUrl}
|
|
258
|
+
fetchpriority="high"
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
<script
|
|
263
|
+
type="application/ld+json"
|
|
264
|
+
is:inline
|
|
265
|
+
set:html={JSON.stringify(structuredDataGraph)}
|
|
266
|
+
/>
|