@brightsideoy/kama 0.1.0-alpha.99 → 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.
Files changed (119) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +726 -261
  3. package/content/definitions/blocks/button.yaml +34 -0
  4. package/content/definitions/blocks/header.yaml +13 -0
  5. package/content/definitions/blocks/header_image.yaml +21 -0
  6. package/content/definitions/blocks/hero.yaml +28 -0
  7. package/content/definitions/blocks/html.yaml +13 -0
  8. package/content/definitions/blocks/image_card.yaml +56 -0
  9. package/content/definitions/blocks/node-list.yaml +61 -0
  10. package/content/definitions/blocks/single_image.yaml +27 -0
  11. package/content/definitions/blocks/text.yaml +15 -0
  12. package/content/definitions/channels/ecommerce.yaml +28 -0
  13. package/content/definitions/channels/website.yaml +41 -0
  14. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  15. package/content/definitions/field-sets/channel-base.yaml +57 -0
  16. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  17. package/content/definitions/field-sets/content-block.yaml +41 -0
  18. package/content/definitions/field-sets/page-base.yaml +44 -0
  19. package/content/definitions/field-sets/page-seo.yaml +34 -0
  20. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  21. package/content/definitions/field-sets/schema-type.yaml +43 -0
  22. package/content/definitions/field-sets/tree-base.yaml +31 -0
  23. package/content/definitions/node-layouts/content.yml +12 -0
  24. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  25. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  26. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  27. package/content/definitions/nodes/address.yaml +40 -0
  28. package/content/definitions/nodes/article.yaml +65 -0
  29. package/content/definitions/nodes/event.yaml +55 -0
  30. package/content/definitions/nodes/external-link.yaml +29 -0
  31. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  32. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  33. package/content/definitions/nodes/person.yaml +37 -0
  34. package/content/definitions/nodes/product.yaml +89 -0
  35. package/content/definitions/taxonomies/categories.yaml +15 -0
  36. package/content/definitions/taxonomies/tags.yaml +12 -0
  37. package/content/definitions/trees/navigation.yaml +24 -0
  38. package/definitions/editor/default.ts +1 -0
  39. package/dist/assets/index-Bzbme3Ob.css +1 -0
  40. package/dist/assets/index-DX98CiK-.js +164 -0
  41. package/dist/dist-CErcWOwp.js +27136 -0
  42. package/dist/index.html +4 -2
  43. package/dist/index.js +26159 -8575
  44. package/dist/schema-compiler.js +48 -0
  45. package/documentation/SCHEMAS.md +182 -0
  46. package/package.json +23 -25
  47. package/schema.sql +67 -23
  48. package/templates/astro-starter/astro.config.mjs +27 -0
  49. package/templates/astro-starter/package.json.template +20 -0
  50. package/templates/astro-starter/public/robots.txt +23 -0
  51. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
  52. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  53. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
  54. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  55. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  56. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
  57. package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
  58. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  59. package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
  60. package/templates/astro-starter/src/components/global/Footer.astro +50 -0
  61. package/templates/astro-starter/src/components/global/Header.astro +171 -0
  62. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  63. package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
  64. package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
  65. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  66. package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
  67. package/templates/astro-starter/src/lib/cms.ts +219 -0
  68. package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
  69. package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
  70. package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
  71. package/templates/astro-starter/src/middleware.ts +508 -0
  72. package/templates/astro-starter/src/pages/404.astro +46 -0
  73. package/templates/astro-starter/src/pages/[...slug].astro +318 -0
  74. package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
  75. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  76. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  77. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  78. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  79. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  80. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  81. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  82. package/templates/astro-starter/tsconfig.json +0 -0
  83. package/templates/kama-worker/index.js +19 -0
  84. package/templates/kama-worker/server.js +104 -0
  85. package/wrangler.json +22 -0
  86. package/defaults/definitions/blocks/button.yaml +0 -15
  87. package/defaults/definitions/blocks/header.yaml +0 -15
  88. package/defaults/definitions/blocks/header_image.yaml +0 -22
  89. package/defaults/definitions/blocks/hero.yaml +0 -27
  90. package/defaults/definitions/blocks/image_card.yaml +0 -57
  91. package/defaults/definitions/blocks/single_image.yaml +0 -28
  92. package/defaults/definitions/blocks/text.yaml +0 -16
  93. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  94. package/defaults/definitions/channels/website.yaml +0 -32
  95. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  96. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  97. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  98. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  99. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  100. package/defaults/definitions/nodes/address.yaml +0 -32
  101. package/defaults/definitions/nodes/article.yaml +0 -43
  102. package/defaults/definitions/nodes/event.yaml +0 -49
  103. package/defaults/definitions/nodes/external-link.yaml +0 -26
  104. package/defaults/definitions/nodes/person.yaml +0 -37
  105. package/defaults/definitions/nodes/product.yaml +0 -86
  106. package/defaults/definitions/trees/navigation.yaml +0 -29
  107. package/defaults/definitions/trees/social-links.yaml +0 -9
  108. package/definitions/_page.yaml +0 -52
  109. package/dist/assets/index-D6qQsWOj.js +0 -1392
  110. package/dist/assets/index-TdW5F0pX.css +0 -1
  111. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  112. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  113. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  114. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  115. package/scripts/bundle-definitions.js +0 -176
  116. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  117. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  118. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  119. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
@@ -0,0 +1,318 @@
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
+ // 🎯 NEW: 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
+ // 🎯 Direct Service Bindings require explicit header flags to bypass CMS KV cache!
91
+ "X-Bypass-Cache": "true",
92
+ };
93
+
94
+ // 🎯 NEW: Inject the secret securely into every outgoing server request
95
+ if (frontendApiSecret) {
96
+ reqHeaders["Authorization"] = `Bearer ${frontendApiSecret}`;
97
+ }
98
+
99
+ const fetchOpts: RequestInit = {
100
+ ...init,
101
+ headers: reqHeaders,
102
+ };
103
+
104
+ const req = new Request(urlStr, fetchOpts);
105
+
106
+ if (cmsWorker && typeof cmsWorker.fetch === "function") {
107
+ return cmsWorker.fetch(req);
108
+ }
109
+ return fetch(req);
110
+ };
111
+
112
+ const channelId = getChannelId();
113
+ const treeId = getTreeId();
114
+
115
+ // 3. PATH & CONCURRENT FETCH RESOLUTION
116
+ const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
117
+ const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
118
+ const requestedSlug = pathParts.join("/");
119
+ const targetSlug = requestedSlug || homeSlug;
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
+ // Rely on the backend API's meta.cacheTtl which calculates the closest publishing event for the node AND its blocks.
155
+ let cacheTtl = pageData?.meta?.cacheTtl;
156
+
157
+ if (cacheTtl === undefined || cacheTtl === null) {
158
+ cacheTtl = 31536000; // Default: 1 Year (Forever)
159
+ }
160
+
161
+ // Clamp to Cloudflare's 60-second minimum for KV expirations
162
+ cacheTtl = Math.max(60, cacheTtl);
163
+
164
+ if (isTimeTraveling) {
165
+ Astro.response.headers.set(
166
+ "Cache-Control",
167
+ "no-cache, no-store, must-revalidate",
168
+ );
169
+ } else {
170
+ // 🎯 FIX: max-age=0 prevents local browser caching. s-maxage handles Cloudflare CDN!
171
+ Astro.response.headers.set(
172
+ "Cache-Control",
173
+ `public, max-age=0, s-maxage=${cacheTtl}, must-revalidate`,
174
+ );
175
+ // Tell middleware how long to store in KV
176
+ Astro.response.headers.set("X-KV-Expiration", cacheTtl.toString());
177
+ }
178
+
179
+ const rootTreeSlug =
180
+ navTree && navTree.length > 0
181
+ ? navTree[0].slug || (navTree[0] as any).key
182
+ : "";
183
+
184
+ const isHomePage =
185
+ targetSlug === homeSlug ||
186
+ (rootTreeSlug && targetSlug === rootTreeSlug) ||
187
+ node?.path === "/" ||
188
+ pathParts.length === 0;
189
+
190
+ const normalizedSlug = isHomePage ? "" : targetSlug;
191
+
192
+ const headerBlocks = content?.header || [];
193
+ const mainBlocks = content?.content || [];
194
+ const sidebarBlocks = content?.sidebar || [];
195
+ const hasSidebar = sidebarBlocks.length > 0;
196
+
197
+ // Detect if this node is a standalone Page Skeleton
198
+ const isSkeleton = node?.nodeType === "page-skeleton";
199
+
200
+ // 🎯 HTML-OVER-THE-WIRE HOT SWAP LOGIC
201
+ // Isolated as a string so we don't violate DRY when injecting into multiple layouts
202
+ const hotSwapScript = `
203
+ if (window !== window.top) {
204
+ window.addEventListener("message", async event => {
205
+ if (event.data && event.data.type === "kama-preview-update") {
206
+ try {
207
+ const currentScrollY = window.scrollY;
208
+ const res = await fetch(event.data.url);
209
+ const newHtml = await res.text();
210
+ const newDoc = new DOMParser().parseFromString(newHtml, "text/html");
211
+
212
+ newDoc.body
213
+ .querySelectorAll("script, style, link, meta, noscript")
214
+ .forEach(el => el.remove());
215
+
216
+ const preserveTags = ["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT"];
217
+
218
+ Array.from(document.body.children).forEach(child => {
219
+ if (!preserveTags.includes(child.tagName.toUpperCase())) {
220
+ child.remove();
221
+ }
222
+ });
223
+
224
+ const referenceNode = document.body.firstChild;
225
+ Array.from(newDoc.body.children).forEach(child => {
226
+ if (!preserveTags.includes(child.tagName.toUpperCase())) {
227
+ document.body.insertBefore(child.cloneNode(true), referenceNode);
228
+ }
229
+ });
230
+
231
+ window.scrollTo(0, currentScrollY);
232
+ } catch (err) {
233
+ console.error("[KAMA Preview] Failed to hot-swap HTML:", err);
234
+ }
235
+ }
236
+ });
237
+ }
238
+ `;
239
+ ---
240
+
241
+ {
242
+ isSkeleton ? (
243
+ <SkeletonLayout
244
+ node={node}
245
+ content={content}
246
+ language={pageData?.language}
247
+ normalizedSlug={normalizedSlug}
248
+ isHomePage={isHomePage}
249
+ >
250
+ <BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
251
+
252
+ {/* 🎯 FIX: Script injected securely inside the layout ONLY during preview mode */}
253
+ {isPreview && <script is:inline set:html={hotSwapScript} />}
254
+ </SkeletonLayout>
255
+ ) : (
256
+ <StandardLayout
257
+ node={node}
258
+ content={content}
259
+ language={pageData?.language}
260
+ normalizedSlug={normalizedSlug}
261
+ isHomePage={isHomePage}
262
+ hasSidebar={hasSidebar}
263
+ headerBlocks={headerBlocks}
264
+ tree={navTree}
265
+ >
266
+ {headerBlocks.length > 0 && (
267
+ <section class="region-header">
268
+ <BlockRenderer
269
+ blocks={headerBlocks}
270
+ contentMap={content}
271
+ region="header"
272
+ />
273
+ </section>
274
+ )}
275
+
276
+ {!isHomePage && (
277
+ <div class="breadcrumb-wrapper">
278
+ <Breadcrumb title={node?.title || "Page"} slug={normalizedSlug} />
279
+ </div>
280
+ )}
281
+
282
+ <div class:list={["content-layout", { "has-sidebar": hasSidebar }]}>
283
+ <div class="main-column">
284
+ {mainBlocks.length > 0 ? (
285
+ <section class="region-main">
286
+ <BlockRenderer
287
+ blocks={mainBlocks}
288
+ contentMap={content}
289
+ region="main"
290
+ />
291
+ </section>
292
+ ) : (
293
+ <div class="empty-content">
294
+ <p data-cms-field="summary">
295
+ {node?.summary || "No content available for this page."}
296
+ </p>
297
+ </div>
298
+ )}
299
+ </div>
300
+
301
+ {hasSidebar && (
302
+ <aside class="sidebar-column">
303
+ <section class="region-sidebar">
304
+ <BlockRenderer
305
+ blocks={sidebarBlocks}
306
+ contentMap={content}
307
+ region="sidebar"
308
+ />
309
+ </section>
310
+ </aside>
311
+ )}
312
+ </div>
313
+
314
+ {/* 🎯 FIX: Script injected securely inside the layout ONLY during preview mode */}
315
+ {isPreview && <script is:inline set:html={hotSwapScript} />}
316
+ </StandardLayout>
317
+ )
318
+ }
@@ -0,0 +1,40 @@
1
+ // src/pages/api/load-nodes.ts
2
+ import type { APIRoute } from "astro";
3
+
4
+ export const GET: APIRoute = async ({ request, locals }) => {
5
+ const url = new URL(request.url);
6
+ const searchParams = url.searchParams;
7
+
8
+ // 1. Get Environment Variables securely
9
+ const safeEnv = (locals as any).safeEnv || {};
10
+ const apiUrl = import.meta.env.PUBLIC_API_URL || safeEnv.PUBLIC_API_URL || "http://localhost:8787";
11
+ const secret = import.meta.env.FRONTEND_API_SECRET || safeEnv.FRONTEND_API_SECRET;
12
+
13
+ // 2. Construct the actual backend URL
14
+ const backendUrl = `${apiUrl.replace(/\/$/, "")}/api/v1/nodes?${searchParams.toString()}`;
15
+
16
+ try {
17
+ // 3. Fetch from the CMS securely
18
+ const response = await fetch(backendUrl, {
19
+ headers: {
20
+ "Accept": "application/json",
21
+ ...(secret ? { "Authorization": `Bearer ${secret}` } : {})
22
+ }
23
+ });
24
+
25
+ if (!response.ok) {
26
+ return new Response(JSON.stringify({ error: "Failed to fetch nodes" }), { status: response.status });
27
+ }
28
+
29
+ const data = await response.json();
30
+
31
+ // 4. Return to the browser
32
+ return new Response(JSON.stringify(data), {
33
+ status: 200,
34
+ headers: { "Content-Type": "application/json" }
35
+ });
36
+ } catch (err) {
37
+ console.error("[Astro Proxy] Failed to load nodes:", err);
38
+ return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500 });
39
+ }
40
+ };
@@ -0,0 +1,171 @@
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
+ };
@@ -0,0 +1,113 @@
1
+ // src/pages/llms.txt.ts
2
+ import type { APIRoute } from "astro";
3
+ import { getTree, getTreeId, getChannelId, getNodeByPath } from "../lib/cms";
4
+
5
+ // Ensure this route is dynamically rendered (SSR)
6
+ export const prerender = false;
7
+
8
+ export const GET: APIRoute = async (context) => {
9
+ // 1. Setup Cloudflare Worker Fetch Binding safely
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
+ // 2. Load keys from environment or fallback defaults
30
+ const channelId = getChannelId() || import.meta.env.PUBLIC_CHANNEL_ID || "main-channel";
31
+ const treeId = getTreeId() || import.meta.env.PUBLIC_TREE_ID || "main-tree";
32
+
33
+ // 🎯 Extract configurable fallback home slug
34
+ const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
35
+ const workerHomeSlug = cfEnv.PUBLIC_HOME_SLUG || (typeof process !== "undefined" ? process.env?.PUBLIC_HOME_SLUG : undefined);
36
+ const homeSlug = String(envHomeSlug || workerHomeSlug || "home").trim();
37
+
38
+ const rawOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
39
+ const siteOrigin = String(rawOrigin).replace(/\/$/, "");
40
+ const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
41
+
42
+ try {
43
+ // 3. Parallel fetch for Navigation Tree first to resolve root node slug
44
+ const rawNavTree = await getTree(treeId, channelId, customFetch).catch(() => []);
45
+ const navTree = Array.isArray(rawNavTree)
46
+ ? rawNavTree
47
+ : (rawNavTree?.tree || rawNavTree?.items || rawNavTree?.data || []);
48
+
49
+ const rootTreeSlug = navTree.length > 0 ? (navTree[0].slug || navTree[0].key || "") : "";
50
+ const resolvedHomeSlug = rootTreeSlug || homeSlug;
51
+
52
+ // Fetch Home Node summary
53
+ const homeData = await getNodeByPath(resolvedHomeSlug, channelId, false, customFetch).catch(() => null);
54
+
55
+ const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Global Content Delivery at the Edge";
56
+
57
+ // 4. Build standard-compliant llms.txt Markdown
58
+ let markdown = `# ${brandName}\n\n`;
59
+ markdown += `> ${siteSummary}\n\n`;
60
+ markdown += `This file serves as a structured directory for AI agents and language models.\n\n`;
61
+ markdown += `## Site Directory\n\n`;
62
+
63
+ // Recursive tree flattener with link annotations
64
+ const buildList = (nodes: any[], depth = 0) => {
65
+ let list = "";
66
+ const indent = " ".repeat(depth);
67
+
68
+ for (const item of nodes) {
69
+ const nodeData = item.node || item;
70
+ if (!nodeData || nodeData.isHidden) continue;
71
+
72
+ const itemSlug = nodeData.slug || nodeData.path || "";
73
+ const isRootItem = itemSlug === resolvedHomeSlug || itemSlug === "" || itemSlug === "/";
74
+ const itemUrl = isRootItem ? siteOrigin : `${siteOrigin}/${itemSlug.replace(/^\//, "")}`;
75
+
76
+ const itemTitle = nodeData.title || nodeData.label || nodeData.menuTitle || "Untitled";
77
+ const abstract = nodeData.abstract || nodeData.summary || nodeData.description;
78
+ const annotation = abstract ? `: ${abstract.trim()}` : "";
79
+
80
+ list += `${indent}- [${itemTitle}](${itemUrl})${annotation}\n`;
81
+
82
+ if (Array.isArray(item.children) && item.children.length > 0) {
83
+ list += buildList(item.children, depth + 1);
84
+ }
85
+ }
86
+ return list;
87
+ };
88
+
89
+ if (navTree.length > 0) {
90
+ markdown += buildList(navTree);
91
+ } else {
92
+ const homeTitle = homeData?.node?.title || brandName;
93
+ markdown += `- [${homeTitle}](${siteOrigin}): Main landing page.\n`;
94
+ }
95
+
96
+ markdown += `\n---\n*Generated dynamically by Kama Engine.*`;
97
+
98
+ return new Response(markdown, {
99
+ status: 200,
100
+ headers: {
101
+ "Content-Type": "text/plain; charset=utf-8",
102
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
103
+ },
104
+ });
105
+
106
+ } catch (error) {
107
+ console.error("[llms.txt Generator Error]:", error);
108
+ return new Response("# Error\nCould not generate AI directory.", {
109
+ status: 500,
110
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
111
+ });
112
+ }
113
+ };
@@ -0,0 +1,14 @@
1
+ // src/pages/media/[...path].ts
2
+ import type { APIRoute } from "astro";
3
+ import { handleMediaProxy } from "../../lib/media/media-proxy";
4
+
5
+ export const prerender = false;
6
+
7
+ export const GET: APIRoute = async (context) => {
8
+ try {
9
+ return await handleMediaProxy(context);
10
+ } catch (error: any) {
11
+ console.error("❌ [Media Proxy Error]:", error?.message || error);
12
+ return new Response("Media Proxy Failed", { status: 500 });
13
+ }
14
+ };