@brightsideoy/kama 1.0.0-alpha.8 → 1.0.0-alpha.9
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/dist/assets/{index-Cw7t4FzH.css → index-BP3z9TWM.css} +1 -1
- package/dist/assets/{index-BoKRJcv3.js → index-Dxb_fjpM.js} +38 -38
- package/dist/{dist-KAwiRitk.js → dist-CUiHkSO8.js} +67 -51
- package/dist/index.html +2 -2
- package/dist/index.js +67 -51
- package/dist/schema-compiler.js +1 -1
- package/package.json +1 -1
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
- package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
- package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
- package/templates/astro-starter/src/pages/[...slug].astro +46 -47
|
@@ -20,7 +20,7 @@ interface Props {
|
|
|
20
20
|
const {
|
|
21
21
|
image = {},
|
|
22
22
|
sizes = "100vw",
|
|
23
|
-
desktopWidths = [768, 1352],
|
|
23
|
+
desktopWidths = [768, 1352, 1920, 2560],
|
|
24
24
|
idealWidth = 1352,
|
|
25
25
|
class: className = "",
|
|
26
26
|
loading = "lazy",
|
|
@@ -90,18 +90,26 @@ function buildAgnosticSrcset(
|
|
|
90
90
|
.join(", ");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// 🎯 High-DPI / Retina Optimization:
|
|
94
|
+
// - Mobile: Includes 320, 640, 768 (covers 2x and 3x Retina mobile displays)
|
|
95
|
+
// - Tablet: Includes 768, 1024, 1352 (covers iPad Pro Retina density)
|
|
96
|
+
// - Desktop: Uses desktopWidths (defaults to 768, 1352, 1920, 2560 for 1440p and 4K)
|
|
94
97
|
const mobileSrcset =
|
|
95
|
-
!isSvg && rawMobileUrl
|
|
98
|
+
!isSvg && rawMobileUrl
|
|
99
|
+
? buildAgnosticSrcset(rawMobileUrl, [320, 640, 768])
|
|
100
|
+
: null;
|
|
96
101
|
|
|
97
102
|
const tabletSrcset =
|
|
98
103
|
!isSvg && rawTabletUrl
|
|
99
|
-
? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
|
|
104
|
+
? buildAgnosticSrcset(rawTabletUrl, [768, 1024, 1352])
|
|
100
105
|
: null;
|
|
101
106
|
|
|
102
107
|
const desktopSrcset =
|
|
103
108
|
!isSvg && rawDesktopUrl
|
|
104
|
-
? buildAgnosticSrcset(
|
|
109
|
+
? buildAgnosticSrcset(
|
|
110
|
+
rawDesktopUrl,
|
|
111
|
+
desktopWidths.length ? desktopWidths : [768, 1352, 1920, 2560],
|
|
112
|
+
)
|
|
105
113
|
: null;
|
|
106
114
|
|
|
107
115
|
const fallbackUrl =
|
|
@@ -28,10 +28,21 @@ const kamaImageService: LocalImageService = {
|
|
|
28
28
|
|
|
29
29
|
const url = new URL(src, 'http://localhost');
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
if (options.
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// 🎯 Pass width if defined (snapped on backend by assets.ts)
|
|
32
|
+
if (options.width && !isNaN(options.width)) {
|
|
33
|
+
url.searchParams.set('w', Math.round(options.width).toString());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 🎯 Pass height only if explicitly provided to maintain intrinsic aspect ratios
|
|
37
|
+
if (options.height && !isNaN(options.height)) {
|
|
38
|
+
url.searchParams.set('h', Math.round(options.height).toString());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 🎯 Default quality to 75 if unsupplied
|
|
42
|
+
url.searchParams.set('q', (options.quality || 75).toString());
|
|
43
|
+
|
|
44
|
+
// 🎯 Default format to 'auto' for backend WebP/AVIF negotiation
|
|
45
|
+
url.searchParams.set('f', options.format ? options.format.toString() : 'auto');
|
|
35
46
|
|
|
36
47
|
return src.startsWith('http') ? url.toString() : (url.pathname + url.search);
|
|
37
48
|
} catch (e) {
|
|
@@ -28,12 +28,31 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
28
28
|
// 🎯 Detect if Astro is requesting a resized variant
|
|
29
29
|
const hasResizeParams = url.searchParams.has('w') || url.searchParams.has('h') || url.searchParams.has('q') || url.searchParams.has('f');
|
|
30
30
|
|
|
31
|
+
// 🎯 Resolve Negotiated Format for Edge Cache Key
|
|
32
|
+
const acceptHeader = request.headers.get('Accept') || '';
|
|
33
|
+
const fParam = url.searchParams.get('f');
|
|
34
|
+
let resolvedFormat = fParam && fParam !== 'auto' ? fParam : '';
|
|
35
|
+
|
|
36
|
+
if (!resolvedFormat && hasResizeParams) {
|
|
37
|
+
if (acceptHeader.includes('image/avif')) resolvedFormat = 'avif';
|
|
38
|
+
else if (acceptHeader.includes('image/webp')) resolvedFormat = 'webp';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 🎯 Construct a safe Cache Key that accounts for format negotiation
|
|
42
|
+
let cacheKeyUrl = request.url;
|
|
43
|
+
if (hasResizeParams && resolvedFormat && !fParam) {
|
|
44
|
+
const cacheUrlObj = new URL(cacheKeyUrl);
|
|
45
|
+
cacheUrlObj.searchParams.set('_resolvedFormat', resolvedFormat);
|
|
46
|
+
cacheKeyUrl = cacheUrlObj.toString();
|
|
47
|
+
}
|
|
48
|
+
const cacheKey = new Request(cacheKeyUrl, request);
|
|
49
|
+
|
|
31
50
|
// 1. Cloudflare Edge Cache API
|
|
32
51
|
let edgeCache: Cache | null = null;
|
|
33
52
|
try {
|
|
34
53
|
if (typeof caches !== "undefined" && caches.default) {
|
|
35
54
|
edgeCache = caches.default;
|
|
36
|
-
const cachedResponse = await edgeCache.match(
|
|
55
|
+
const cachedResponse = await edgeCache.match(cacheKey);
|
|
37
56
|
if (cachedResponse) return cachedResponse;
|
|
38
57
|
}
|
|
39
58
|
} catch (err) {
|
|
@@ -67,7 +86,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
67
86
|
const r2Response = new Response(r2Object.body, { status: 200, headers });
|
|
68
87
|
|
|
69
88
|
if (edgeCache) {
|
|
70
|
-
const cachePromise = edgeCache.put(
|
|
89
|
+
const cachePromise = edgeCache.put(cacheKey, r2Response.clone());
|
|
71
90
|
if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
|
|
72
91
|
else cachePromise.catch(e => console.warn(e));
|
|
73
92
|
}
|
|
@@ -112,7 +131,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
112
131
|
});
|
|
113
132
|
|
|
114
133
|
if (edgeCache) {
|
|
115
|
-
const cachePromise = edgeCache.put(
|
|
134
|
+
const cachePromise = edgeCache.put(cacheKey, proxyResponse.clone());
|
|
116
135
|
if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
|
|
117
136
|
else cachePromise.catch(e => console.warn(e));
|
|
118
137
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
// src/lib/media/media-url.ts
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes absolute CMS URLs into clean relative /media/... paths.
|
|
5
|
+
* Ensures CSP compliance by routing requests through the Astro frontend proxy.
|
|
6
|
+
*/
|
|
3
7
|
export function toRelativeMediaPath(rawUrl: string): string {
|
|
4
8
|
if (!rawUrl) return "";
|
|
5
9
|
|
|
6
|
-
let clean = String(rawUrl);
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
let clean = String(rawUrl).trim();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Parse absolute or relative URLs safely using a dummy origin fallback
|
|
14
|
+
const urlObj = new URL(clean, "http://localhost");
|
|
15
|
+
clean = urlObj.pathname;
|
|
16
|
+
} catch {
|
|
17
|
+
// Fallback cleanup for malformed strings
|
|
18
|
+
clean = clean.replace(/^https?:\/\/[^/]+/, "").split("?")[0].split("#")[0];
|
|
19
|
+
}
|
|
9
20
|
|
|
10
21
|
const mediaIndex = clean.indexOf("/media/");
|
|
11
22
|
if (mediaIndex !== -1) {
|
|
@@ -26,6 +37,9 @@ export function isSvgImage(urlOrMime: string): boolean {
|
|
|
26
37
|
return lower.endsWith('.svg') || lower.includes('image/svg+xml') || lower.includes('.svg?');
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Constructs an agnostic /media/... URL with standard optimization query parameters.
|
|
42
|
+
*/
|
|
29
43
|
export function buildAgnosticImageUrl(
|
|
30
44
|
url: string,
|
|
31
45
|
width: number,
|
|
@@ -187,6 +187,46 @@ const hasSidebar = sidebarBlocks.length > 0;
|
|
|
187
187
|
|
|
188
188
|
// Detect if this node is a standalone Page Skeleton
|
|
189
189
|
const isSkeleton = node?.nodeType === "page-skeleton";
|
|
190
|
+
|
|
191
|
+
// 🎯 HTML-OVER-THE-WIRE HOT SWAP LOGIC
|
|
192
|
+
// Isolated as a string so we don't violate DRY when injecting into multiple layouts
|
|
193
|
+
const hotSwapScript = `
|
|
194
|
+
if (window !== window.top) {
|
|
195
|
+
window.addEventListener("message", async event => {
|
|
196
|
+
if (event.data && event.data.type === "kama-preview-update") {
|
|
197
|
+
try {
|
|
198
|
+
const currentScrollY = window.scrollY;
|
|
199
|
+
const res = await fetch(event.data.url);
|
|
200
|
+
const newHtml = await res.text();
|
|
201
|
+
const newDoc = new DOMParser().parseFromString(newHtml, "text/html");
|
|
202
|
+
|
|
203
|
+
newDoc.body
|
|
204
|
+
.querySelectorAll("script, style, link, meta, noscript")
|
|
205
|
+
.forEach(el => el.remove());
|
|
206
|
+
|
|
207
|
+
const preserveTags = ["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT"];
|
|
208
|
+
|
|
209
|
+
Array.from(document.body.children).forEach(child => {
|
|
210
|
+
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
211
|
+
child.remove();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const referenceNode = document.body.firstChild;
|
|
216
|
+
Array.from(newDoc.body.children).forEach(child => {
|
|
217
|
+
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
218
|
+
document.body.insertBefore(child.cloneNode(true), referenceNode);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
window.scrollTo(0, currentScrollY);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error("[KAMA Preview] Failed to hot-swap HTML:", err);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
190
230
|
---
|
|
191
231
|
|
|
192
232
|
{
|
|
@@ -199,6 +239,9 @@ const isSkeleton = node?.nodeType === "page-skeleton";
|
|
|
199
239
|
isHomePage={isHomePage}
|
|
200
240
|
>
|
|
201
241
|
<BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
|
|
242
|
+
|
|
243
|
+
{/* 🎯 FIX: Script injected securely inside the layout ONLY during preview mode */}
|
|
244
|
+
{isPreview && <script is:inline set:html={hotSwapScript} />}
|
|
202
245
|
</SkeletonLayout>
|
|
203
246
|
) : (
|
|
204
247
|
<StandardLayout
|
|
@@ -258,53 +301,9 @@ const isSkeleton = node?.nodeType === "page-skeleton";
|
|
|
258
301
|
</aside>
|
|
259
302
|
)}
|
|
260
303
|
</div>
|
|
304
|
+
|
|
305
|
+
{/* 🎯 FIX: Script injected securely inside the layout ONLY during preview mode */}
|
|
306
|
+
{isPreview && <script is:inline set:html={hotSwapScript} />}
|
|
261
307
|
</StandardLayout>
|
|
262
308
|
)
|
|
263
309
|
}
|
|
264
|
-
<script is:inline>
|
|
265
|
-
// 🎯 HTML-Over-The-Wire Hot-Swap Logic (Vite & Astro Safe)
|
|
266
|
-
if (window !== window.top) {
|
|
267
|
-
window.addEventListener("message", async event => {
|
|
268
|
-
if (event.data && event.data.type === "kama-preview-update") {
|
|
269
|
-
try {
|
|
270
|
-
// 1. Remember scroll position
|
|
271
|
-
const currentScrollY = window.scrollY;
|
|
272
|
-
|
|
273
|
-
// 2. Fetch the newly saved draft HTML in the background
|
|
274
|
-
const res = await fetch(event.data.url);
|
|
275
|
-
const newHtml = await res.text();
|
|
276
|
-
const newDoc = new DOMParser().parseFromString(newHtml, "text/html");
|
|
277
|
-
|
|
278
|
-
// 🎯 CORE FIX: Strip all embedded scripts, styles, and Vite client links
|
|
279
|
-
// from the NEW document before they ever touch the live DOM.
|
|
280
|
-
// This stops the browser from aggressively re-fetching them and throwing ERR_ABORTED.
|
|
281
|
-
newDoc.body
|
|
282
|
-
.querySelectorAll("script, style, link, meta, noscript")
|
|
283
|
-
.forEach(el => el.remove());
|
|
284
|
-
|
|
285
|
-
const preserveTags = ["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT"];
|
|
286
|
-
|
|
287
|
-
// 3. Safely wipe the current body EXCEPT for our preserved assets
|
|
288
|
-
Array.from(document.body.children).forEach(child => {
|
|
289
|
-
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
290
|
-
child.remove();
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// 4. Inject the clean structural content
|
|
295
|
-
const referenceNode = document.body.firstChild;
|
|
296
|
-
Array.from(newDoc.body.children).forEach(child => {
|
|
297
|
-
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
298
|
-
document.body.insertBefore(child.cloneNode(true), referenceNode);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// 5. Restore scroll position instantly
|
|
303
|
-
window.scrollTo(0, currentScrollY);
|
|
304
|
-
} catch (err) {
|
|
305
|
-
console.error("[KAMA Preview] Failed to hot-swap HTML:", err);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
</script>
|