@brightsideoy/kama 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +604 -364
- package/content/definitions/block-kama-forms.yaml +5 -0
- package/content/definitions/blocks/button.yaml +15 -5
- package/content/definitions/blocks/image-gallery.yaml +34 -0
- package/content/definitions/blocks/node-list.yaml +75 -0
- package/content/definitions/blocks/youtube.yaml +8 -0
- package/content/definitions/field-sets/access.yaml +10 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/field-sets/page-base.yaml +16 -1
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/media-tag.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -0
- package/definitions/editor/default.ts +15 -1
- package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
- package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
- package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
- package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
- package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
- package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
- package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
- package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
- package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
- package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
- package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
- package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
- package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
- package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
- package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
- package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
- package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
- package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
- package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
- package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
- package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
- package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
- package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
- package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
- package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
- package/dist/assets/circle-check-A-QeFqt3.js +1 -0
- package/dist/assets/database-hg5SqKLG.js +1 -0
- package/dist/assets/folder-tree-BWYot72H.js +1 -0
- package/dist/assets/globe-BraoljaN.js +1 -0
- package/dist/assets/history-pqIbKK9u.js +1 -0
- package/dist/assets/index-CY0VYSyt.css +1 -0
- package/dist/assets/index-DdG9Pozx.js +14 -0
- package/dist/assets/layers-B6RgfpPR.js +1 -0
- package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
- package/dist/assets/monitor-CrpvInqG.js +1 -0
- package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
- package/dist/assets/plus-CX_GHzXs.js +1 -0
- package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
- package/dist/assets/save-Ctv8vW9H.js +1 -0
- package/dist/assets/schema-helpers-DvficKko.js +1 -0
- package/dist/assets/send-F-mJeL54.js +1 -0
- package/dist/assets/smartphone-EPce6VdP.js +1 -0
- package/dist/assets/trash-2-CAUF1KtI.js +1 -0
- package/dist/assets/useLocalStorage-C805uplI.js +1 -0
- package/dist/assets/useNodes-CqFlc72m.js +1 -0
- package/dist/assets/user-IFlT6mYq.js +1 -0
- package/dist/build-seed.js +495 -0
- package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
- package/dist/index.html +34 -2
- package/dist/index.js +3116 -1315
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +13 -6
- package/schema.sql +44 -1
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
- package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
- 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/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
- package/templates/astro-starter/src/lib/cms.ts +33 -11
- 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/middleware.ts +158 -100
- package/templates/astro-starter/src/pages/[...slug].astro +77 -3
- package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
- package/templates/kama-worker/index.js +38 -15
- package/templates/kama-worker/server.js +13 -5
- package/templates/kama-worker/setup-local-db.mjs +17 -0
- package/dist/assets/index-F2rfUymo.css +0 -1
- package/dist/assets/index-lXxH3VFK.js +0 -164
|
@@ -84,8 +84,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
84
84
|
return new Response("Not Found", { status: 404 });
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING
|
|
87
|
+
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING WITH SURGICAL WORKER CACHING
|
|
88
88
|
if (pathname.startsWith("/media/")) {
|
|
89
|
+
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
90
|
+
// Construct a clean cache key ignoring session headers
|
|
91
|
+
const cacheKey = new Request(context.url.toString(), { method: "GET" });
|
|
92
|
+
|
|
93
|
+
// 1. Check surgical Worker Edge Cache for pre-encoded media (15ms HIT)
|
|
94
|
+
if (!isDev && cache) {
|
|
95
|
+
try {
|
|
96
|
+
const cachedResponse = await cache.match(cacheKey);
|
|
97
|
+
if (cachedResponse) {
|
|
98
|
+
return cachedResponse;
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Fall back to fetch if cache read fails
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Cold Fetch: Pass to CMS worker for AVIF transformation
|
|
89
106
|
const cmsWorker = workerEnv?.CMS_WORKER;
|
|
90
107
|
const envApiUrl = import.meta.env?.PUBLIC_API_URL;
|
|
91
108
|
const workerApiUrl = workerEnv?.PUBLIC_API_URL;
|
|
@@ -93,12 +110,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
93
110
|
const cleanApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl.slice(0, -1) : baseApiUrl;
|
|
94
111
|
|
|
95
112
|
const mediaUrl = `${cleanApiUrl}${pathname}${context.url.search}`;
|
|
96
|
-
const mediaReq = new Request(mediaUrl, {
|
|
97
|
-
headers: context.request.headers
|
|
98
|
-
});
|
|
113
|
+
const mediaReq = new Request(mediaUrl, { headers: context.request.headers });
|
|
99
114
|
|
|
100
115
|
let mediaResponse: Response;
|
|
101
|
-
|
|
102
116
|
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
103
117
|
mediaResponse = await cmsWorker.fetch(mediaReq);
|
|
104
118
|
} else {
|
|
@@ -107,17 +121,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
107
121
|
|
|
108
122
|
const newHeaders = new Headers(mediaResponse.headers);
|
|
109
123
|
|
|
110
|
-
// 🎯 DEV FIX: Only apply 1-year cache on media in Production
|
|
111
124
|
if (!isDev) {
|
|
112
125
|
newHeaders.set("Cache-Control", "public, max-age=31536000, s-maxage=31536000, immutable");
|
|
113
126
|
}
|
|
114
127
|
newHeaders.set("Access-Control-Allow-Origin", "*");
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
const finalResponse = new Response(mediaResponse.body, {
|
|
117
130
|
status: mediaResponse.status,
|
|
118
131
|
statusText: mediaResponse.statusText,
|
|
119
132
|
headers: newHeaders
|
|
120
133
|
});
|
|
134
|
+
|
|
135
|
+
// 3. Store ONLY successful image transformations in Worker Edge Cache
|
|
136
|
+
if (!isDev && cache && mediaResponse.status === 200) {
|
|
137
|
+
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
138
|
+
cfContext.waitUntil(cache.put(cacheKey, finalResponse.clone()));
|
|
139
|
+
} else {
|
|
140
|
+
cache.put(cacheKey, finalResponse.clone()).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return finalResponse;
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
// 🎯 0C. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
|
|
@@ -130,7 +154,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
const authHeader = context.request.headers.get("Authorization");
|
|
133
|
-
const expectedToken = workerEnv.CLOUDFLARE_API_TOKEN;
|
|
157
|
+
const expectedToken = workerEnv.FRONTEND_API_SECRET || workerEnv.CLOUDFLARE_API_TOKEN;
|
|
134
158
|
|
|
135
159
|
if (!expectedToken || authHeader !== `Bearer ${expectedToken}`) {
|
|
136
160
|
return new Response(JSON.stringify({ error: "Unauthorized Token Mismatch" }), {
|
|
@@ -144,11 +168,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
144
168
|
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
145
169
|
const origin = context.url.origin;
|
|
146
170
|
|
|
171
|
+
if (body.purge_everything && !HTML_CACHE) {
|
|
172
|
+
return new Response(JSON.stringify({ success: false, error: "HTML_CACHE binding is unavailable" }), {
|
|
173
|
+
status: 503,
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
147
178
|
// A. Purge Workers Caching Layer (Astro cfContext)
|
|
148
179
|
if (typeof cfContext?.cache?.purge === 'function') {
|
|
149
180
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
// 🎯 FIX: Only execute global purge if the CMS explicitly requested it!
|
|
182
|
+
if (body.purge_everything) {
|
|
183
|
+
await cfContext.cache.purge({ purgeEverything: true });
|
|
184
|
+
console.log("✅ New Cloudflare Workers Cache Engine Purged (Global)");
|
|
185
|
+
}
|
|
152
186
|
} catch (e) {
|
|
153
187
|
console.error("❌ Failed to purge new Workers Cache Engine", e);
|
|
154
188
|
}
|
|
@@ -161,30 +195,34 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
161
195
|
|
|
162
196
|
if (body.purge_everything) {
|
|
163
197
|
// 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
|
|
198
|
+
try {
|
|
199
|
+
await HTML_CACHE.put("site_version", now);
|
|
200
|
+
await HTML_CACHE.put("v:/", now);
|
|
201
|
+
console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
|
|
202
|
+
} catch (kvLimitErr: any) {
|
|
203
|
+
console.error(`[KV Error] Global version bump failed: ${kvLimitErr.message}`);
|
|
204
|
+
return new Response(JSON.stringify({ success: false, error: "Failed to invalidate HTML KV cache" }), {
|
|
205
|
+
status: 503,
|
|
206
|
+
headers: { "Content-Type": "application/json" },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} else if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
210
|
+
// Shared fragments and cross-page content depend on the site version too.
|
|
164
211
|
await HTML_CACHE.put("site_version", now);
|
|
165
212
|
|
|
166
|
-
// 🎯
|
|
167
|
-
|
|
168
|
-
await HTML_CACHE.put("v:/index", now);
|
|
169
|
-
|
|
170
|
-
console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
|
|
171
|
-
} else if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
172
|
-
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps
|
|
173
|
-
const purgeTasks = [];
|
|
174
|
-
for (const targetUrl of body.urls) {
|
|
213
|
+
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps safely wrapped for quota
|
|
214
|
+
const purgeTasks = body.urls.map(async (targetUrl: string) => {
|
|
175
215
|
const urlPath = new URL(targetUrl).pathname;
|
|
176
216
|
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (cleanPath === "/" || cleanPath === "/index") {
|
|
182
|
-
purgeTasks.push(HTML_CACHE.put("v:/", now));
|
|
183
|
-
purgeTasks.push(HTML_CACHE.put("v:/index", now));
|
|
217
|
+
try {
|
|
218
|
+
await HTML_CACHE.put(`v:${cleanPath}`, now);
|
|
219
|
+
} catch (kvLimitErr: any) {
|
|
220
|
+
console.warn(`[KV Warning] Skipped version tag for ${cleanPath}: ${kvLimitErr.message}`);
|
|
184
221
|
}
|
|
185
|
-
}
|
|
222
|
+
});
|
|
186
223
|
await Promise.all(purgeTasks);
|
|
187
224
|
console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
|
|
225
|
+
// CMS CacheManager will automatically send the X-Cache-Warmer requests now!
|
|
188
226
|
}
|
|
189
227
|
} catch (kvErr) {
|
|
190
228
|
console.error("❌ KV Version Tagging Error:", kvErr);
|
|
@@ -196,11 +234,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
196
234
|
if (cache) {
|
|
197
235
|
const rootUrls = [
|
|
198
236
|
origin,
|
|
199
|
-
`${origin}
|
|
200
|
-
`${origin}/index`,
|
|
201
|
-
`${origin}/index/`
|
|
237
|
+
`${origin}/`
|
|
202
238
|
];
|
|
203
|
-
|
|
204
239
|
for (const url of rootUrls) {
|
|
205
240
|
await cache.delete(new Request(url));
|
|
206
241
|
}
|
|
@@ -215,7 +250,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
215
250
|
});
|
|
216
251
|
}
|
|
217
252
|
|
|
218
|
-
// D. Surgical Legacy Worker Cache Purge
|
|
253
|
+
// D. Surgical Legacy Worker Cache Purge
|
|
219
254
|
if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
220
255
|
const results = [];
|
|
221
256
|
|
|
@@ -223,17 +258,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
223
258
|
const cleanUrl = targetUrl.split('?')[0];
|
|
224
259
|
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
225
260
|
const urlWithSlash = urlNoSlash + "/";
|
|
226
|
-
|
|
227
261
|
const pathsToClear = [urlNoSlash, urlWithSlash];
|
|
228
|
-
const urlPath = new URL(targetUrl).pathname;
|
|
229
|
-
|
|
230
|
-
// 🎯 FIX: Clear legacy cache alias if it's the home page
|
|
231
|
-
if (urlPath === "/" || urlPath === "/index") {
|
|
232
|
-
pathsToClear.push(`${origin}/`);
|
|
233
|
-
pathsToClear.push(`${origin}/index`);
|
|
234
|
-
pathsToClear.push(`${origin}/index/`);
|
|
235
|
-
pathsToClear.push(`${origin}`);
|
|
236
|
-
}
|
|
237
262
|
|
|
238
263
|
let purged = false;
|
|
239
264
|
if (cache) {
|
|
@@ -267,37 +292,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
267
292
|
let purgedKv = false;
|
|
268
293
|
|
|
269
294
|
if (HTML_CACHE) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (cleanPath === "/" || cleanPath === "/index") {
|
|
285
|
-
await HTML_CACHE.delete("v:/");
|
|
286
|
-
await HTML_CACHE.delete("v:/index");
|
|
295
|
+
try {
|
|
296
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
297
|
+
HTML_CACHE.get("site_version"),
|
|
298
|
+
HTML_CACHE.get(`v:${cleanPath}`)
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
if (siteVer || pageVer) {
|
|
302
|
+
const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
303
|
+
await HTML_CACHE.delete(activeCacheKey);
|
|
304
|
+
purgedKv = true;
|
|
305
|
+
}
|
|
306
|
+
await HTML_CACHE.delete(`v:${cleanPath}`);
|
|
307
|
+
} catch (kvLimitErr: any) {
|
|
308
|
+
console.warn(`[KV Warning] Skipped deletion for ${cleanPath}: ${kvLimitErr.message}`);
|
|
287
309
|
}
|
|
288
310
|
}
|
|
289
311
|
|
|
290
312
|
const cleanUrl = targetUrl.split('?')[0];
|
|
291
313
|
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
292
314
|
const urlWithSlash = urlNoSlash + "/";
|
|
293
|
-
|
|
294
315
|
const pathsToClear = [urlNoSlash, urlWithSlash];
|
|
295
|
-
if (urlPath === "/" || urlPath === "/index") {
|
|
296
|
-
pathsToClear.push(`${origin}/`);
|
|
297
|
-
pathsToClear.push(`${origin}/index`);
|
|
298
|
-
pathsToClear.push(`${origin}/index/`);
|
|
299
|
-
pathsToClear.push(`${origin}`);
|
|
300
|
-
}
|
|
301
316
|
|
|
302
317
|
let purgedLegacy = false;
|
|
303
318
|
if (cache) {
|
|
@@ -337,32 +352,60 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
337
352
|
|
|
338
353
|
// 🎯 1. KV STATIC "FILE" CACHE READ WITH VERSION TAGGING
|
|
339
354
|
const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
|
|
340
|
-
|
|
341
|
-
|
|
355
|
+
|
|
356
|
+
// 🎯 ARCHITECTURE FIX: Allow HTML partials to be edge-cached alongside regular HTML
|
|
357
|
+
const isHtmlPage = !pathname.includes('.') && (!pathname.startsWith('/api/') || pathname.startsWith('/api/partials/'));
|
|
358
|
+
|
|
359
|
+
// 🎯 ARCHITECTURE FIX: Include query string for partials so offset=6 and offset=12 have distinct cache keys
|
|
360
|
+
const cleanLookupPath = pathname === "/index" || pathname === "/index/"
|
|
361
|
+
? "/"
|
|
362
|
+
: (pathname.replace(/\/$/, "") || "/") + (pathname.startsWith('/api/partials/') ? context.url.search : "");
|
|
342
363
|
|
|
343
364
|
let activeCacheKey = cleanLookupPath;
|
|
344
365
|
|
|
345
|
-
// 🎯 FIX:
|
|
346
|
-
if (!isDev && isHtmlPage && HTML_CACHE && !
|
|
366
|
+
// 🎯 FIX: Retrieve the activeCacheKey versions even if it's a warmer request, so it doesn't overwrite them!
|
|
367
|
+
if (!isDev && isHtmlPage && HTML_CACHE && !isTimeTraveling) {
|
|
347
368
|
try {
|
|
369
|
+
// 🎯 Partials inherit the global site_version so they automatically purge when the CMS publishes
|
|
370
|
+
const pageVersionKey = pathname.startsWith('/api/partials/') ? "site_version" : `v:${pathname.replace(/\/$/, "") || "/"}`;
|
|
371
|
+
|
|
348
372
|
const [siteVer, pageVer] = await Promise.all([
|
|
349
373
|
HTML_CACHE.get("site_version"),
|
|
350
|
-
HTML_CACHE.get(
|
|
374
|
+
HTML_CACHE.get(pageVersionKey)
|
|
351
375
|
]);
|
|
352
376
|
|
|
377
|
+
// 🎯 Combine versions deterministically
|
|
353
378
|
activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
354
379
|
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"
|
|
363
|
-
|
|
380
|
+
// 🎯 Only serve cached HTML if it is NOT a warmer request
|
|
381
|
+
if (!isWarmer) {
|
|
382
|
+
const cachedData = await HTML_CACHE.get(activeCacheKey);
|
|
383
|
+
if (cachedData) {
|
|
384
|
+
let finalHtml = cachedData;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
if (cachedData.startsWith("{")) {
|
|
388
|
+
const parsed = JSON.parse(cachedData);
|
|
389
|
+
if (parsed.html) {
|
|
390
|
+
finalHtml = parsed.html;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (jsonErr) {
|
|
394
|
+
// Fallback to raw HTML
|
|
364
395
|
}
|
|
365
|
-
|
|
396
|
+
|
|
397
|
+
return new Response(finalHtml, {
|
|
398
|
+
status: 200,
|
|
399
|
+
headers: {
|
|
400
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
401
|
+
"X-Cache-Status": "HIT-KV-STATICFILE",
|
|
402
|
+
"X-Cache-Key": activeCacheKey,
|
|
403
|
+
// Prevent Cloudflare Edge from locking stale HTML in RAM.
|
|
404
|
+
// Force Edge to revalidate with Worker logic while keeping browser cache fresh.
|
|
405
|
+
"Cache-Control": "public, max-age=0, must-revalidate"
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
366
409
|
}
|
|
367
410
|
} catch (e) {
|
|
368
411
|
console.warn("[Middleware] KV lookup bypass:", e);
|
|
@@ -373,36 +416,54 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
373
416
|
const response = await next();
|
|
374
417
|
|
|
375
418
|
// 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
|
|
376
|
-
// 🎯 FIX: Bypass KV Write in local dev OR if we are time traveling (never save drafts to public cache)
|
|
377
419
|
if (
|
|
378
420
|
!isDev &&
|
|
379
|
-
isHtmlPage &&
|
|
380
|
-
HTML_CACHE &&
|
|
421
|
+
isHtmlPage && HTML_CACHE &&
|
|
381
422
|
!isTimeTraveling &&
|
|
382
423
|
response.status === 200 &&
|
|
383
424
|
response.headers.get("content-type")?.includes("text/html")
|
|
384
425
|
) {
|
|
385
426
|
const htmlText = await response.clone().text();
|
|
386
427
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
428
|
+
// Extract the TTL injected by Astro SSR
|
|
429
|
+
const kvTtlHeader = response.headers.get("X-KV-Expiration");
|
|
430
|
+
const kvTtl = kvTtlHeader ? parseInt(kvTtlHeader, 10) : undefined;
|
|
390
431
|
|
|
391
|
-
|
|
432
|
+
// Clean up the internal header so it doesn't leak to the public browser
|
|
433
|
+
response.headers.delete("X-KV-Expiration");
|
|
392
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
const writeTask = (async () => {
|
|
436
|
+
// 🎯 FIX: Removed `|| isWarmer` here so warmer requests safely write to the existing activeCacheKey
|
|
437
|
+
if (activeCacheKey === cleanLookupPath) {
|
|
438
|
+
const forcedVersion = Date.now().toString();
|
|
439
|
+
try {
|
|
440
|
+
// 🎯 FIX: Do NOT overwrite global site_version from a frontend partial cache miss
|
|
441
|
+
if (!pathname.startsWith('/api/partials/')) {
|
|
442
|
+
await HTML_CACHE.put(`v:${pathname.replace(/\/$/, "") || "/"}`, forcedVersion);
|
|
443
|
+
}
|
|
444
|
+
const siteVer = await HTML_CACHE.get("site_version") || "1";
|
|
445
|
+
const pageVer = pathname.startsWith('/api/partials/') ? siteVer : forcedVersion;
|
|
446
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${pageVer}`;
|
|
447
|
+
} catch (kvLimitErr: any) {
|
|
448
|
+
console.warn(`[KV Warning] Skipped prewarm version bump: ${kvLimitErr.message}`);
|
|
397
449
|
}
|
|
450
|
+
}
|
|
398
451
|
|
|
399
|
-
|
|
452
|
+
// Save TTL alongside HTML so CDN can properly cache reads
|
|
453
|
+
const storedTtl = kvTtl !== undefined ? Math.max(60, kvTtl) : 31536000;
|
|
454
|
+
const payload = JSON.stringify({ html: htmlText, ttl: storedTtl });
|
|
400
455
|
|
|
401
|
-
|
|
456
|
+
try {
|
|
457
|
+
if (kvTtl !== undefined) {
|
|
458
|
+
await HTML_CACHE.put(activeCacheKey, payload, { expirationTtl: storedTtl });
|
|
459
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Expires in ${storedTtl}s)`);
|
|
460
|
+
} else {
|
|
461
|
+
await HTML_CACHE.put(activeCacheKey, payload);
|
|
462
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Infinite TTL)`);
|
|
463
|
+
}
|
|
464
|
+
} catch (kvLimitErr: any) {
|
|
465
|
+
console.warn(`[KV Warning] Skipped HTML cache save (Quota limit exceeded): ${kvLimitErr.message}`);
|
|
402
466
|
}
|
|
403
|
-
|
|
404
|
-
await HTML_CACHE.put(activeCacheKey, htmlText);
|
|
405
|
-
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey}`);
|
|
406
467
|
})();
|
|
407
468
|
|
|
408
469
|
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
@@ -428,7 +489,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
428
489
|
}
|
|
429
490
|
|
|
430
491
|
// 🎯 4. STATIC ASSET CACHING FOR COMPRESSED BUNDLES / ASSETS
|
|
431
|
-
// 🎯 DEV FIX: Do not apply max-age=31536000 locally to prevent breaking Vite HMR
|
|
432
492
|
if (!isDev) {
|
|
433
493
|
if (
|
|
434
494
|
pathname.startsWith("/_image") ||
|
|
@@ -465,8 +525,6 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
465
525
|
} else {
|
|
466
526
|
// Production headers
|
|
467
527
|
response.headers.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
|
468
|
-
// 🎯 FIX: Remove X-Frame-Options entirely since it breaks cross-subdomain framing.
|
|
469
|
-
// Rely completely on CSP frame-ancestors to whitelist ALL kamaengine subdomains + your env URL.
|
|
470
528
|
response.headers.set(
|
|
471
529
|
"Content-Security-Policy",
|
|
472
530
|
`default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data: blob:; connect-src 'self' https:; frame-ancestors 'self' https://*.kamaengine.com https://kama.kamaengine.com ${cmsOrigin};`
|
|
@@ -59,6 +59,10 @@ const envApiUrl = import.meta.env.PUBLIC_API_URL;
|
|
|
59
59
|
const workerApiUrl = safeEnv.PUBLIC_API_URL;
|
|
60
60
|
const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
|
|
61
61
|
|
|
62
|
+
// 🎯 Safely extract the secret across both environments
|
|
63
|
+
const frontendApiSecret =
|
|
64
|
+
import.meta.env.FRONTEND_API_SECRET || safeEnv.FRONTEND_API_SECRET;
|
|
65
|
+
|
|
62
66
|
const cleanApiUrl = baseApiUrl.endsWith("/")
|
|
63
67
|
? baseApiUrl.slice(0, -1)
|
|
64
68
|
: baseApiUrl;
|
|
@@ -83,10 +87,13 @@ const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
83
87
|
...init?.headers,
|
|
84
88
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
85
89
|
Pragma: "no-cache",
|
|
86
|
-
// 🎯 Direct Service Bindings require explicit header flags to bypass CMS KV cache!
|
|
87
90
|
"X-Bypass-Cache": "true",
|
|
88
91
|
};
|
|
89
92
|
|
|
93
|
+
if (frontendApiSecret) {
|
|
94
|
+
reqHeaders["Authorization"] = `Bearer ${frontendApiSecret}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
const fetchOpts: RequestInit = {
|
|
91
98
|
...init,
|
|
92
99
|
headers: reqHeaders,
|
|
@@ -107,7 +114,9 @@ const treeId = getTreeId();
|
|
|
107
114
|
const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
|
|
108
115
|
const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
|
|
109
116
|
const requestedSlug = pathParts.join("/");
|
|
110
|
-
|
|
117
|
+
|
|
118
|
+
// 🎯 ONLY FIX NEEDED HERE: Pass '/' if empty so home route resolves, otherwise requestedSlug
|
|
119
|
+
const targetSlug = requestedSlug || "/";
|
|
111
120
|
|
|
112
121
|
let pageData: NodeResponse | null = null;
|
|
113
122
|
let navTree: TreeNode[] = [];
|
|
@@ -129,7 +138,7 @@ try {
|
|
|
129
138
|
]);
|
|
130
139
|
|
|
131
140
|
navTree = treeResult || [];
|
|
132
|
-
pageData = nodeResult;
|
|
141
|
+
pageData = nodeResult as NodeResponse | null;
|
|
133
142
|
} catch (err) {
|
|
134
143
|
console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
|
|
135
144
|
}
|
|
@@ -141,12 +150,35 @@ if (!pageData || !pageData.node) {
|
|
|
141
150
|
const node = pageData.node;
|
|
142
151
|
const content = pageData.content || {};
|
|
143
152
|
|
|
153
|
+
// 🎯 SMART EDGE CACHING LOGIC (API-Driven TTL)
|
|
154
|
+
let cacheTtl = pageData?.meta?.cacheTtl;
|
|
155
|
+
|
|
156
|
+
if (cacheTtl === undefined || cacheTtl === null) {
|
|
157
|
+
cacheTtl = 31536000; // Default: 1 Year (Forever)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cacheTtl = Math.max(60, cacheTtl);
|
|
161
|
+
|
|
162
|
+
if (isTimeTraveling) {
|
|
163
|
+
Astro.response.headers.set(
|
|
164
|
+
"Cache-Control",
|
|
165
|
+
"no-cache, no-store, must-revalidate",
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
Astro.response.headers.set(
|
|
169
|
+
"Cache-Control",
|
|
170
|
+
`public, max-age=0, s-maxage=${cacheTtl}, must-revalidate`,
|
|
171
|
+
);
|
|
172
|
+
Astro.response.headers.set("X-KV-Expiration", cacheTtl.toString());
|
|
173
|
+
}
|
|
174
|
+
|
|
144
175
|
const rootTreeSlug =
|
|
145
176
|
navTree && navTree.length > 0
|
|
146
177
|
? navTree[0].slug || (navTree[0] as any).key
|
|
147
178
|
: "";
|
|
148
179
|
|
|
149
180
|
const isHomePage =
|
|
181
|
+
targetSlug === "/" ||
|
|
150
182
|
targetSlug === homeSlug ||
|
|
151
183
|
(rootTreeSlug && targetSlug === rootTreeSlug) ||
|
|
152
184
|
node?.path === "/" ||
|
|
@@ -161,6 +193,44 @@ const hasSidebar = sidebarBlocks.length > 0;
|
|
|
161
193
|
|
|
162
194
|
// Detect if this node is a standalone Page Skeleton
|
|
163
195
|
const isSkeleton = node?.nodeType === "page-skeleton";
|
|
196
|
+
|
|
197
|
+
const hotSwapScript = `
|
|
198
|
+
if (window !== window.top) {
|
|
199
|
+
window.addEventListener("message", async event => {
|
|
200
|
+
if (event.data && event.data.type === "kama-preview-update") {
|
|
201
|
+
try {
|
|
202
|
+
const currentScrollY = window.scrollY;
|
|
203
|
+
const res = await fetch(event.data.url);
|
|
204
|
+
const newHtml = await res.text();
|
|
205
|
+
const newDoc = new DOMParser().parseFromString(newHtml, "text/html");
|
|
206
|
+
|
|
207
|
+
newDoc.body
|
|
208
|
+
.querySelectorAll("script, style, link, meta, noscript")
|
|
209
|
+
.forEach(el => el.remove());
|
|
210
|
+
|
|
211
|
+
const preserveTags = ["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT"];
|
|
212
|
+
|
|
213
|
+
Array.from(document.body.children).forEach(child => {
|
|
214
|
+
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
215
|
+
child.remove();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const referenceNode = document.body.firstChild;
|
|
220
|
+
Array.from(newDoc.body.children).forEach(child => {
|
|
221
|
+
if (!preserveTags.includes(child.tagName.toUpperCase())) {
|
|
222
|
+
document.body.insertBefore(child.cloneNode(true), referenceNode);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
window.scrollTo(0, currentScrollY);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error("[KAMA Preview] Failed to hot-swap HTML:", err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
164
234
|
---
|
|
165
235
|
|
|
166
236
|
{
|
|
@@ -173,6 +243,8 @@ const isSkeleton = node?.nodeType === "page-skeleton";
|
|
|
173
243
|
isHomePage={isHomePage}
|
|
174
244
|
>
|
|
175
245
|
<BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
|
|
246
|
+
|
|
247
|
+
{isPreview && <script is:inline set:html={hotSwapScript} />}
|
|
176
248
|
</SkeletonLayout>
|
|
177
249
|
) : (
|
|
178
250
|
<StandardLayout
|
|
@@ -232,6 +304,8 @@ const isSkeleton = node?.nodeType === "page-skeleton";
|
|
|
232
304
|
</aside>
|
|
233
305
|
)}
|
|
234
306
|
</div>
|
|
307
|
+
|
|
308
|
+
{isPreview && <script is:inline set:html={hotSwapScript} />}
|
|
235
309
|
</StandardLayout>
|
|
236
310
|
)
|
|
237
311
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/pages/api/load-nodes.ts
|
|
2
|
+
import type { APIRoute } from "astro";
|
|
3
|
+
|
|
4
|
+
export const prerender = false; // Required to ensure this runs server-side on Cloudflare
|
|
5
|
+
|
|
6
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
const searchParams = url.searchParams;
|
|
9
|
+
|
|
10
|
+
// 1. Get Environment Variables securely via middleware
|
|
11
|
+
const cfEnv = (locals as any).safeEnv || {};
|
|
12
|
+
|
|
13
|
+
const cmsWorker = cfEnv.CMS_WORKER;
|
|
14
|
+
const apiUrl = cfEnv.PUBLIC_API_URL || import.meta.env.PUBLIC_API_URL || "http://localhost:8787";
|
|
15
|
+
const secret = cfEnv.FRONTEND_API_SECRET || import.meta.env.FRONTEND_API_SECRET;
|
|
16
|
+
|
|
17
|
+
if (!secret) {
|
|
18
|
+
console.error("🚨 [Load Nodes Proxy] Critical Error: FRONTEND_API_SECRET is missing from the environment!");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Construct the actual backend URL
|
|
22
|
+
const cleanApiUrl = apiUrl.replace(/\/$/, "");
|
|
23
|
+
const backendUrl = `${cleanApiUrl}/api/v1/nodes?${searchParams.toString()}`;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 3. Prepare the secure headers
|
|
27
|
+
const reqHeaders: Record<string, string> = {
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (secret) {
|
|
32
|
+
reqHeaders["Authorization"] = `Bearer ${secret}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const backendReq = new Request(backendUrl, {
|
|
36
|
+
method: "GET",
|
|
37
|
+
headers: reqHeaders
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 4. Fetch from the CMS securely (Bypass Cloudflare Access via Service Binding)
|
|
41
|
+
let response;
|
|
42
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
43
|
+
response = await cmsWorker.fetch(backendReq);
|
|
44
|
+
} else {
|
|
45
|
+
response = await fetch(backendReq);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
console.error(`[Load Nodes Proxy] Backend rejected request with status: ${response.status}`);
|
|
50
|
+
return new Response(JSON.stringify({ error: "Failed to fetch nodes" }), { status: response.status });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
|
|
55
|
+
// 5. Return to the browser
|
|
56
|
+
return new Response(JSON.stringify(data), {
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
// 🎯 ARCHITECTURE FIX: Cache at the Cloudflare Edge for 60 seconds to eliminate redundant DB queries.
|
|
61
|
+
"Cache-Control": "public, max-age=0, s-maxage=60, stale-while-revalidate=120"
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("[Load Nodes Proxy] Failed to load nodes:", err);
|
|
66
|
+
return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500 });
|
|
67
|
+
}
|
|
68
|
+
};
|