@brightsideoy/kama 0.1.0-alpha.149 → 0.1.0-alpha.151
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 +58 -21
- package/content/definitions/blocks/html.yaml +1 -0
- package/dist/assets/index-BJ80E0Sz.css +1 -0
- package/dist/assets/index-DknACOqv.js +162 -0
- package/dist/{dist-a3ACJkRE.js → dist-DvbQty0F.js} +34 -31
- package/dist/index.html +4 -2
- package/dist/index.js +34 -31
- package/dist/schema-compiler.js +1 -1
- package/package.json +1 -1
- package/schema.sql +1 -0
- package/templates/astro-starter/src/middleware.ts +293 -0
- package/templates/astro-starter/src/pages/[...slug].astro +79 -39
- package/dist/assets/index-BSBvGvQz.css +0 -1
- package/dist/assets/index-ckG1oyv-.js +0 -162
- package/templates/astro-starter/middleware/api-purge.ts +0 -130
- package/templates/astro-starter/middleware/index.ts +0 -10
- package/templates/astro-starter/middleware/kv-cache.ts +0 -76
- package/templates/astro-starter/middleware/response-headers.ts +0 -43
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { defineMiddleware } from "astro:middleware";
|
|
2
|
+
import { env } from "cloudflare:workers";
|
|
3
|
+
|
|
4
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
5
|
+
const pathname = context.url.pathname;
|
|
6
|
+
const cfContext = (context.locals as any).cfContext;
|
|
7
|
+
const workerEnv = cfContext?.env || (env as any);
|
|
8
|
+
const HTML_CACHE = workerEnv?.HTML_CACHE; // KV Namespace binding
|
|
9
|
+
|
|
10
|
+
// 🎯 0. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
|
|
11
|
+
if (pathname === "/api/purge" || pathname === "/api/purge/") {
|
|
12
|
+
if (context.request.method !== "POST") {
|
|
13
|
+
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
14
|
+
status: 405,
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const authHeader = context.request.headers.get("Authorization");
|
|
20
|
+
const expectedToken = workerEnv.CLOUDFLARE_API_TOKEN;
|
|
21
|
+
|
|
22
|
+
if (!expectedToken || authHeader !== `Bearer ${expectedToken}`) {
|
|
23
|
+
return new Response(JSON.stringify({ error: "Unauthorized Token Mismatch" }), {
|
|
24
|
+
status: 401,
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const body = await context.request.json();
|
|
31
|
+
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
32
|
+
const origin = context.url.origin;
|
|
33
|
+
|
|
34
|
+
// A. Purge Workers Caching Layer (Astro cfContext)
|
|
35
|
+
if (typeof cfContext?.cache?.purge === 'function') {
|
|
36
|
+
try {
|
|
37
|
+
await cfContext.cache.purge({ purgeEverything: true });
|
|
38
|
+
console.log("✅ New Cloudflare Workers Cache Engine Purged");
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("❌ Failed to purge new Workers Cache Engine", e);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// B. Version-Based Instant Invalidation (0ms KV Write)
|
|
45
|
+
if (HTML_CACHE) {
|
|
46
|
+
try {
|
|
47
|
+
const now = Date.now().toString();
|
|
48
|
+
|
|
49
|
+
if (body.purge_everything) {
|
|
50
|
+
// 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
|
|
51
|
+
await HTML_CACHE.put("site_version", now);
|
|
52
|
+
console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
|
|
53
|
+
} else if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
54
|
+
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps
|
|
55
|
+
const purgeTasks = body.urls.map((targetUrl: string) => {
|
|
56
|
+
const urlPath = new URL(targetUrl).pathname;
|
|
57
|
+
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
58
|
+
return HTML_CACHE.put(`v:${cleanPath}`, now);
|
|
59
|
+
});
|
|
60
|
+
await Promise.all(purgeTasks);
|
|
61
|
+
console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
|
|
62
|
+
}
|
|
63
|
+
} catch (kvErr) {
|
|
64
|
+
console.error("❌ KV Version Tagging Error:", kvErr);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// C. Global Legacy Worker Cache Purge
|
|
69
|
+
if (body.purge_everything) {
|
|
70
|
+
if (cache) {
|
|
71
|
+
const rootUrls = [
|
|
72
|
+
origin,
|
|
73
|
+
`${origin}/`,
|
|
74
|
+
`${origin}/home`,
|
|
75
|
+
`${origin}/home/`
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const url of rootUrls) {
|
|
79
|
+
await cache.delete(new Request(url));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Response(JSON.stringify({
|
|
84
|
+
success: true,
|
|
85
|
+
message: "All Worker Caches Purged"
|
|
86
|
+
}), {
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// D. Surgical Legacy Worker Cache Purge (For Updated Pages)
|
|
93
|
+
if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
94
|
+
const results = [];
|
|
95
|
+
|
|
96
|
+
for (const targetUrl of body.urls) {
|
|
97
|
+
const cleanUrl = targetUrl.split('?')[0];
|
|
98
|
+
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
99
|
+
const urlWithSlash = urlNoSlash + "/";
|
|
100
|
+
|
|
101
|
+
let p1 = false, p2 = false;
|
|
102
|
+
if (cache) {
|
|
103
|
+
p1 = await cache.delete(new Request(urlNoSlash));
|
|
104
|
+
p2 = await cache.delete(new Request(urlWithSlash));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
results.push({ url: targetUrl, purged: p1 || p2 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return new Response(JSON.stringify({
|
|
111
|
+
success: true,
|
|
112
|
+
message: "URLs processed and version tags updated",
|
|
113
|
+
results
|
|
114
|
+
}), {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 🎯 E. SURGICAL DELETION FOR DELETED PAGES
|
|
121
|
+
if (Array.isArray(body.deleted_urls) && body.deleted_urls.length > 0) {
|
|
122
|
+
const results: any[] = [];
|
|
123
|
+
|
|
124
|
+
const deleteTasks = body.deleted_urls.map(async (targetUrl: string) => {
|
|
125
|
+
const urlPath = new URL(targetUrl).pathname;
|
|
126
|
+
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
127
|
+
|
|
128
|
+
let purgedKv = false;
|
|
129
|
+
|
|
130
|
+
if (HTML_CACHE) {
|
|
131
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
132
|
+
HTML_CACHE.get("site_version"),
|
|
133
|
+
HTML_CACHE.get(`v:${cleanPath}`)
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
if (siteVer || pageVer) {
|
|
137
|
+
const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
138
|
+
await HTML_CACHE.delete(activeCacheKey);
|
|
139
|
+
purgedKv = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await HTML_CACHE.delete(`v:${cleanPath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cleanUrl = targetUrl.split('?')[0];
|
|
146
|
+
const urlNoSlash = cleanUrl.replace(/\/$/, "");
|
|
147
|
+
const urlWithSlash = urlNoSlash + "/";
|
|
148
|
+
|
|
149
|
+
let p1 = false, p2 = false;
|
|
150
|
+
if (cache) {
|
|
151
|
+
p1 = await cache.delete(new Request(urlNoSlash));
|
|
152
|
+
p2 = await cache.delete(new Request(urlWithSlash));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
results.push({ url: targetUrl, purgedKv, purgedLegacy: p1 || p2 });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await Promise.all(deleteTasks);
|
|
159
|
+
|
|
160
|
+
return new Response(JSON.stringify({
|
|
161
|
+
success: true,
|
|
162
|
+
message: "Deleted URLs processed and KV keys permanently removed",
|
|
163
|
+
results
|
|
164
|
+
}), {
|
|
165
|
+
status: 200,
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return new Response(JSON.stringify({ error: "Invalid payload format" }), {
|
|
171
|
+
status: 400,
|
|
172
|
+
headers: { "Content-Type": "application/json" },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
177
|
+
status: 500,
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 🎯 1. KV STATIC "FILE" CACHE READ WITH VERSION TAGGING
|
|
184
|
+
const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
|
|
185
|
+
const isHtmlPage = !pathname.includes('.') && !pathname.startsWith('/api/');
|
|
186
|
+
const cleanLookupPath = pathname.replace(/\/$/, "") || "/";
|
|
187
|
+
|
|
188
|
+
let activeCacheKey = cleanLookupPath;
|
|
189
|
+
|
|
190
|
+
if (isHtmlPage && HTML_CACHE && !isWarmer) {
|
|
191
|
+
try {
|
|
192
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
193
|
+
HTML_CACHE.get("site_version"),
|
|
194
|
+
HTML_CACHE.get(`v:${cleanLookupPath}`)
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
198
|
+
|
|
199
|
+
const cachedHtml = await HTML_CACHE.get(activeCacheKey);
|
|
200
|
+
if (cachedHtml) {
|
|
201
|
+
return new Response(cachedHtml, {
|
|
202
|
+
status: 200,
|
|
203
|
+
headers: {
|
|
204
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
205
|
+
"X-Cache-Status": "HIT-KV-STATICFILE",
|
|
206
|
+
"X-Cache-Key": activeCacheKey,
|
|
207
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn("[Middleware] KV lookup bypass:", e);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Process downstream SSR routes
|
|
217
|
+
const response = await next();
|
|
218
|
+
|
|
219
|
+
// 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
|
|
220
|
+
if (
|
|
221
|
+
isHtmlPage &&
|
|
222
|
+
HTML_CACHE &&
|
|
223
|
+
response.status === 200 &&
|
|
224
|
+
response.headers.get("content-type")?.includes("text/html")
|
|
225
|
+
) {
|
|
226
|
+
const htmlText = await response.clone().text();
|
|
227
|
+
|
|
228
|
+
const writeTask = (async () => {
|
|
229
|
+
if (activeCacheKey === cleanLookupPath || isWarmer) {
|
|
230
|
+
const forcedVersion = Date.now().toString();
|
|
231
|
+
|
|
232
|
+
await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
|
|
233
|
+
|
|
234
|
+
const siteVer = await HTML_CACHE.get("site_version") || "1";
|
|
235
|
+
|
|
236
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await HTML_CACHE.put(activeCacheKey, htmlText);
|
|
240
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey}`);
|
|
241
|
+
})();
|
|
242
|
+
|
|
243
|
+
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
244
|
+
cfContext.waitUntil(writeTask);
|
|
245
|
+
} else {
|
|
246
|
+
writeTask.catch((e: any) => console.error("KV write error:", e));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 🎯 3. SERVE LLMS & LLXML ENDPOINTS
|
|
251
|
+
if (pathname === "/llms.txt" || pathname === "/llms-full.txt") {
|
|
252
|
+
response.headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
253
|
+
response.headers.set("Cache-Control", "public, max-age=3600");
|
|
254
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (pathname === "/llxml.txt") {
|
|
259
|
+
response.headers.set("Content-Type", "application/xml; charset=utf-8");
|
|
260
|
+
response.headers.set("Cache-Control", "public, max-age=3600");
|
|
261
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
262
|
+
return response;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 🎯 4. IMAGE & STATIC ASSET CACHING
|
|
266
|
+
if (
|
|
267
|
+
pathname.startsWith("/media/") ||
|
|
268
|
+
pathname.startsWith("/_image") ||
|
|
269
|
+
pathname.startsWith("/cdn-cgi/image/") ||
|
|
270
|
+
pathname.match(/\.(png|jpg|jpeg|webp|avif|svg|gif|ico|woff2?|css|js)$/i)
|
|
271
|
+
) {
|
|
272
|
+
response.headers.set(
|
|
273
|
+
"Cache-Control",
|
|
274
|
+
"public, max-age=31536000, s-maxage=31536000, immutable"
|
|
275
|
+
);
|
|
276
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
277
|
+
return response;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 🎯 5. SECURITY HEADERS FOR HTML PAGES
|
|
281
|
+
if (response.headers.get("content-type")?.includes("text/html")) {
|
|
282
|
+
response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
|
|
283
|
+
response.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
284
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
285
|
+
response.headers.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
|
286
|
+
response.headers.set(
|
|
287
|
+
"Content-Security-Policy",
|
|
288
|
+
"default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data: blob:; connect-src 'self' https:; frame-ancestors 'self';"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return response;
|
|
293
|
+
});
|
|
@@ -6,18 +6,28 @@ import Breadcrumb from "../components/global/Breadcrumb.astro";
|
|
|
6
6
|
import { getNodeByPath, getChannelId, getTree, getTreeId } from "../lib/cms";
|
|
7
7
|
import type { NodeResponse, TreeNode } from "../lib/cms";
|
|
8
8
|
|
|
9
|
+
// 🎯 Import environment variables directly from Cloudflare
|
|
10
|
+
import { env } from "cloudflare:workers";
|
|
11
|
+
|
|
9
12
|
export const prerender = false;
|
|
10
13
|
|
|
11
14
|
const { slug } = Astro.params;
|
|
15
|
+
|
|
16
|
+
// Extract Preview & Revision parameters from the URL
|
|
12
17
|
const searchParams = Astro.url.searchParams;
|
|
13
18
|
const isPreview = searchParams.get("preview") === "true";
|
|
14
19
|
const revisionId = searchParams.get("revisionId");
|
|
15
20
|
|
|
21
|
+
// Smart Edge Caching Logic
|
|
22
|
+
const isTimeTraveling = isPreview || !!revisionId;
|
|
23
|
+
|
|
24
|
+
// 🎯 FIX: Allow Cloudflare Edge to revalidate with Worker KV middleware
|
|
16
25
|
Astro.response.headers.set(
|
|
17
26
|
"Cache-Control",
|
|
18
|
-
"
|
|
27
|
+
"public, no-cache, must-revalidate",
|
|
19
28
|
);
|
|
20
29
|
|
|
30
|
+
// 1. STATIC ASSET CHECK
|
|
21
31
|
const staticExts = [
|
|
22
32
|
".ico",
|
|
23
33
|
".png",
|
|
@@ -37,11 +47,21 @@ if (isStaticAsset) {
|
|
|
37
47
|
return new Response(null, { status: 404 });
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
50
|
+
// 2. ENV & WORKER RESOLUTION
|
|
51
|
+
const workerEnv = env || {};
|
|
52
|
+
const cmsWorker = workerEnv.CMS_WORKER;
|
|
53
|
+
|
|
54
|
+
// Extract environment configuration & fallback slug
|
|
55
|
+
const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
|
|
56
|
+
const workerHomeSlug = (workerEnv as any).PUBLIC_HOME_SLUG;
|
|
57
|
+
const homeSlug = String(envHomeSlug || workerHomeSlug || "home").trim();
|
|
58
|
+
|
|
59
|
+
const envApiUrl = import.meta.env.PUBLIC_API_URL;
|
|
60
|
+
const workerApiUrl = workerEnv.PUBLIC_API_URL;
|
|
42
61
|
const baseApiUrl = String(
|
|
43
|
-
|
|
62
|
+
envApiUrl || workerApiUrl || "https://admin.kamaengine.com",
|
|
44
63
|
);
|
|
64
|
+
|
|
45
65
|
const cleanApiUrl = baseApiUrl.endsWith("/")
|
|
46
66
|
? baseApiUrl.slice(0, -1)
|
|
47
67
|
: baseApiUrl;
|
|
@@ -51,55 +71,71 @@ const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
51
71
|
if (urlStr.startsWith("/")) urlStr = `${cleanApiUrl}${urlStr}`;
|
|
52
72
|
|
|
53
73
|
const urlObj = new URL(urlStr);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
|
|
75
|
+
if (isTimeTraveling) {
|
|
76
|
+
if (revisionId) urlObj.searchParams.set("revisionId", revisionId);
|
|
77
|
+
if (isPreview) urlObj.searchParams.set("preview", "true");
|
|
78
|
+
urlObj.searchParams.set("_t", Date.now().toString());
|
|
79
|
+
} else {
|
|
80
|
+
urlObj.searchParams.set("_cb", Date.now().toString());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
urlStr = urlObj.toString();
|
|
84
|
+
|
|
85
|
+
const reqHeaders: Record<string, string> = {
|
|
86
|
+
...init?.headers,
|
|
87
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
88
|
+
Pragma: "no-cache",
|
|
89
|
+
// 🎯 CRITICAL FIX: Direct Service Bindings require explicit header flags to bypass CMS KV cache!
|
|
90
|
+
"X-Bypass-Cache": "true",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const fetchOpts: RequestInit = {
|
|
94
|
+
...init,
|
|
95
|
+
headers: reqHeaders,
|
|
96
|
+
cache: "no-store",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const req = new Request(urlStr, fetchOpts);
|
|
100
|
+
|
|
101
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
102
|
+
return cmsWorker.fetch(req);
|
|
103
|
+
}
|
|
104
|
+
return fetch(req);
|
|
70
105
|
};
|
|
71
106
|
|
|
72
107
|
const channelId = getChannelId();
|
|
73
108
|
const treeId = getTreeId();
|
|
74
109
|
|
|
110
|
+
// 3. PATH & CONCURRENT FETCH RESOLUTION
|
|
75
111
|
const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
|
|
76
112
|
const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
|
|
113
|
+
const requestedSlug = pathParts.join("/");
|
|
114
|
+
const targetSlug = requestedSlug || homeSlug;
|
|
77
115
|
|
|
78
116
|
let pageData: NodeResponse | null = null;
|
|
79
117
|
let navTree: TreeNode[] = [];
|
|
80
|
-
let targetSlug = pathParts.join("/");
|
|
81
118
|
|
|
119
|
+
// 🎯 Fetch Tree and Node in PARALLEL using the cache-busting customFetch
|
|
82
120
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
121
|
+
const [treeResult, nodeResult] = await Promise.all([
|
|
122
|
+
getTree(treeId, channelId, customFetch).catch(err => {
|
|
123
|
+
console.error(`❌ [SSR Error] Failed to fetch tree "${treeId}":`, err);
|
|
124
|
+
return [];
|
|
125
|
+
}),
|
|
126
|
+
getNodeByPath(targetSlug, channelId, isPreview, customFetch).catch(err => {
|
|
127
|
+
console.error(
|
|
128
|
+
`❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
|
|
129
|
+
err,
|
|
130
|
+
);
|
|
131
|
+
return null;
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
95
134
|
|
|
96
|
-
|
|
97
|
-
pageData =
|
|
135
|
+
navTree = treeResult || [];
|
|
136
|
+
pageData = nodeResult;
|
|
98
137
|
} catch (err) {
|
|
99
|
-
console.error(
|
|
100
|
-
`❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
|
|
101
|
-
err,
|
|
102
|
-
);
|
|
138
|
+
console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
if (!pageData || !pageData.node) {
|
|
@@ -113,6 +149,7 @@ const rootTreeSlug =
|
|
|
113
149
|
navTree && navTree.length > 0
|
|
114
150
|
? navTree[0].slug || (navTree[0] as any).key
|
|
115
151
|
: "";
|
|
152
|
+
|
|
116
153
|
const isHomePage =
|
|
117
154
|
targetSlug === homeSlug ||
|
|
118
155
|
(rootTreeSlug && targetSlug === rootTreeSlug) ||
|
|
@@ -120,10 +157,13 @@ const isHomePage =
|
|
|
120
157
|
pathParts.length === 0;
|
|
121
158
|
|
|
122
159
|
const normalizedSlug = isHomePage ? "" : targetSlug;
|
|
160
|
+
|
|
123
161
|
const headerBlocks = content?.header || [];
|
|
124
162
|
const mainBlocks = content?.content || [];
|
|
125
163
|
const sidebarBlocks = content?.sidebar || [];
|
|
126
164
|
const hasSidebar = sidebarBlocks.length > 0;
|
|
165
|
+
|
|
166
|
+
// Detect if this node is a standalone Page Skeleton
|
|
127
167
|
const isSkeleton = node?.nodeType === "page-skeleton";
|
|
128
168
|
---
|
|
129
169
|
|