@brightsideoy/kama 0.1.0-alpha.99 → 1.0.0-alpha.3
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 +775 -251
- package/content/definitions/blocks/button.yaml +24 -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/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 +49 -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-C3LzwJjY.css +1 -0
- package/dist/assets/index-DhguVxzs.js +164 -0
- package/dist/dist-cK3xGnBZ.js +26457 -0
- package/dist/index.html +4 -2
- package/dist/index.js +24028 -7125
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +20 -24
- package/schema.sql +55 -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 +134 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +132 -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 +92 -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 +172 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +62 -0
- package/templates/astro-starter/src/lib/cms.ts +217 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +56 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +129 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +98 -0
- package/templates/astro-starter/src/middleware.ts +435 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +243 -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-D6qQsWOj.js +0 -1392
- package/dist/assets/index-TdW5F0pX.css +0 -1
- 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,243 @@
|
|
|
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 & SYSTEM ROUTE CHECK
|
|
30
|
+
const staticExts = [
|
|
31
|
+
".ico",
|
|
32
|
+
".png",
|
|
33
|
+
".jpg",
|
|
34
|
+
".jpeg",
|
|
35
|
+
".svg",
|
|
36
|
+
".txt",
|
|
37
|
+
".xml",
|
|
38
|
+
".webmanifest",
|
|
39
|
+
".astro",
|
|
40
|
+
".ts",
|
|
41
|
+
".tsx",
|
|
42
|
+
".js",
|
|
43
|
+
".jsx",
|
|
44
|
+
".css",
|
|
45
|
+
".map",
|
|
46
|
+
".json",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const slugStr = String(slug || "");
|
|
50
|
+
// Strip leading slashes to correctly evaluate both "src/..." and "/src/..."
|
|
51
|
+
const cleanSlug = slugStr.replace(/^\/+/, "").toLowerCase();
|
|
52
|
+
|
|
53
|
+
// Check extension regardless of slash placement
|
|
54
|
+
const isStaticAsset = staticExts.some(ext => cleanSlug.endsWith(ext));
|
|
55
|
+
|
|
56
|
+
// Block Vite client internal hits, source file paths, and node_modules
|
|
57
|
+
const isSystemPath =
|
|
58
|
+
cleanSlug.startsWith("src/") ||
|
|
59
|
+
cleanSlug.startsWith("@vite/") ||
|
|
60
|
+
cleanSlug.startsWith("node_modules/") ||
|
|
61
|
+
cleanSlug.startsWith("users/") ||
|
|
62
|
+
cleanSlug.includes("components/") ||
|
|
63
|
+
cleanSlug.includes("npm-test/");
|
|
64
|
+
|
|
65
|
+
if (isStaticAsset || isSystemPath) {
|
|
66
|
+
return new Response(null, { status: 404 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. ENV & WORKER RESOLUTION (Safe Dual-Environment)
|
|
70
|
+
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
71
|
+
const cmsWorker = safeEnv.CMS_WORKER;
|
|
72
|
+
|
|
73
|
+
const envApiUrl = import.meta.env.PUBLIC_API_URL;
|
|
74
|
+
const workerApiUrl = safeEnv.PUBLIC_API_URL;
|
|
75
|
+
const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
|
|
76
|
+
|
|
77
|
+
const cleanApiUrl = baseApiUrl.endsWith("/")
|
|
78
|
+
? baseApiUrl.slice(0, -1)
|
|
79
|
+
: baseApiUrl;
|
|
80
|
+
|
|
81
|
+
const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
82
|
+
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
83
|
+
if (urlStr.startsWith("/")) urlStr = `${cleanApiUrl}${urlStr}`;
|
|
84
|
+
|
|
85
|
+
const urlObj = new URL(urlStr);
|
|
86
|
+
|
|
87
|
+
if (isTimeTraveling) {
|
|
88
|
+
if (revisionId) urlObj.searchParams.set("revisionId", revisionId);
|
|
89
|
+
if (isPreview) urlObj.searchParams.set("preview", "true");
|
|
90
|
+
urlObj.searchParams.set("_t", Date.now().toString());
|
|
91
|
+
} else {
|
|
92
|
+
urlObj.searchParams.set("_cb", Date.now().toString());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
urlStr = urlObj.toString();
|
|
96
|
+
|
|
97
|
+
const reqHeaders: Record<string, string> = {
|
|
98
|
+
...init?.headers,
|
|
99
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
100
|
+
Pragma: "no-cache",
|
|
101
|
+
// 🎯 Direct Service Bindings require explicit header flags to bypass CMS KV cache!
|
|
102
|
+
"X-Bypass-Cache": "true",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const fetchOpts: RequestInit = {
|
|
106
|
+
...init,
|
|
107
|
+
headers: reqHeaders,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const req = new Request(urlStr, fetchOpts);
|
|
111
|
+
|
|
112
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
113
|
+
return cmsWorker.fetch(req);
|
|
114
|
+
}
|
|
115
|
+
return fetch(req);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const channelId = getChannelId();
|
|
119
|
+
const treeId = getTreeId();
|
|
120
|
+
|
|
121
|
+
// 3. PATH & CONCURRENT FETCH RESOLUTION
|
|
122
|
+
const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
|
|
123
|
+
const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
|
|
124
|
+
const targetSlug = pathParts.join("/"); // 🎯 This is now "" for the root URL!
|
|
125
|
+
|
|
126
|
+
let pageData: NodeResponse | null = null;
|
|
127
|
+
let navTree: TreeNode[] = [];
|
|
128
|
+
|
|
129
|
+
// 🎯 Fetch Tree and Node in PARALLEL using the cache-busting customFetch
|
|
130
|
+
try {
|
|
131
|
+
const [treeResult, nodeResult] = await Promise.all([
|
|
132
|
+
getTree(treeId, channelId, customFetch).catch(err => {
|
|
133
|
+
console.error(`❌ [SSR Error] Failed to fetch tree "${treeId}":`, err);
|
|
134
|
+
return [];
|
|
135
|
+
}),
|
|
136
|
+
// 🎯 We just pass the targetSlug (even if it's empty). The backend handles it!
|
|
137
|
+
getNodeByPath(targetSlug, channelId, isPreview, customFetch).catch(err => {
|
|
138
|
+
console.error(
|
|
139
|
+
`❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
|
|
140
|
+
err,
|
|
141
|
+
);
|
|
142
|
+
return null;
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
navTree = treeResult || [];
|
|
147
|
+
pageData = nodeResult;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!pageData || !pageData.node) {
|
|
153
|
+
return new Response("Page Not Found", { status: 404 });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const node = pageData.node;
|
|
157
|
+
const content = pageData.content || {};
|
|
158
|
+
|
|
159
|
+
// 🎯 Vastly simplified homepage detection!
|
|
160
|
+
const isHomePage = targetSlug === "" || node?.path === "/";
|
|
161
|
+
const normalizedSlug = isHomePage ? "" : targetSlug;
|
|
162
|
+
|
|
163
|
+
const headerBlocks = content?.header || [];
|
|
164
|
+
const mainBlocks = content?.content || [];
|
|
165
|
+
const sidebarBlocks = content?.sidebar || [];
|
|
166
|
+
const hasSidebar = sidebarBlocks.length > 0;
|
|
167
|
+
|
|
168
|
+
// Detect if this node is a standalone Page Skeleton
|
|
169
|
+
const isSkeleton = node?.nodeType === "page-skeleton";
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
isSkeleton ? (
|
|
174
|
+
<SkeletonLayout
|
|
175
|
+
node={node}
|
|
176
|
+
content={content}
|
|
177
|
+
language={pageData?.language}
|
|
178
|
+
normalizedSlug={normalizedSlug}
|
|
179
|
+
isHomePage={isHomePage}
|
|
180
|
+
>
|
|
181
|
+
<BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
|
|
182
|
+
</SkeletonLayout>
|
|
183
|
+
) : (
|
|
184
|
+
<StandardLayout
|
|
185
|
+
node={node}
|
|
186
|
+
content={content}
|
|
187
|
+
language={pageData?.language}
|
|
188
|
+
normalizedSlug={normalizedSlug}
|
|
189
|
+
isHomePage={isHomePage}
|
|
190
|
+
hasSidebar={hasSidebar}
|
|
191
|
+
headerBlocks={headerBlocks}
|
|
192
|
+
tree={navTree}
|
|
193
|
+
>
|
|
194
|
+
{headerBlocks.length > 0 && (
|
|
195
|
+
<section class="region-header">
|
|
196
|
+
<BlockRenderer
|
|
197
|
+
blocks={headerBlocks}
|
|
198
|
+
contentMap={content}
|
|
199
|
+
region="header"
|
|
200
|
+
/>
|
|
201
|
+
</section>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{!isHomePage && (
|
|
205
|
+
<div class="breadcrumb-wrapper">
|
|
206
|
+
<Breadcrumb title={node?.title || "Page"} slug={normalizedSlug} />
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
<div class:list={["content-layout", { "has-sidebar": hasSidebar }]}>
|
|
211
|
+
<div class="main-column">
|
|
212
|
+
{mainBlocks.length > 0 ? (
|
|
213
|
+
<section class="region-main">
|
|
214
|
+
<BlockRenderer
|
|
215
|
+
blocks={mainBlocks}
|
|
216
|
+
contentMap={content}
|
|
217
|
+
region="main"
|
|
218
|
+
/>
|
|
219
|
+
</section>
|
|
220
|
+
) : (
|
|
221
|
+
<div class="empty-content">
|
|
222
|
+
<p data-cms-field="summary">
|
|
223
|
+
{node?.summary || "No content available for this page."}
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{hasSidebar && (
|
|
230
|
+
<aside class="sidebar-column">
|
|
231
|
+
<section class="region-sidebar">
|
|
232
|
+
<BlockRenderer
|
|
233
|
+
blocks={sidebarBlocks}
|
|
234
|
+
contentMap={content}
|
|
235
|
+
region="sidebar"
|
|
236
|
+
/>
|
|
237
|
+
</section>
|
|
238
|
+
</aside>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</StandardLayout>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/pages/partials/footer.astro
|
|
3
|
+
export const partial = true;
|
|
4
|
+
|
|
5
|
+
import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
|
|
6
|
+
import { getChannelId } from "@/lib/cms";
|
|
7
|
+
|
|
8
|
+
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
9
|
+
const cmsWorker = safeEnv.CMS_WORKER;
|
|
10
|
+
const channelId = getChannelId() || "website";
|
|
11
|
+
|
|
12
|
+
const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
13
|
+
const reqInit = { ...init };
|
|
14
|
+
delete reqInit.cache;
|
|
15
|
+
|
|
16
|
+
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
17
|
+
if (urlStr.startsWith("/")) {
|
|
18
|
+
const baseUrl =
|
|
19
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
20
|
+
safeEnv.PUBLIC_API_URL ||
|
|
21
|
+
"http://127.0.0.1:8787";
|
|
22
|
+
urlStr = `${baseUrl}${urlStr}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const req = new Request(urlStr, reqInit);
|
|
26
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function")
|
|
27
|
+
return cmsWorker.fetch(req);
|
|
28
|
+
return fetch(req);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 🎯 Resolve the Home Page slug
|
|
32
|
+
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
|
|
33
|
+
|
|
34
|
+
let footerBlocks: any[] = [];
|
|
35
|
+
let contentMap: Record<string, any[]> = {};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// 🎯 Fetch the Home Page node from Kama CMS Public API
|
|
39
|
+
const res = await customFetch(
|
|
40
|
+
`/api/v1/node/${homeSlug}?channelId=${channelId}`,
|
|
41
|
+
);
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
|
|
45
|
+
// Kama CMS public node endpoints usually organize blocks inside a 'content' object mapped by region
|
|
46
|
+
const allContent = data.content || {};
|
|
47
|
+
|
|
48
|
+
// Extract specifically the footer region from the Home page
|
|
49
|
+
footerBlocks = allContent.footer || [];
|
|
50
|
+
contentMap = allContent;
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("[Partials/Footer] Failed to fetch home page blocks:", e);
|
|
54
|
+
}
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
footerBlocks && footerBlocks.length > 0 ? (
|
|
59
|
+
<BlockRenderer
|
|
60
|
+
blocks={footerBlocks}
|
|
61
|
+
contentMap={contentMap}
|
|
62
|
+
region="footer"
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<div class="footer-default">
|
|
66
|
+
<p>© {new Date().getFullYear()} all rights reserved.</p>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|