@brightsideoy/kama 1.0.0-alpha.20 → 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.
Files changed (65) hide show
  1. package/content/definitions/blocks/image-gallery.yaml +34 -0
  2. package/content/definitions/blocks/youtube.yaml +8 -0
  3. package/content/definitions/field-sets/access.yaml +10 -0
  4. package/content/definitions/field-sets/page-base.yaml +16 -1
  5. package/content/definitions/taxonomies/media-tag.yaml +15 -0
  6. package/definitions/editor/default.ts +15 -1
  7. package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
  8. package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
  9. package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
  10. package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
  11. package/dist/assets/{CmsHelp-DyPJLGGi.js → CmsHelp-BQkr9KyU.js} +1 -1
  12. package/dist/assets/{CmsPublisher-DNLwjirk.js → CmsPublisher-XSCPrp5L.js} +1 -1
  13. package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
  14. package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
  15. package/dist/assets/{TrashWorkspace-DgIg1v6t.js → TrashWorkspace-CATe46WF.js} +1 -1
  16. package/dist/assets/index-CY0VYSyt.css +1 -0
  17. package/dist/assets/index-DdG9Pozx.js +14 -0
  18. package/dist/assets/{plus-FQRsZBoJ.js → plus-CX_GHzXs.js} +1 -1
  19. package/dist/assets/useNodes-CqFlc72m.js +1 -0
  20. package/dist/{dist-D_Q1y4Kh.js → dist-3Xc2TTZw.js} +1117 -456
  21. package/dist/index.html +4 -4
  22. package/dist/index.js +1117 -456
  23. package/dist/schema-compiler.js +1 -1
  24. package/package.json +5 -5
  25. package/schema.sql +10 -0
  26. package/templates/astro-starter/src/components/global/NavigationMenu.astro +11 -3
  27. package/dist/assets/AssetEditor-D5j7pwcK.css +0 -1
  28. package/dist/assets/AssetEditor-Dt1zCR7_.js +0 -1
  29. package/dist/assets/NodeEditor-CiXQKXK1.js +0 -147
  30. package/dist/assets/NodeEditor-pZI3gu4a.css +0 -1
  31. package/dist/assets/index-DQ6_RmCI.js +0 -14
  32. package/dist/assets/index-DcKiaRjI.css +0 -1
  33. package/dist/assets/useNodes-D2-VxsxR.js +0 -1
  34. package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +0 -143
  35. package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +0 -67
  36. package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +0 -126
  37. package/templates/astro-starter/old_src/components/blocks/Hero.astro +0 -218
  38. package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +0 -25
  39. package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +0 -91
  40. package/templates/astro-starter/old_src/components/blocks/NodeList.astro +0 -274
  41. package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +0 -15
  42. package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +0 -108
  43. package/templates/astro-starter/old_src/components/global/Footer.astro +0 -50
  44. package/templates/astro-starter/old_src/components/global/Header.astro +0 -171
  45. package/templates/astro-starter/old_src/components/global/MetaHead.astro +0 -266
  46. package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +0 -413
  47. package/templates/astro-starter/old_src/components/ui/KamaImage.astro +0 -180
  48. package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +0 -56
  49. package/templates/astro-starter/old_src/layouts/StandardLayout.astro +0 -63
  50. package/templates/astro-starter/old_src/lib/cms.ts +0 -219
  51. package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +0 -67
  52. package/templates/astro-starter/old_src/lib/media/media-proxy.ts +0 -148
  53. package/templates/astro-starter/old_src/lib/media/media-url.ts +0 -112
  54. package/templates/astro-starter/old_src/middleware.ts +0 -536
  55. package/templates/astro-starter/old_src/pages/404.astro +0 -46
  56. package/templates/astro-starter/old_src/pages/[...slug].astro +0 -311
  57. package/templates/astro-starter/old_src/pages/api/load-nodes.ts +0 -68
  58. package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +0 -101
  59. package/templates/astro-starter/old_src/pages/llms-full.txt.ts +0 -171
  60. package/templates/astro-starter/old_src/pages/llms.txt.ts +0 -113
  61. package/templates/astro-starter/old_src/pages/media/[...path].ts +0 -14
  62. package/templates/astro-starter/old_src/pages/partials/footer.astro +0 -69
  63. package/templates/astro-starter/old_src/pages/partials/nav.astro +0 -141
  64. package/templates/astro-starter/old_src/styles/standard-theme.css +0 -157
  65. package/templates/astro-starter/old_src/utils/tiptap.ts +0 -113
@@ -1,311 +0,0 @@
1
- ---
2
- // src/pages/[...slug].astro
3
-
4
- import StandardLayout from "../layouts/StandardLayout.astro";
5
- import SkeletonLayout from "../layouts/SkeletonLayout.astro";
6
- import BlockRenderer from "../components/blocks/BlockRenderer.astro";
7
- import Breadcrumb from "../components/global/Breadcrumb.astro";
8
- import { getNodeByPath, getChannelId, getTree, getTreeId } from "../lib/cms";
9
- import type { NodeResponse, TreeNode } from "../lib/cms";
10
-
11
- export const prerender = false;
12
-
13
- const { slug } = Astro.params;
14
-
15
- // Extract Preview & Revision parameters from the URL
16
- const searchParams = Astro.url.searchParams;
17
- const isPreview = searchParams.get("preview") === "true";
18
- const revisionId = searchParams.get("revisionId");
19
-
20
- // Smart Edge Caching Logic
21
- const isTimeTraveling = isPreview || !!revisionId;
22
-
23
- // 🎯 Allow Cloudflare Edge to revalidate with Worker KV middleware
24
- Astro.response.headers.set(
25
- "Cache-Control",
26
- "public, no-cache, must-revalidate",
27
- );
28
-
29
- // 1. STATIC ASSET CHECK
30
- const staticExts = [
31
- ".ico",
32
- ".png",
33
- ".jpg",
34
- ".jpeg",
35
- ".svg",
36
- ".txt",
37
- ".xml",
38
- ".webmanifest",
39
- ];
40
- const slugStr = String(slug || "");
41
- const isStaticAsset =
42
- !slugStr.includes("/") &&
43
- staticExts.some(ext => slugStr.toLowerCase().endsWith(ext));
44
-
45
- if (isStaticAsset) {
46
- return new Response(null, { status: 404 });
47
- }
48
-
49
- // 2. ENV & WORKER RESOLUTION (Safe Dual-Environment)
50
- const safeEnv = (Astro.locals as any).safeEnv || {};
51
- const cmsWorker = safeEnv.CMS_WORKER;
52
-
53
- // Extract environment configuration & fallback slug
54
- const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
55
- const workerHomeSlug = safeEnv.PUBLIC_HOME_SLUG;
56
- const homeSlug = String(envHomeSlug || workerHomeSlug || "index").trim();
57
-
58
- const envApiUrl = import.meta.env.PUBLIC_API_URL;
59
- const workerApiUrl = safeEnv.PUBLIC_API_URL;
60
- const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
61
-
62
- // 🎯 Safely extract the secret across both environments
63
- const frontendApiSecret =
64
- import.meta.env.FRONTEND_API_SECRET || safeEnv.FRONTEND_API_SECRET;
65
-
66
- const cleanApiUrl = baseApiUrl.endsWith("/")
67
- ? baseApiUrl.slice(0, -1)
68
- : baseApiUrl;
69
-
70
- const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
71
- let urlStr = input instanceof URL ? input.toString() : String(input);
72
- if (urlStr.startsWith("/")) urlStr = `${cleanApiUrl}${urlStr}`;
73
-
74
- const urlObj = new URL(urlStr);
75
-
76
- if (isTimeTraveling) {
77
- if (revisionId) urlObj.searchParams.set("revisionId", revisionId);
78
- if (isPreview) urlObj.searchParams.set("preview", "true");
79
- urlObj.searchParams.set("_t", Date.now().toString());
80
- } else {
81
- urlObj.searchParams.set("_cb", Date.now().toString());
82
- }
83
-
84
- urlStr = urlObj.toString();
85
-
86
- const reqHeaders: Record<string, string> = {
87
- ...init?.headers,
88
- "Cache-Control": "no-cache, no-store, must-revalidate",
89
- Pragma: "no-cache",
90
- "X-Bypass-Cache": "true",
91
- };
92
-
93
- if (frontendApiSecret) {
94
- reqHeaders["Authorization"] = `Bearer ${frontendApiSecret}`;
95
- }
96
-
97
- const fetchOpts: RequestInit = {
98
- ...init,
99
- headers: reqHeaders,
100
- };
101
-
102
- const req = new Request(urlStr, fetchOpts);
103
-
104
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
105
- return cmsWorker.fetch(req);
106
- }
107
- return fetch(req);
108
- };
109
-
110
- const channelId = getChannelId();
111
- const treeId = getTreeId();
112
-
113
- // 3. PATH & CONCURRENT FETCH RESOLUTION
114
- const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
115
- const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
116
- const requestedSlug = pathParts.join("/");
117
-
118
- // 🎯 ONLY FIX NEEDED HERE: Pass '/' if empty so home route resolves, otherwise requestedSlug
119
- const targetSlug = requestedSlug || "/";
120
-
121
- let pageData: NodeResponse | null = null;
122
- let navTree: TreeNode[] = [];
123
-
124
- // 🎯 Fetch Tree and Node in PARALLEL using the cache-busting customFetch
125
- try {
126
- const [treeResult, nodeResult] = await Promise.all([
127
- getTree(treeId, channelId, customFetch).catch(err => {
128
- console.error(`❌ [SSR Error] Failed to fetch tree "${treeId}":`, err);
129
- return [];
130
- }),
131
- getNodeByPath(targetSlug, channelId, isPreview, customFetch).catch(err => {
132
- console.error(
133
- `❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
134
- err,
135
- );
136
- return null;
137
- }),
138
- ]);
139
-
140
- navTree = treeResult || [];
141
- pageData = nodeResult as NodeResponse | null;
142
- } catch (err) {
143
- console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
144
- }
145
-
146
- if (!pageData || !pageData.node) {
147
- return new Response("Page Not Found", { status: 404 });
148
- }
149
-
150
- const node = pageData.node;
151
- const content = pageData.content || {};
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
-
175
- const rootTreeSlug =
176
- navTree && navTree.length > 0
177
- ? navTree[0].slug || (navTree[0] as any).key
178
- : "";
179
-
180
- const isHomePage =
181
- targetSlug === "/" ||
182
- targetSlug === homeSlug ||
183
- (rootTreeSlug && targetSlug === rootTreeSlug) ||
184
- node?.path === "/" ||
185
- pathParts.length === 0;
186
-
187
- const normalizedSlug = isHomePage ? "" : targetSlug;
188
-
189
- const headerBlocks = content?.header || [];
190
- const mainBlocks = content?.content || [];
191
- const sidebarBlocks = content?.sidebar || [];
192
- const hasSidebar = sidebarBlocks.length > 0;
193
-
194
- // Detect if this node is a standalone Page Skeleton
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
- `;
234
- ---
235
-
236
- {
237
- isSkeleton ? (
238
- <SkeletonLayout
239
- node={node}
240
- content={content}
241
- language={pageData?.language}
242
- normalizedSlug={normalizedSlug}
243
- isHomePage={isHomePage}
244
- >
245
- <BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
246
-
247
- {isPreview && <script is:inline set:html={hotSwapScript} />}
248
- </SkeletonLayout>
249
- ) : (
250
- <StandardLayout
251
- node={node}
252
- content={content}
253
- language={pageData?.language}
254
- normalizedSlug={normalizedSlug}
255
- isHomePage={isHomePage}
256
- hasSidebar={hasSidebar}
257
- headerBlocks={headerBlocks}
258
- tree={navTree}
259
- >
260
- {headerBlocks.length > 0 && (
261
- <section class="region-header">
262
- <BlockRenderer
263
- blocks={headerBlocks}
264
- contentMap={content}
265
- region="header"
266
- />
267
- </section>
268
- )}
269
-
270
- {!isHomePage && (
271
- <div class="breadcrumb-wrapper">
272
- <Breadcrumb title={node?.title || "Page"} slug={normalizedSlug} />
273
- </div>
274
- )}
275
-
276
- <div class:list={["content-layout", { "has-sidebar": hasSidebar }]}>
277
- <div class="main-column">
278
- {mainBlocks.length > 0 ? (
279
- <section class="region-main">
280
- <BlockRenderer
281
- blocks={mainBlocks}
282
- contentMap={content}
283
- region="main"
284
- />
285
- </section>
286
- ) : (
287
- <div class="empty-content">
288
- <p data-cms-field="summary">
289
- {node?.summary || "No content available for this page."}
290
- </p>
291
- </div>
292
- )}
293
- </div>
294
-
295
- {hasSidebar && (
296
- <aside class="sidebar-column">
297
- <section class="region-sidebar">
298
- <BlockRenderer
299
- blocks={sidebarBlocks}
300
- contentMap={content}
301
- region="sidebar"
302
- />
303
- </section>
304
- </aside>
305
- )}
306
- </div>
307
-
308
- {isPreview && <script is:inline set:html={hotSwapScript} />}
309
- </StandardLayout>
310
- )
311
- }
@@ -1,68 +0,0 @@
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
- };
@@ -1,101 +0,0 @@
1
- ---
2
- // src/pages/api/partials/node-cards.astro
3
- export const partial = true; // Tells Astro not to inject DOCTYPE or <head> tags
4
-
5
- import KamaImage from "@/components/ui/KamaImage.astro";
6
-
7
- const { request, locals } = Astro;
8
- const url = new URL(request.url);
9
- const searchParams = url.searchParams;
10
-
11
- // 1. Get Environment Variables securely via middleware
12
- const cfEnv = (locals as any).safeEnv || {};
13
- const cmsWorker = cfEnv.CMS_WORKER;
14
- const apiUrl =
15
- cfEnv.PUBLIC_API_URL ||
16
- import.meta.env.PUBLIC_API_URL ||
17
- "http://localhost:8787";
18
- const secret = cfEnv.FRONTEND_API_SECRET || import.meta.env.FRONTEND_API_SECRET;
19
-
20
- // 2. Construct the actual backend URL
21
- const cleanApiUrl = apiUrl.replace(/\/$/, "");
22
- const backendUrl = `${cleanApiUrl}/api/v1/nodes?${searchParams.toString()}`;
23
-
24
- // 3. Prepare the secure headers
25
- const reqHeaders: Record<string, string> = { Accept: "application/json" };
26
- if (secret) reqHeaders["Authorization"] = `Bearer ${secret}`;
27
-
28
- const backendReq = new Request(backendUrl, {
29
- method: "GET",
30
- headers: reqHeaders,
31
- });
32
-
33
- // 4. Fetch securely using Service Binding
34
- let response;
35
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
36
- response = await cmsWorker.fetch(backendReq);
37
- } else {
38
- response = await fetch(backendReq);
39
- }
40
-
41
- let itemsArray: any[] = [];
42
- let limit = 12;
43
- let consumedRows = 0;
44
-
45
- if (response.ok) {
46
- const data = await response.json();
47
- itemsArray = Array.isArray(data)
48
- ? data
49
- : data.items || data.nodes || data.data || [];
50
- limit = data.limit || 12;
51
- consumedRows =
52
- data.rawCount !== undefined ? data.rawCount : itemsArray.length;
53
- }
54
-
55
- // 5. Send pagination data back to the frontend script via custom HTTP Headers
56
- Astro.response.headers.set("X-Consumed-Rows", consumedRows.toString());
57
- Astro.response.headers.set("X-Limit", limit.toString());
58
- Astro.response.headers.set(
59
- "Cache-Control",
60
- "public, max-age=0, s-maxage=60, stale-while-revalidate=120",
61
- );
62
- ---
63
-
64
- {
65
- itemsArray.map((item: any) => {
66
- const dateStr = item.createdAt
67
- ? new Date(item.createdAt).toLocaleDateString("en-GB", {
68
- day: "numeric",
69
- month: "short",
70
- year: "numeric",
71
- })
72
- : "";
73
-
74
- return (
75
- <article class="node-card">
76
- <a href={`/${item.slug}`} class="node-card-link">
77
- {item.image ? (
78
- <KamaImage
79
- image={item.image}
80
- sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 33vw"
81
- desktopWidths={[320, 640, 768, 1024]}
82
- idealWidth={429}
83
- loading="lazy"
84
- />
85
- ) : (
86
- <div class="image-placeholder" />
87
- )}
88
- <div class="node-card-content">
89
- <h3 class="node-card-title">{item.title}</h3>
90
- {dateStr && (
91
- <time datetime={item.createdAt} class="node-card-date">
92
- {dateStr}
93
- </time>
94
- )}
95
- {item.excerpt && <p class="node-card-excerpt">{item.excerpt}</p>}
96
- </div>
97
- </a>
98
- </article>
99
- );
100
- })
101
- }
@@ -1,171 +0,0 @@
1
- // src/pages/llms-full.txt.ts
2
- import type { APIRoute } from "astro";
3
- import { getTree, getTreeId, getChannelId, getNodeByPath } from "../lib/cms";
4
- import { renderTiptapNode } from "../utils/tiptap";
5
-
6
- export const prerender = false;
7
-
8
- export const GET: APIRoute = async (context) => {
9
- // 1. Setup Worker Fetch Binding
10
- const locals = (context.locals as any) || {};
11
- const cfEnv = locals.cfContext?.env || {};
12
- const cmsWorker = cfEnv.CMS_WORKER;
13
-
14
- const envApiUrl = import.meta.env.PUBLIC_API_URL;
15
- const workerApiUrl = cfEnv.PUBLIC_API_URL || (typeof process !== "undefined" ? process.env?.PUBLIC_API_URL : undefined);
16
- const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787").replace(/\/$/, "");
17
-
18
- const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
19
- let urlStr = input instanceof URL ? input.toString() : String(input);
20
- if (urlStr.startsWith("/")) urlStr = `${baseApiUrl}${urlStr}`;
21
-
22
- const req = new Request(urlStr, init);
23
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
24
- return cmsWorker.fetch(req);
25
- }
26
- return fetch(req);
27
- };
28
-
29
- const channelId = getChannelId() || import.meta.env.PUBLIC_CHANNEL_ID || "main-channel";
30
- const treeId = getTreeId() || import.meta.env.PUBLIC_TREE_ID || "main-tree";
31
-
32
- // 🎯 Extract configurable fallback home slug
33
- const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
34
- const workerHomeSlug = cfEnv.PUBLIC_HOME_SLUG || (typeof process !== "undefined" ? process.env?.PUBLIC_HOME_SLUG : undefined);
35
- const homeSlug = String(envHomeSlug || workerHomeSlug || "home").trim();
36
-
37
- const rawOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
38
- const siteOrigin = String(rawOrigin).replace(/\/$/, "");
39
- const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
40
-
41
- try {
42
- // 2. Fetch Navigation Tree first to determine root nodes
43
- const rawNavTree = await getTree(treeId, channelId, customFetch).catch(() => []);
44
- const navTree = Array.isArray(rawNavTree)
45
- ? rawNavTree
46
- : (rawNavTree?.tree || rawNavTree?.items || rawNavTree?.data || []);
47
-
48
- // Determine home page slug (first root node in tree, or fallback home slug)
49
- const rootTreeSlug = navTree.length > 0 ? (navTree[0].slug || navTree[0].key || "") : "";
50
- const resolvedHomeSlug = rootTreeSlug || homeSlug;
51
-
52
- // Fetch Home Node (for global context)
53
- const homeData = await getNodeByPath(resolvedHomeSlug, channelId, false, customFetch).catch(() => null);
54
-
55
- const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Full Application Documentation Corpus";
56
-
57
- // 3. Header section matching llmstxt.org spec
58
- let fullText = `# ${brandName} - Full Corpus\n\n`;
59
- fullText += `> ${siteSummary}\n\n`;
60
- fullText += `This file contains the complete unabridged text content for all pages across the application.\n\n`;
61
-
62
- // Helper to extract text from block data (handles TipTap AST & raw strings)
63
- const parseBlockText = (blockData: any): string => {
64
- if (!blockData) return "";
65
- let rawText = "";
66
-
67
- if (typeof blockData === "string") {
68
- rawText = blockData;
69
- } else if (blockData.type === "doc") {
70
- rawText = renderTiptapNode(blockData);
71
- } else if (blockData.body) {
72
- rawText = parseBlockText(blockData.body);
73
- } else if (blockData.text) {
74
- rawText = parseBlockText(blockData.text);
75
- }
76
-
77
- // Convert basic HTML block tags to plain text linebreaks
78
- return rawText
79
- .replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "## $1\n")
80
- .replace(/<p[^>]*>(.*?)<\/p>/gi, "$1\n")
81
- .replace(/<br\s*\/?>/gi, "\n")
82
- .replace(/<[^>]+>/g, ""); // Strip any remaining tags
83
- };
84
-
85
- // Recursive walker to fetch each page's full content
86
- const processNodes = async (nodes: any[], depth = 1) => {
87
- let sectionContent = "";
88
-
89
- for (const item of nodes) {
90
- const nodeData = item.node || item;
91
- if (!nodeData || nodeData.isHidden) continue;
92
-
93
- const itemSlug = nodeData.slug || nodeData.path || "";
94
- const isRootItem = itemSlug === resolvedHomeSlug || itemSlug === "" || itemSlug === "/";
95
- const itemUrl = isRootItem ? siteOrigin : `${siteOrigin}/${itemSlug.replace(/^\//, "")}`;
96
- const itemTitle = nodeData.title || nodeData.label || "Untitled Page";
97
-
98
- const fetchSlug = isRootItem ? resolvedHomeSlug : itemSlug.replace(/^\//, "");
99
-
100
- // Fetch node's block contents
101
- const pageData = await getNodeByPath(
102
- fetchSlug,
103
- channelId,
104
- false,
105
- customFetch
106
- ).catch(() => null);
107
-
108
- // Add Document Title Header
109
- sectionContent += `\n---\n\n`;
110
- sectionContent += `# ${itemTitle}\n`;
111
- sectionContent += `**URL:** ${itemUrl}\n`;
112
- if (nodeData.abstract || nodeData.summary) {
113
- sectionContent += `**Abstract:** ${nodeData.abstract || nodeData.summary}\n`;
114
- }
115
- sectionContent += `\n`;
116
-
117
- // Render all blocks on the node
118
- const regions = pageData?.content || {};
119
- let blockCount = 0;
120
-
121
- Object.entries(regions).forEach(([regionName, blocks]) => {
122
- if (!Array.isArray(blocks)) return;
123
-
124
- blocks.forEach((block: any) => {
125
- const data = block.data || {};
126
- const renderedText = parseBlockText(data);
127
-
128
- if (renderedText && renderedText.trim()) {
129
- sectionContent += `${renderedText.trim()}\n\n`;
130
- blockCount++;
131
- }
132
- });
133
- });
134
-
135
- if (blockCount === 0) {
136
- sectionContent += `*(No text content available for this node)*\n\n`;
137
- }
138
-
139
- // Recursively process child pages
140
- if (Array.isArray(item.children) && item.children.length > 0) {
141
- sectionContent += await processNodes(item.children, depth + 1);
142
- }
143
- }
144
-
145
- return sectionContent;
146
- };
147
-
148
- if (navTree.length > 0) {
149
- fullText += await processNodes(navTree);
150
- } else {
151
- fullText += `\n---\n\n# Home\n**URL:** ${siteOrigin}\n\nNo additional pages found in navigation tree.\n`;
152
- }
153
-
154
- fullText += `\n---\n*Generated dynamically by Kama Engine.*`;
155
-
156
- return new Response(fullText, {
157
- status: 200,
158
- headers: {
159
- "Content-Type": "text/plain; charset=utf-8",
160
- "Cache-Control": "public, max-age=3600, s-maxage=86400"
161
- },
162
- });
163
-
164
- } catch (error) {
165
- console.error("[llms-full.txt Generator Error]:", error);
166
- return new Response("# Error\nCould not generate full AI context corpus.", {
167
- status: 500,
168
- headers: { "Content-Type": "text/plain; charset=utf-8" }
169
- });
170
- }
171
- };