@brightsideoy/kama 0.1.0-alpha.113 → 0.1.0-alpha.115
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 +172 -379
- package/content/definitions/blocks/button.yaml +7 -8
- package/content/definitions/blocks/header.yaml +7 -9
- package/content/definitions/blocks/header_image.yaml +14 -15
- package/content/definitions/blocks/hero.yaml +22 -21
- package/content/definitions/blocks/image_card.yaml +46 -47
- package/content/definitions/blocks/single_image.yaml +19 -20
- package/content/definitions/blocks/text.yaml +8 -9
- package/content/definitions/channels/ecommerce.yaml +14 -16
- package/content/definitions/channels/website.yaml +20 -21
- package/content/definitions/field-sets/access.yaml +3 -5
- package/content/definitions/field-sets/channel-base.yaml +25 -25
- package/content/definitions/field-sets/content-block.yaml +36 -41
- package/content/definitions/field-sets/page-base.yaml +40 -45
- package/content/definitions/field-sets/page-meta.yaml +1 -4
- package/content/definitions/field-sets/page-social.yaml +6 -8
- package/content/definitions/field-sets/tree-base.yaml +25 -25
- package/content/definitions/nodes/address.yaml +29 -21
- package/content/definitions/nodes/article.yaml +24 -19
- package/content/definitions/nodes/event.yaml +32 -26
- package/content/definitions/nodes/external-link.yaml +20 -17
- package/content/definitions/nodes/page.yaml +38 -35
- package/content/definitions/nodes/person.yaml +29 -27
- package/content/definitions/nodes/product.yaml +52 -49
- package/content/definitions/trees/navigation.yaml +26 -21
- package/content/definitions/trees/social-links.yaml +5 -2
- package/dist/assets/index-8T39DEP3.js +162 -0
- package/dist/assets/index-B9oh2_TL.css +1 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6492 -4249
- package/package.json +5 -6
- package/schema.sql +5 -13
- package/templates/astro-starter/astro.config.mjs +40 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +125 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
- package/templates/astro-starter/src/components/global/Footer.astro +48 -0
- package/templates/astro-starter/src/components/global/Header.astro +92 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +148 -0
- package/templates/astro-starter/src/kama-image-service.ts +41 -0
- package/templates/astro-starter/src/layouts/Layout.astro +280 -0
- package/templates/astro-starter/src/lib/cms.ts +149 -0
- package/templates/astro-starter/src/lib/media-url.ts +34 -0
- package/templates/astro-starter/src/middleware.ts +306 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +234 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/test-local.sh +76 -0
- package/dist/assets/index-10GEw0KY.css +0 -1
- package/dist/assets/index-THJ6BqPP.js +0 -1394
- package/kama-worker/bundled-definitions.js +0 -2394
- package/kama-worker/bundled-definitions.ts +0 -2395
- package/scripts/bundle-definitions.js +0 -224
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/layouts/Layout.astro
|
|
3
|
+
import Header from "@/components/global/Header.astro";
|
|
4
|
+
import Footer from "@/components/global/Footer.astro";
|
|
5
|
+
import type { TreeNode, ContentBlock } from "@/lib/cms";
|
|
6
|
+
// 🎯 FIX: Import from pure TypeScript utility, NOT from KamaImage.astro
|
|
7
|
+
import { buildAgnosticImageUrl } from "@/lib/media-url";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
node: Record<string, any>;
|
|
11
|
+
content: Record<string, ContentBlock[]>;
|
|
12
|
+
normalizedSlug: string;
|
|
13
|
+
isHomePage: boolean;
|
|
14
|
+
hasSidebar?: boolean;
|
|
15
|
+
headerBlocks?: ContentBlock[];
|
|
16
|
+
tree?: TreeNode[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
node = {},
|
|
21
|
+
content = {},
|
|
22
|
+
normalizedSlug,
|
|
23
|
+
isHomePage,
|
|
24
|
+
headerBlocks = [],
|
|
25
|
+
tree
|
|
26
|
+
} = Astro.props;
|
|
27
|
+
|
|
28
|
+
const footerBlocks = content?.footer || [];
|
|
29
|
+
|
|
30
|
+
// 🎯 Detect LCP image candidate to preload immediately in <head>
|
|
31
|
+
let lcpImageUrl = "";
|
|
32
|
+
const firstHeaderBlock = headerBlocks && headerBlocks[0];
|
|
33
|
+
const firstContentBlock = content?.content && content.content[0];
|
|
34
|
+
|
|
35
|
+
const candidateBlock = (firstHeaderBlock?.data?.image ? firstHeaderBlock : null)
|
|
36
|
+
|| (firstContentBlock?.data?.image ? firstContentBlock : null);
|
|
37
|
+
|
|
38
|
+
if (candidateBlock?.data?.image) {
|
|
39
|
+
const candidateImg = candidateBlock.data.image;
|
|
40
|
+
const rawUrl = candidateImg.variants?.standard?.url || candidateImg.url || candidateImg.path;
|
|
41
|
+
if (rawUrl) {
|
|
42
|
+
lcpImageUrl = buildAgnosticImageUrl(rawUrl, 1352, 75);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const siteOrigin = import.meta.env.PUBLIC_SITE_URL || Astro.site?.origin || "https://example.com";
|
|
47
|
+
const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
|
|
48
|
+
const agencyName = import.meta.env.PUBLIC_AGENCY_NAME || "My Company Name";
|
|
49
|
+
const brandLogo = `${siteOrigin}/logo.png`;
|
|
50
|
+
|
|
51
|
+
const prefix = node?.titlePrefix?.trim() ? `${node.titlePrefix.trim()} ` : "";
|
|
52
|
+
const suffix = node?.titleSuffix?.trim() ? ` ${node.titleSuffix.trim()}` : "";
|
|
53
|
+
const baseTitle = node?.title?.trim() || brandName;
|
|
54
|
+
const fullTitle = `${prefix}${baseTitle}${suffix}`;
|
|
55
|
+
|
|
56
|
+
const canonicalURL = new URL(Astro.url.pathname, siteOrigin).href;
|
|
57
|
+
|
|
58
|
+
const breadcrumbListItems = [
|
|
59
|
+
{
|
|
60
|
+
"@type": "ListItem",
|
|
61
|
+
"position": 1,
|
|
62
|
+
"name": "Home",
|
|
63
|
+
"item": siteOrigin,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
if (!isHomePage && normalizedSlug) {
|
|
68
|
+
const segments = normalizedSlug.split("/").filter(Boolean);
|
|
69
|
+
let currentPath = "";
|
|
70
|
+
|
|
71
|
+
segments.forEach((segment, index) => {
|
|
72
|
+
currentPath += `/${segment}`;
|
|
73
|
+
const isLast = index === segments.length - 1;
|
|
74
|
+
|
|
75
|
+
const fallbackTitle = segment
|
|
76
|
+
.replace(/-/g, " ")
|
|
77
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
78
|
+
|
|
79
|
+
const segmentTitle = isLast && node?.title ? node.title : fallbackTitle;
|
|
80
|
+
|
|
81
|
+
breadcrumbListItems.push({
|
|
82
|
+
"@type": "ListItem",
|
|
83
|
+
"position": index + 2,
|
|
84
|
+
"name": segmentTitle,
|
|
85
|
+
"item": `${siteOrigin}${currentPath}`,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const structuredDataGraph = {
|
|
91
|
+
"@context": "https://schema.org",
|
|
92
|
+
"@graph": [
|
|
93
|
+
{
|
|
94
|
+
"@type": "Organization",
|
|
95
|
+
"@id": `${siteOrigin}/#organization`,
|
|
96
|
+
"name": agencyName,
|
|
97
|
+
"url": siteOrigin,
|
|
98
|
+
"logo": {
|
|
99
|
+
"@type": "ImageObject",
|
|
100
|
+
"url": brandLogo,
|
|
101
|
+
"caption": agencyName
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"@type": "SoftwareApplication",
|
|
106
|
+
"@id": `${siteOrigin}/#application`,
|
|
107
|
+
"name": brandName,
|
|
108
|
+
"applicationCategory": "WebApplication",
|
|
109
|
+
"operatingSystem": "Cloudflare Workers",
|
|
110
|
+
"publisher": {
|
|
111
|
+
"@id": `${siteOrigin}/#organization`
|
|
112
|
+
},
|
|
113
|
+
"description": "Corporate Web Management built with KAMA Engine, Astro, and Cloudflare Edge infrastructure."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"@type": "WebSite",
|
|
117
|
+
"@id": `${siteOrigin}/#website`,
|
|
118
|
+
"url": siteOrigin,
|
|
119
|
+
"name": brandName,
|
|
120
|
+
"publisher": {
|
|
121
|
+
"@id": `${siteOrigin}/#organization`
|
|
122
|
+
},
|
|
123
|
+
"potentialAction": {
|
|
124
|
+
"@type": "SearchAction",
|
|
125
|
+
"target": {
|
|
126
|
+
"@type": "EntryPoint",
|
|
127
|
+
"urlTemplate": `${siteOrigin}/search?q={search_term_string}`
|
|
128
|
+
},
|
|
129
|
+
"query-input": "required name=search_term_string"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"@type": "BreadcrumbList",
|
|
134
|
+
"@id": `${siteOrigin}${normalizedSlug ? `/${normalizedSlug}` : ''}/#breadcrumb`,
|
|
135
|
+
"itemListElement": breadcrumbListItems
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="utf-8" />
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
145
|
+
|
|
146
|
+
<title>{fullTitle}</title>
|
|
147
|
+
|
|
148
|
+
<meta
|
|
149
|
+
name="description"
|
|
150
|
+
content={node?.summary || `${brandName} - Corporate Web Application`}
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
<link rel="canonical" href={canonicalURL} />
|
|
154
|
+
|
|
155
|
+
{lcpImageUrl && (
|
|
156
|
+
<link
|
|
157
|
+
rel="preload"
|
|
158
|
+
as="image"
|
|
159
|
+
href={lcpImageUrl}
|
|
160
|
+
fetchpriority="high"
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
<script
|
|
165
|
+
type="application/ld+json"
|
|
166
|
+
is:inline
|
|
167
|
+
set:html={JSON.stringify(structuredDataGraph)}
|
|
168
|
+
/>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<Header node={node} tree={tree} />
|
|
172
|
+
|
|
173
|
+
<main class="content-block">
|
|
174
|
+
<div class="content-container">
|
|
175
|
+
<slot />
|
|
176
|
+
</div>
|
|
177
|
+
</main>
|
|
178
|
+
|
|
179
|
+
<Footer blocks={footerBlocks} contentMap={content} />
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|
|
182
|
+
|
|
183
|
+
<style is:global>
|
|
184
|
+
*,
|
|
185
|
+
*::before,
|
|
186
|
+
*::after {
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
border-radius: 0 !important;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
html {
|
|
192
|
+
font-size: 16px;
|
|
193
|
+
-webkit-text-size-adjust: 100%;
|
|
194
|
+
-webkit-font-smoothing: antialiased;
|
|
195
|
+
-moz-osx-font-smoothing: grayscale;
|
|
196
|
+
text-rendering: optimizeLegibility;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
body {
|
|
200
|
+
margin: 0;
|
|
201
|
+
padding: 0;
|
|
202
|
+
font-family:
|
|
203
|
+
system-ui,
|
|
204
|
+
-apple-system,
|
|
205
|
+
BlinkMacSystemFont,
|
|
206
|
+
"Segoe UI",
|
|
207
|
+
Roboto,
|
|
208
|
+
Helvetica,
|
|
209
|
+
Arial,
|
|
210
|
+
sans-serif;
|
|
211
|
+
font-display: swap;
|
|
212
|
+
background-color: #ffffff;
|
|
213
|
+
color: #1e293b;
|
|
214
|
+
min-height: 100vh;
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
line-height: 1.65;
|
|
218
|
+
letter-spacing: -0.011em;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
h1,
|
|
222
|
+
h2,
|
|
223
|
+
h3,
|
|
224
|
+
h4,
|
|
225
|
+
h5,
|
|
226
|
+
h6 {
|
|
227
|
+
color: #0f172a;
|
|
228
|
+
font-weight: 700;
|
|
229
|
+
line-height: 1.25;
|
|
230
|
+
letter-spacing: -0.022em;
|
|
231
|
+
margin-top: 1.6em;
|
|
232
|
+
margin-bottom: 0.6em;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
h1 {
|
|
236
|
+
font-size: 2.25rem;
|
|
237
|
+
}
|
|
238
|
+
h2 {
|
|
239
|
+
font-size: 1.65rem;
|
|
240
|
+
}
|
|
241
|
+
h3 {
|
|
242
|
+
font-size: 1.35rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
p {
|
|
246
|
+
margin-top: 0;
|
|
247
|
+
margin-bottom: 1.25em;
|
|
248
|
+
color: #334155;
|
|
249
|
+
font-size: 1.03rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
a {
|
|
253
|
+
color: #0284c7;
|
|
254
|
+
text-decoration-thickness: 1px;
|
|
255
|
+
text-underline-offset: 3px;
|
|
256
|
+
transition: color 0.15s ease;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
a:hover {
|
|
260
|
+
color: #0369a1;
|
|
261
|
+
}
|
|
262
|
+
li>p:last-child {
|
|
263
|
+
margin-bottom: 0;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
266
|
+
|
|
267
|
+
<style>
|
|
268
|
+
.content-block {
|
|
269
|
+
flex: 1;
|
|
270
|
+
padding: 40px 0 64px 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.content-container {
|
|
274
|
+
max-width: 1400px;
|
|
275
|
+
margin: 0 auto;
|
|
276
|
+
padding: 0 24px;
|
|
277
|
+
width: 100%;
|
|
278
|
+
box-sizing: border-box;
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ContentBlock {
|
|
19
|
+
instanceId: number;
|
|
20
|
+
componentId: string;
|
|
21
|
+
data: Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RegionMeta {
|
|
25
|
+
isInherited: boolean;
|
|
26
|
+
inheritedFromId?: string | number;
|
|
27
|
+
inheritedFromTitle?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NodeResponse {
|
|
31
|
+
channelId: string | number;
|
|
32
|
+
treeId: string | number;
|
|
33
|
+
language: string;
|
|
34
|
+
node: {
|
|
35
|
+
id: number;
|
|
36
|
+
title: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
nodeType: string;
|
|
39
|
+
path?: string;
|
|
40
|
+
createdAt?: string;
|
|
41
|
+
updatedAt?: string;
|
|
42
|
+
summary?: string;
|
|
43
|
+
children?: any[];
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
};
|
|
46
|
+
content: Record<string, ContentBlock[]>;
|
|
47
|
+
regionMeta?: Record<string, RegionMeta>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves base CMS URL across runtime environments safely via Astro
|
|
52
|
+
*/
|
|
53
|
+
export function getCmsUrl(): string {
|
|
54
|
+
const url =
|
|
55
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
56
|
+
import.meta.env.PUBLIC_CMS_URL ||
|
|
57
|
+
'http://localhost:8787';
|
|
58
|
+
return url.replace(/\/+$/, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolves Default Channel ID across environments
|
|
63
|
+
*/
|
|
64
|
+
export function getChannelId(): string {
|
|
65
|
+
return import.meta.env.PUBLIC_CHANNEL_ID || 'website';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves Default Tree ID across environments
|
|
70
|
+
*/
|
|
71
|
+
export function getTreeId(): string {
|
|
72
|
+
return import.meta.env.PUBLIC_TREE_ID || 'navigation';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch tree navigation hierarchy by tree key/ID and channel.
|
|
77
|
+
* Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
|
|
78
|
+
*/
|
|
79
|
+
export async function getTree(
|
|
80
|
+
treeKeyOrId: string = getTreeId(),
|
|
81
|
+
channel: string | number = getChannelId(),
|
|
82
|
+
customFetch: typeof fetch = fetch
|
|
83
|
+
): Promise<TreeNode[]> {
|
|
84
|
+
const baseUrl = getCmsUrl();
|
|
85
|
+
const requestUrl = `${baseUrl}/api/v1/tree/${treeKeyOrId}?channelId=${channel}`;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const res = await customFetch(requestUrl, {
|
|
89
|
+
headers: {
|
|
90
|
+
Accept: 'application/json',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!res.ok) return [];
|
|
95
|
+
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
return data.items || [];
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[CMS Client] Failed to fetch public tree:', err);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch single content node payload by its full URL path.
|
|
106
|
+
* Supports passing Cloudflare Service Bindings (e.g. env.CMS_WORKER.fetch)
|
|
107
|
+
*/
|
|
108
|
+
export async function getNodeByPath(
|
|
109
|
+
path: string,
|
|
110
|
+
channel: string | number = getChannelId(),
|
|
111
|
+
preview = false,
|
|
112
|
+
customFetch: typeof fetch = fetch
|
|
113
|
+
): Promise<NodeResponse | null> {
|
|
114
|
+
const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'home';
|
|
115
|
+
const baseUrl = getCmsUrl();
|
|
116
|
+
const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const res = await customFetch(requestUrl, {
|
|
120
|
+
headers: {
|
|
121
|
+
Accept: 'application/json',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!res.ok) return null;
|
|
126
|
+
|
|
127
|
+
return (await res.json()) as NodeResponse;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[CMS Client] Failed to fetch node by path:', err);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolves relative media URLs against base CMS URL
|
|
136
|
+
*/
|
|
137
|
+
export function getMediaUrl(url?: string): string {
|
|
138
|
+
if (!url) return '';
|
|
139
|
+
|
|
140
|
+
const baseUrl = getCmsUrl();
|
|
141
|
+
let cleanUrl = url.replace(/^http:\/\/localhost:\d+/, baseUrl);
|
|
142
|
+
|
|
143
|
+
if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) {
|
|
144
|
+
return cleanUrl;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cleanPath = cleanUrl.replace(/^\/+/, '');
|
|
148
|
+
return `${baseUrl}/${cleanPath}`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/lib/media-url.ts
|
|
2
|
+
|
|
3
|
+
export function toRelativeMediaPath(rawUrl: string): string {
|
|
4
|
+
if (!rawUrl) return "";
|
|
5
|
+
|
|
6
|
+
let clean = String(rawUrl);
|
|
7
|
+
clean = clean.replace(/^https?:\/\/[^/]+/, "");
|
|
8
|
+
clean = clean.split("?")[0].split("#")[0];
|
|
9
|
+
|
|
10
|
+
const mediaIndex = clean.indexOf("/media/");
|
|
11
|
+
if (mediaIndex !== -1) {
|
|
12
|
+
return `/media/${clean
|
|
13
|
+
.slice(mediaIndex + "/media/".length)
|
|
14
|
+
.replace(/^\/+/, "")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return clean.startsWith("/") ? clean : `/${clean}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildAgnosticImageUrl(
|
|
21
|
+
url: string,
|
|
22
|
+
width: number,
|
|
23
|
+
quality = 75,
|
|
24
|
+
): string {
|
|
25
|
+
const cleanPath = toRelativeMediaPath(url);
|
|
26
|
+
if (!cleanPath) return "";
|
|
27
|
+
|
|
28
|
+
const urlObj = new URL(cleanPath, "http://localhost");
|
|
29
|
+
urlObj.searchParams.set("w", String(width));
|
|
30
|
+
urlObj.searchParams.set("q", String(quality));
|
|
31
|
+
urlObj.searchParams.set("f", "auto");
|
|
32
|
+
|
|
33
|
+
return urlObj.pathname + urlObj.search;
|
|
34
|
+
}
|