@brightsideoy/kama 0.1.0-alpha.98 → 1.0.0-alpha.10
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 +726 -261
- package/content/definitions/blocks/button.yaml +34 -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/node-list.yaml +61 -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 +57 -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 +65 -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-Bzbme3Ob.css +1 -0
- package/dist/assets/index-DX98CiK-.js +164 -0
- package/dist/dist-CErcWOwp.js +27136 -0
- package/dist/index.html +4 -2
- package/dist/index.js +26159 -8575
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +23 -25
- package/schema.sql +67 -23
- 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 +143 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -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 +91 -0
- package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
- package/templates/astro-starter/src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/src/components/global/Header.astro +171 -0
- package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
- package/templates/astro-starter/src/lib/cms.ts +219 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
- package/templates/astro-starter/src/middleware.ts +508 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +318 -0
- package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -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 +104 -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,508 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
|
|
3
|
+
import { defineMiddleware } from "astro:middleware";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safely extracts environment variables across Astro 7, Node.js, and Cloudflare Workers
|
|
7
|
+
* without triggering the `@astrojs/cloudflare` runtime.env getter exception.
|
|
8
|
+
*/
|
|
9
|
+
async function getSafeEnv(locals: any): Promise<Record<string, any>> {
|
|
10
|
+
// 1. Try native Cloudflare Workers module (Astro 5+ / workerd)
|
|
11
|
+
try {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
const cf = await import("cloudflare:workers");
|
|
14
|
+
if (cf?.env) return cf.env;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// Not running inside a Cloudflare Worker container
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Safe check for locals.runtime / cfContext (Local Node.js / Hono)
|
|
20
|
+
const runtime = locals?.runtime || locals?.cfContext;
|
|
21
|
+
if (runtime && typeof runtime === "object") {
|
|
22
|
+
if ("CMS_WORKER" in runtime || "PUBLIC_API_URL" in runtime) {
|
|
23
|
+
return runtime;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Safely check property descriptor to avoid triggering throwing getters
|
|
27
|
+
try {
|
|
28
|
+
const desc = Object.getOwnPropertyDescriptor(runtime, "env");
|
|
29
|
+
if (desc && !desc.get) {
|
|
30
|
+
return runtime.env;
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Ignore getter throws
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Fallback for standalone Node.js mode
|
|
38
|
+
return typeof process !== "undefined" ? process.env || {} : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
42
|
+
const pathname = context.url.pathname;
|
|
43
|
+
const pathLower = pathname.toLowerCase();
|
|
44
|
+
|
|
45
|
+
// 🎯 Detect if we are previewing a draft or looking at a snapshot revision
|
|
46
|
+
const isPreview = context.url.searchParams.get("preview") === "true";
|
|
47
|
+
const hasRevision = context.url.searchParams.has("revisionId");
|
|
48
|
+
const isTimeTraveling = isPreview || hasRevision;
|
|
49
|
+
|
|
50
|
+
// 🎯 Detect Local Vite Dev Mode to bypass edge caching locally
|
|
51
|
+
const isDev = import.meta.env.DEV;
|
|
52
|
+
|
|
53
|
+
// 🎯 Safe dual-environment context extraction
|
|
54
|
+
const workerEnv = await getSafeEnv(context.locals);
|
|
55
|
+
|
|
56
|
+
// Attach to locals so Astro components (like Header.astro) don't have to re-evaluate it
|
|
57
|
+
(context.locals as any).safeEnv = workerEnv;
|
|
58
|
+
|
|
59
|
+
// 🎯 Safe ctx extraction (Fixing the runtime.ctx crash in Astro 6/7)
|
|
60
|
+
let cfContext = (context.locals as any).cfContext;
|
|
61
|
+
if (!cfContext) {
|
|
62
|
+
const runtime = (context.locals as any).runtime;
|
|
63
|
+
if (runtime && typeof runtime === "object") {
|
|
64
|
+
try {
|
|
65
|
+
const desc = Object.getOwnPropertyDescriptor(runtime, "ctx");
|
|
66
|
+
if (desc && !desc.get) {
|
|
67
|
+
cfContext = runtime.ctx;
|
|
68
|
+
}
|
|
69
|
+
} catch (e) { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
cfContext = cfContext || {};
|
|
73
|
+
|
|
74
|
+
const HTML_CACHE = workerEnv?.HTML_CACHE; // KV Namespace binding
|
|
75
|
+
|
|
76
|
+
// 🎯 0. INSTANT EDGE SPAM DROP (Blocks bots before hit to CMS worker/database)
|
|
77
|
+
const isSpam =
|
|
78
|
+
pathLower.includes("wp-") ||
|
|
79
|
+
pathLower.includes("wordpress") ||
|
|
80
|
+
pathLower.endsWith(".php") ||
|
|
81
|
+
pathLower === "/wp";
|
|
82
|
+
|
|
83
|
+
if (isSpam) {
|
|
84
|
+
return new Response("Not Found", { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING
|
|
88
|
+
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING WITH SURGICAL WORKER CACHING
|
|
89
|
+
if (pathname.startsWith("/media/")) {
|
|
90
|
+
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
91
|
+
// Construct a clean cache key ignoring session headers
|
|
92
|
+
const cacheKey = new Request(context.url.toString(), { method: "GET" });
|
|
93
|
+
|
|
94
|
+
// 1. Check surgical Worker Edge Cache for pre-encoded media (15ms HIT)
|
|
95
|
+
if (!isDev && cache) {
|
|
96
|
+
try {
|
|
97
|
+
const cachedResponse = await cache.match(cacheKey);
|
|
98
|
+
if (cachedResponse) {
|
|
99
|
+
return cachedResponse;
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Fall back to fetch if cache read fails
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Cold Fetch: Pass to CMS worker for AVIF transformation
|
|
107
|
+
const cmsWorker = workerEnv?.CMS_WORKER;
|
|
108
|
+
const envApiUrl = import.meta.env?.PUBLIC_API_URL;
|
|
109
|
+
const workerApiUrl = workerEnv?.PUBLIC_API_URL;
|
|
110
|
+
const baseApiUrl = String(envApiUrl || workerApiUrl || "https://kama.kamaengine.com");
|
|
111
|
+
const cleanApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl.slice(0, -1) : baseApiUrl;
|
|
112
|
+
|
|
113
|
+
const mediaUrl = `${cleanApiUrl}${pathname}${context.url.search}`;
|
|
114
|
+
const mediaReq = new Request(mediaUrl, { headers: context.request.headers });
|
|
115
|
+
|
|
116
|
+
let mediaResponse: Response;
|
|
117
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
118
|
+
mediaResponse = await cmsWorker.fetch(mediaReq);
|
|
119
|
+
} else {
|
|
120
|
+
mediaResponse = await fetch(mediaReq);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newHeaders = new Headers(mediaResponse.headers);
|
|
124
|
+
|
|
125
|
+
if (!isDev) {
|
|
126
|
+
newHeaders.set("Cache-Control", "public, max-age=31536000, s-maxage=31536000, immutable");
|
|
127
|
+
}
|
|
128
|
+
newHeaders.set("Access-Control-Allow-Origin", "*");
|
|
129
|
+
|
|
130
|
+
const finalResponse = new Response(mediaResponse.body, {
|
|
131
|
+
status: mediaResponse.status,
|
|
132
|
+
statusText: mediaResponse.statusText,
|
|
133
|
+
headers: newHeaders
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 3. Store ONLY successful image transformations in Worker Edge Cache
|
|
137
|
+
if (!isDev && cache && mediaResponse.status === 200) {
|
|
138
|
+
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
139
|
+
cfContext.waitUntil(cache.put(cacheKey, finalResponse.clone()));
|
|
140
|
+
} else {
|
|
141
|
+
cache.put(cacheKey, finalResponse.clone()).catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return finalResponse;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 🎯 0C. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
|
|
149
|
+
if (pathname === "/api/purge" || pathname === "/api/purge/") {
|
|
150
|
+
if (context.request.method !== "POST") {
|
|
151
|
+
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
152
|
+
status: 405,
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const authHeader = context.request.headers.get("Authorization");
|
|
158
|
+
const expectedToken = workerEnv.CLOUDFLARE_API_TOKEN;
|
|
159
|
+
|
|
160
|
+
if (!expectedToken || authHeader !== `Bearer ${expectedToken}`) {
|
|
161
|
+
return new Response(JSON.stringify({ error: "Unauthorized Token Mismatch" }), {
|
|
162
|
+
status: 401,
|
|
163
|
+
headers: { "Content-Type": "application/json" },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const body = await context.request.json();
|
|
169
|
+
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
170
|
+
const origin = context.url.origin;
|
|
171
|
+
|
|
172
|
+
// A. Purge Workers Caching Layer (Astro cfContext)
|
|
173
|
+
if (typeof cfContext?.cache?.purge === 'function') {
|
|
174
|
+
try {
|
|
175
|
+
// 🎯 FIX: Only execute global purge if the CMS explicitly requested it!
|
|
176
|
+
if (body.purge_everything) {
|
|
177
|
+
await cfContext.cache.purge({ purgeEverything: true });
|
|
178
|
+
console.log("✅ New Cloudflare Workers Cache Engine Purged (Global)");
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.error("❌ Failed to purge new Workers Cache Engine", e);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// B. Version-Based Instant Invalidation (0ms KV Write)
|
|
186
|
+
if (HTML_CACHE) {
|
|
187
|
+
try {
|
|
188
|
+
const now = Date.now().toString();
|
|
189
|
+
|
|
190
|
+
if (body.purge_everything) {
|
|
191
|
+
// 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
|
|
192
|
+
try {
|
|
193
|
+
await HTML_CACHE.put("site_version", now);
|
|
194
|
+
await HTML_CACHE.put("v:/", now);
|
|
195
|
+
console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
|
|
196
|
+
} catch (kvLimitErr: any) {
|
|
197
|
+
console.warn(`[KV Warning] Skipped global version bump: ${kvLimitErr.message}`);
|
|
198
|
+
}
|
|
199
|
+
} else if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
200
|
+
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps safely wrapped for quota
|
|
201
|
+
const purgeTasks = body.urls.map(async (targetUrl: string) => {
|
|
202
|
+
const urlPath = new URL(targetUrl).pathname;
|
|
203
|
+
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
204
|
+
try {
|
|
205
|
+
await HTML_CACHE.put(`v:${cleanPath}`, now);
|
|
206
|
+
} catch (kvLimitErr: any) {
|
|
207
|
+
console.warn(`[KV Warning] Skipped version tag for ${cleanPath}: ${kvLimitErr.message}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
await Promise.all(purgeTasks);
|
|
211
|
+
console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
|
|
212
|
+
// CMS CacheManager will automatically send the X-Cache-Warmer requests now!
|
|
213
|
+
}
|
|
214
|
+
} catch (kvErr) {
|
|
215
|
+
console.error("❌ KV Version Tagging Error:", kvErr);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// C. Global Legacy Worker Cache Purge
|
|
220
|
+
if (body.purge_everything) {
|
|
221
|
+
if (cache) {
|
|
222
|
+
const rootUrls = [
|
|
223
|
+
origin,
|
|
224
|
+
`${origin}/`
|
|
225
|
+
];
|
|
226
|
+
for (const url of rootUrls) {
|
|
227
|
+
await cache.delete(new Request(url));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new Response(JSON.stringify({
|
|
232
|
+
success: true,
|
|
233
|
+
message: "All Worker Caches Purged"
|
|
234
|
+
}), {
|
|
235
|
+
status: 200,
|
|
236
|
+
headers: { "Content-Type": "application/json" },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// D. Surgical Legacy Worker Cache Purge
|
|
241
|
+
if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
242
|
+
const results = [];
|
|
243
|
+
|
|
244
|
+
for (const targetUrl of body.urls) {
|
|
245
|
+
const cleanUrl = targetUrl.split('?')[0];
|
|
246
|
+
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
247
|
+
const urlWithSlash = urlNoSlash + "/";
|
|
248
|
+
const pathsToClear = [urlNoSlash, urlWithSlash];
|
|
249
|
+
|
|
250
|
+
let purged = false;
|
|
251
|
+
if (cache) {
|
|
252
|
+
for (const p of pathsToClear) {
|
|
253
|
+
const res = await cache.delete(new Request(p));
|
|
254
|
+
if (res) purged = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
results.push({ url: targetUrl, purged });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return new Response(JSON.stringify({
|
|
262
|
+
success: true,
|
|
263
|
+
message: "URLs processed and version tags updated",
|
|
264
|
+
results
|
|
265
|
+
}), {
|
|
266
|
+
status: 200,
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 🎯 E. SURGICAL DELETION FOR DELETED PAGES
|
|
272
|
+
if (Array.isArray(body.deleted_urls) && body.deleted_urls.length > 0) {
|
|
273
|
+
const results: any[] = [];
|
|
274
|
+
|
|
275
|
+
const deleteTasks = body.deleted_urls.map(async (targetUrl: string) => {
|
|
276
|
+
const urlPath = new URL(targetUrl).pathname;
|
|
277
|
+
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
278
|
+
|
|
279
|
+
let purgedKv = false;
|
|
280
|
+
|
|
281
|
+
if (HTML_CACHE) {
|
|
282
|
+
try {
|
|
283
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
284
|
+
HTML_CACHE.get("site_version"),
|
|
285
|
+
HTML_CACHE.get(`v:${cleanPath}`)
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
if (siteVer || pageVer) {
|
|
289
|
+
const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
290
|
+
await HTML_CACHE.delete(activeCacheKey);
|
|
291
|
+
purgedKv = true;
|
|
292
|
+
}
|
|
293
|
+
await HTML_CACHE.delete(`v:${cleanPath}`);
|
|
294
|
+
} catch (kvLimitErr: any) {
|
|
295
|
+
console.warn(`[KV Warning] Skipped deletion for ${cleanPath}: ${kvLimitErr.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const cleanUrl = targetUrl.split('?')[0];
|
|
300
|
+
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
301
|
+
const urlWithSlash = urlNoSlash + "/";
|
|
302
|
+
const pathsToClear = [urlNoSlash, urlWithSlash];
|
|
303
|
+
|
|
304
|
+
let purgedLegacy = false;
|
|
305
|
+
if (cache) {
|
|
306
|
+
for (const p of pathsToClear) {
|
|
307
|
+
const res = await cache.delete(new Request(p));
|
|
308
|
+
if (res) purgedLegacy = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
results.push({ url: targetUrl, purgedKv, purgedLegacy });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await Promise.all(deleteTasks);
|
|
316
|
+
|
|
317
|
+
return new Response(JSON.stringify({
|
|
318
|
+
success: true,
|
|
319
|
+
message: "Deleted URLs processed and KV keys permanently removed",
|
|
320
|
+
results
|
|
321
|
+
}), {
|
|
322
|
+
status: 200,
|
|
323
|
+
headers: { "Content-Type": "application/json" },
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return new Response(JSON.stringify({ error: "Invalid payload format" }), {
|
|
328
|
+
status: 400,
|
|
329
|
+
headers: { "Content-Type": "application/json" },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
} catch (err: any) {
|
|
333
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
334
|
+
status: 500,
|
|
335
|
+
headers: { "Content-Type": "application/json" },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 🎯 1. KV STATIC "FILE" CACHE READ WITH VERSION TAGGING
|
|
341
|
+
const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
|
|
342
|
+
const isHtmlPage = !pathname.includes('.') && !pathname.startsWith('/api/');
|
|
343
|
+
// 🎯 The ONLY way we refer to the homepage in KV is "/"
|
|
344
|
+
const cleanLookupPath = pathname === "/index" || pathname === "/index/"
|
|
345
|
+
? "/"
|
|
346
|
+
: (pathname.replace(/\/$/, "") || "/");
|
|
347
|
+
|
|
348
|
+
let activeCacheKey = cleanLookupPath;
|
|
349
|
+
|
|
350
|
+
if (!isDev && isHtmlPage && HTML_CACHE && !isWarmer && !isTimeTraveling) {
|
|
351
|
+
try {
|
|
352
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
353
|
+
HTML_CACHE.get("site_version"),
|
|
354
|
+
HTML_CACHE.get(`v:${cleanLookupPath}`)
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
// 🎯 Combine versions deterministically
|
|
358
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
359
|
+
|
|
360
|
+
const cachedData = await HTML_CACHE.get(activeCacheKey);
|
|
361
|
+
if (cachedData) {
|
|
362
|
+
let finalHtml = cachedData;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (cachedData.startsWith("{")) {
|
|
366
|
+
const parsed = JSON.parse(cachedData);
|
|
367
|
+
if (parsed.html) {
|
|
368
|
+
finalHtml = parsed.html;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch (jsonErr) {
|
|
372
|
+
// Fallback to raw HTML
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return new Response(finalHtml, {
|
|
376
|
+
status: 200,
|
|
377
|
+
headers: {
|
|
378
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
379
|
+
"X-Cache-Status": "HIT-KV-STATICFILE",
|
|
380
|
+
"X-Cache-Key": activeCacheKey,
|
|
381
|
+
// 🎯 ARCHITECTURE FIX: Prevent Cloudflare Edge from locking stale HTML in RAM.
|
|
382
|
+
// Force Edge to revalidate with Worker logic while keeping browser cache fresh.
|
|
383
|
+
"Cache-Control": "public, max-age=0, must-revalidate"
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} catch (e) {
|
|
388
|
+
console.warn("[Middleware] KV lookup bypass:", e);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Process downstream SSR routes
|
|
393
|
+
const response = await next();
|
|
394
|
+
|
|
395
|
+
// 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
|
|
396
|
+
if (
|
|
397
|
+
!isDev &&
|
|
398
|
+
isHtmlPage && HTML_CACHE &&
|
|
399
|
+
!isTimeTraveling &&
|
|
400
|
+
response.status === 200 &&
|
|
401
|
+
response.headers.get("content-type")?.includes("text/html")
|
|
402
|
+
) {
|
|
403
|
+
const htmlText = await response.clone().text();
|
|
404
|
+
|
|
405
|
+
// Extract the TTL injected by Astro SSR
|
|
406
|
+
const kvTtlHeader = response.headers.get("X-KV-Expiration");
|
|
407
|
+
const kvTtl = kvTtlHeader ? parseInt(kvTtlHeader, 10) : undefined;
|
|
408
|
+
|
|
409
|
+
// Clean up the internal header so it doesn't leak to the public browser
|
|
410
|
+
response.headers.delete("X-KV-Expiration");
|
|
411
|
+
|
|
412
|
+
const writeTask = (async () => {
|
|
413
|
+
if (activeCacheKey === cleanLookupPath || isWarmer) {
|
|
414
|
+
const forcedVersion = Date.now().toString();
|
|
415
|
+
try {
|
|
416
|
+
await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
|
|
417
|
+
const siteVer = await HTML_CACHE.get("site_version") || "1";
|
|
418
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
|
|
419
|
+
} catch (kvLimitErr: any) {
|
|
420
|
+
console.warn(`[KV Warning] Skipped prewarm version bump: ${kvLimitErr.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 🎯 Architecture Fix: Save TTL alongside HTML so CDN can properly cache reads
|
|
425
|
+
const storedTtl = kvTtl !== undefined ? Math.max(60, kvTtl) : 31536000;
|
|
426
|
+
const payload = JSON.stringify({ html: htmlText, ttl: storedTtl });
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
if (kvTtl !== undefined) {
|
|
430
|
+
await HTML_CACHE.put(activeCacheKey, payload, { expirationTtl: storedTtl });
|
|
431
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Expires in ${storedTtl}s)`);
|
|
432
|
+
} else {
|
|
433
|
+
await HTML_CACHE.put(activeCacheKey, payload);
|
|
434
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Infinite TTL)`);
|
|
435
|
+
}
|
|
436
|
+
} catch (kvLimitErr: any) {
|
|
437
|
+
console.warn(`[KV Warning] Skipped HTML cache save (Quota limit exceeded): ${kvLimitErr.message}`);
|
|
438
|
+
}
|
|
439
|
+
})();
|
|
440
|
+
|
|
441
|
+
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
442
|
+
cfContext.waitUntil(writeTask);
|
|
443
|
+
} else {
|
|
444
|
+
writeTask.catch((e: any) => console.error("KV write error:", e));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 🎯 3. SERVE LLMS & LLXML ENDPOINTS
|
|
449
|
+
if (pathname === "/llms.txt" || pathname === "/llms-full.txt") {
|
|
450
|
+
response.headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
451
|
+
response.headers.set("Cache-Control", "public, max-age=3600");
|
|
452
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
453
|
+
return response;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (pathname === "/llxml.txt") {
|
|
457
|
+
response.headers.set("Content-Type", "application/xml; charset=utf-8");
|
|
458
|
+
response.headers.set("Cache-Control", "public, max-age=3600");
|
|
459
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
460
|
+
return response;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 🎯 4. STATIC ASSET CACHING FOR COMPRESSED BUNDLES / ASSETS
|
|
464
|
+
if (!isDev) {
|
|
465
|
+
if (
|
|
466
|
+
pathname.startsWith("/_image") ||
|
|
467
|
+
pathname.startsWith("/cdn-cgi/image/") ||
|
|
468
|
+
pathname.match(/\.(png|jpg|jpeg|webp|avif|svg|gif|ico|woff2?|css|js)$/i)
|
|
469
|
+
) {
|
|
470
|
+
response.headers.set(
|
|
471
|
+
"Cache-Control",
|
|
472
|
+
"public, max-age=31536000, s-maxage=31536000, immutable"
|
|
473
|
+
);
|
|
474
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
475
|
+
return response;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 🎯 5. SECURITY HEADERS FOR HTML PAGES
|
|
480
|
+
if (response.headers.get("content-type")?.includes("text/html")) {
|
|
481
|
+
response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
|
|
482
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
483
|
+
|
|
484
|
+
// 🎯 Extract origin from env dynamically
|
|
485
|
+
const cmsUrl = workerEnv?.PUBLIC_CMS_URL || workerEnv?.PUBLIC_API_URL || "http://localhost:8787";
|
|
486
|
+
let cmsOrigin = "";
|
|
487
|
+
try {
|
|
488
|
+
cmsOrigin = new URL(cmsUrl).origin;
|
|
489
|
+
} catch (e) { }
|
|
490
|
+
|
|
491
|
+
if (isDev) {
|
|
492
|
+
response.headers.set("X-Frame-Options", "ALLOWALL");
|
|
493
|
+
response.headers.set(
|
|
494
|
+
"Content-Security-Policy",
|
|
495
|
+
"default-src 'self' http: https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; frame-ancestors *;"
|
|
496
|
+
);
|
|
497
|
+
} else {
|
|
498
|
+
// Production headers
|
|
499
|
+
response.headers.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
|
500
|
+
response.headers.set(
|
|
501
|
+
"Content-Security-Policy",
|
|
502
|
+
`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};`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return response;
|
|
508
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/pages/404.astro
|
|
3
|
+
import StandardLayout from "../layouts/StandardLayout.astro";
|
|
4
|
+
|
|
5
|
+
const node = {
|
|
6
|
+
title: "Page Not Found",
|
|
7
|
+
summary: "We couldn't find the page you were looking for.",
|
|
8
|
+
};
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<StandardLayout
|
|
12
|
+
node={node}
|
|
13
|
+
isHomePage={false}
|
|
14
|
+
hasSidebar={false}
|
|
15
|
+
content={{}}
|
|
16
|
+
normalizedSlug="404"
|
|
17
|
+
headerBlocks={[]}
|
|
18
|
+
>
|
|
19
|
+
<div class="error-page">
|
|
20
|
+
<h1>404</h1>
|
|
21
|
+
<h2>Page Not Found</h2>
|
|
22
|
+
<p>Sorry, the page you are looking for doesn't exist or has been moved.</p>
|
|
23
|
+
<a href="/" class="button">Go back home</a>
|
|
24
|
+
</div>
|
|
25
|
+
</StandardLayout>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.error-page {
|
|
29
|
+
text-align: center;
|
|
30
|
+
padding: 100px 20px;
|
|
31
|
+
}
|
|
32
|
+
.error-page h1 {
|
|
33
|
+
font-size: 6rem;
|
|
34
|
+
margin-bottom: 0;
|
|
35
|
+
color: #0284c7;
|
|
36
|
+
}
|
|
37
|
+
.button {
|
|
38
|
+
display: inline-block;
|
|
39
|
+
margin-top: 24px;
|
|
40
|
+
padding: 12px 24px;
|
|
41
|
+
background: #0f172a;
|
|
42
|
+
color: white;
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
}
|
|
46
|
+
</style>
|