@brightsideoy/kama 0.1.0-alpha.12 โ 0.1.0-alpha.121
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 +195 -0
- package/bin/cli.js +602 -0
- package/content/definitions/block-layouts/three-column.yaml +20 -0
- package/content/definitions/block-layouts/two-column.yaml +29 -0
- 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 +12 -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 +17 -0
- package/content/definitions/field-sets/access.yaml +27 -0
- package/content/definitions/field-sets/channel-base.yaml +50 -0
- package/content/definitions/field-sets/channel-seo.yaml +21 -0
- package/content/definitions/field-sets/comments.yaml +11 -0
- 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/content/definitions/field-sets/page-social.yaml +78 -0
- package/content/definitions/field-sets/schema-type.yaml +43 -0
- package/content/definitions/field-sets/tree-base.yaml +31 -0
- package/content/definitions/field-sets/visibility.yaml +8 -0
- package/content/definitions/node-layouts/content.yml +12 -0
- package/content/definitions/node-layouts/landing-page.yml +25 -0
- package/content/definitions/node-layouts/single-column.yml +44 -0
- package/content/definitions/node-layouts/two-column.yml +49 -0
- package/content/definitions/nodes/address.yaml +40 -0
- package/content/definitions/nodes/article.yaml +48 -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/content/definitions/nodes/page.yaml +57 -0
- package/content/definitions/nodes/person.yaml +41 -0
- package/content/definitions/nodes/product.yaml +89 -0
- package/content/definitions/trees/navigation.yaml +24 -0
- package/definitions/editor/button.ts +11 -0
- package/definitions/editor/default.ts +21 -0
- package/definitions/editor/header.ts +36 -0
- package/definitions/editor/minimal.ts +15 -0
- package/definitions/validators/iban.ts +12 -0
- package/definitions/validators/magic-word.ts +7 -0
- package/dist/assets/index-C6GoXL-R.js +162 -0
- package/dist/assets/index-Drmp3h6P.css +1 -0
- package/dist/index.html +13 -0
- package/dist/index.js +12209 -2045
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +14 -11
- package/schema.sql +204 -0
- package/templates/astro-starter/astro.config.mjs +40 -0
- package/templates/astro-starter/middleware/api-purge.ts +130 -0
- package/templates/astro-starter/middleware/index.ts +10 -0
- package/templates/astro-starter/middleware/kv-cache.ts +76 -0
- package/templates/astro-starter/middleware/response-headers.ts +43 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/public/css/standard-theme.css +157 -0
- package/templates/astro-starter/public/robots.txt +23 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +133 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +24 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
- package/templates/astro-starter/src/components/global/Footer.astro +48 -0
- package/templates/astro-starter/src/components/global/Header.astro +92 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
- package/templates/astro-starter/src/components/seo/SeoHead.astro +150 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +149 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +45 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
- package/templates/astro-starter/src/lib/cms.ts +149 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +41 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +127 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +34 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +201 -0
- package/templates/astro-starter/src/pages/llms-full.txt.ts +161 -0
- package/templates/astro-starter/src/pages/llms.txt.ts +107 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +7 -0
- package/templates/astro-starter/src/utils/tiptap.ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/test-local.sh +76 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/pages/[...slug].astro
|
|
3
|
+
import StandardLayout from "../layouts/StandardLayout.astro";
|
|
4
|
+
import SkeletonLayout from "../layouts/SkeletonLayout.astro";
|
|
5
|
+
import BlockRenderer from "../components/blocks/BlockRenderer.astro";
|
|
6
|
+
import Breadcrumb from "../components/global/Breadcrumb.astro";
|
|
7
|
+
import { getNodeByPath, getChannelId, getTree, getTreeId } from "../lib/cms";
|
|
8
|
+
import type { NodeResponse, TreeNode } from "../lib/cms";
|
|
9
|
+
import { env } from "cloudflare:workers";
|
|
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
|
+
Astro.response.headers.set(
|
|
21
|
+
"Cache-Control",
|
|
22
|
+
"no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// 1. STATIC ASSET CHECK
|
|
26
|
+
const staticExts = [
|
|
27
|
+
".ico",
|
|
28
|
+
".png",
|
|
29
|
+
".jpg",
|
|
30
|
+
".jpeg",
|
|
31
|
+
".svg",
|
|
32
|
+
".txt",
|
|
33
|
+
".xml",
|
|
34
|
+
".webmanifest",
|
|
35
|
+
];
|
|
36
|
+
const slugStr = String(slug || "");
|
|
37
|
+
const isStaticAsset =
|
|
38
|
+
!slugStr.includes("/") &&
|
|
39
|
+
staticExts.some(ext => slugStr.toLowerCase().endsWith(ext));
|
|
40
|
+
|
|
41
|
+
if (isStaticAsset) {
|
|
42
|
+
return new Response(null, { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. PATH RESOLUTION
|
|
46
|
+
const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
|
|
47
|
+
const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
|
|
48
|
+
const pathToFetch = pathParts.length > 0 ? pathParts.join("/") : "home";
|
|
49
|
+
|
|
50
|
+
// 3. ENV & WORKER RESOLUTION
|
|
51
|
+
const locals = (Astro.locals as any) || {};
|
|
52
|
+
// SAFELY EXTRACT WORKER ENV USING THE CLOUDFLARE MODULE
|
|
53
|
+
const workerEnv = env || process.env || {};
|
|
54
|
+
const cmsWorker = locals.cfContext?.env?.CMS_WORKER || workerEnv.CMS_WORKER;
|
|
55
|
+
|
|
56
|
+
// CRITICAL FIX: Flattened to single line to bypass Rolldown parser bug
|
|
57
|
+
const envApiUrl = import.meta.env.PUBLIC_API_URL;
|
|
58
|
+
const workerApiUrl = workerEnv.PUBLIC_API_URL;
|
|
59
|
+
const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
|
|
60
|
+
|
|
61
|
+
const cleanApiUrl = baseApiUrl.endsWith("/")
|
|
62
|
+
? baseApiUrl.slice(0, -1)
|
|
63
|
+
: baseApiUrl;
|
|
64
|
+
|
|
65
|
+
const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
66
|
+
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
67
|
+
if (urlStr.startsWith("/")) urlStr = `${cleanApiUrl}${urlStr}`;
|
|
68
|
+
|
|
69
|
+
const urlObj = new URL(urlStr);
|
|
70
|
+
urlObj.searchParams.set("_t", Date.now().toString());
|
|
71
|
+
|
|
72
|
+
// Inject Revision ID and Preview flag if we are time-traveling
|
|
73
|
+
if (revisionId) urlObj.searchParams.set("revisionId", revisionId);
|
|
74
|
+
if (isPreview) urlObj.searchParams.set("preview", "true");
|
|
75
|
+
|
|
76
|
+
urlStr = urlObj.toString();
|
|
77
|
+
|
|
78
|
+
const req = new Request(urlStr, {
|
|
79
|
+
...init,
|
|
80
|
+
cache: "no-store",
|
|
81
|
+
headers: {
|
|
82
|
+
...init?.headers,
|
|
83
|
+
"X-Bypass-Cache": "true",
|
|
84
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
89
|
+
return cmsWorker.fetch(req);
|
|
90
|
+
}
|
|
91
|
+
return fetch(req);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const channelId = getChannelId();
|
|
95
|
+
const treeId = getTreeId();
|
|
96
|
+
|
|
97
|
+
let pageData: NodeResponse | null = null;
|
|
98
|
+
let navTree: TreeNode[] = [];
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const [fetchedPage, fetchedTree] = await Promise.all([
|
|
102
|
+
getNodeByPath(pathToFetch, channelId, isPreview, customFetch),
|
|
103
|
+
getTree(treeId, channelId, customFetch),
|
|
104
|
+
]);
|
|
105
|
+
pageData = fetchedPage;
|
|
106
|
+
navTree = fetchedTree;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(
|
|
109
|
+
`โ [SSR Error] Failed to fetch data for path "${pathToFetch}":`,
|
|
110
|
+
err,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!pageData || !pageData.node) {
|
|
115
|
+
return new Response("Page Not Found", { status: 404 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const node = pageData.node;
|
|
119
|
+
const content = pageData.content || {};
|
|
120
|
+
|
|
121
|
+
const isHomePage =
|
|
122
|
+
pathToFetch === "home" || node?.path === "/" || pathToFetch === "";
|
|
123
|
+
const normalizedSlug = isHomePage ? "" : pathToFetch;
|
|
124
|
+
|
|
125
|
+
const headerBlocks = content?.header || [];
|
|
126
|
+
const mainBlocks = content?.content || [];
|
|
127
|
+
const sidebarBlocks = content?.sidebar || [];
|
|
128
|
+
const hasSidebar = sidebarBlocks.length > 0;
|
|
129
|
+
|
|
130
|
+
// ๐ฏ Detect if this node is a standalone Page Skeleton
|
|
131
|
+
const isSkeleton = node?.nodeType === "page-skeleton";
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
isSkeleton ? (
|
|
136
|
+
<SkeletonLayout
|
|
137
|
+
node={node}
|
|
138
|
+
content={content}
|
|
139
|
+
normalizedSlug={normalizedSlug}
|
|
140
|
+
isHomePage={isHomePage}
|
|
141
|
+
>
|
|
142
|
+
<BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
|
|
143
|
+
</SkeletonLayout>
|
|
144
|
+
) : (
|
|
145
|
+
<StandardLayout
|
|
146
|
+
node={node}
|
|
147
|
+
content={content}
|
|
148
|
+
normalizedSlug={normalizedSlug}
|
|
149
|
+
isHomePage={isHomePage}
|
|
150
|
+
hasSidebar={hasSidebar}
|
|
151
|
+
headerBlocks={headerBlocks}
|
|
152
|
+
tree={navTree}
|
|
153
|
+
>
|
|
154
|
+
{headerBlocks.length > 0 && (
|
|
155
|
+
<section class="region-header">
|
|
156
|
+
<BlockRenderer
|
|
157
|
+
blocks={headerBlocks}
|
|
158
|
+
contentMap={content}
|
|
159
|
+
region="header"
|
|
160
|
+
/>
|
|
161
|
+
</section>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{!isHomePage && (
|
|
165
|
+
<div class="breadcrumb-wrapper">
|
|
166
|
+
<Breadcrumb title={node?.title || "Page"} slug={normalizedSlug} />
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<div class:list={["content-layout", { "has-sidebar": hasSidebar }]}>
|
|
171
|
+
<div class="main-column">
|
|
172
|
+
{mainBlocks.length > 0 ? (
|
|
173
|
+
<section class="region-main">
|
|
174
|
+
<BlockRenderer
|
|
175
|
+
blocks={mainBlocks}
|
|
176
|
+
contentMap={content}
|
|
177
|
+
region="main"
|
|
178
|
+
/>
|
|
179
|
+
</section>
|
|
180
|
+
) : (
|
|
181
|
+
<div class="empty-content">
|
|
182
|
+
<p>{node?.summary || "No content available for this page."}</p>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{hasSidebar && (
|
|
188
|
+
<aside class="sidebar-column">
|
|
189
|
+
<section class="region-sidebar">
|
|
190
|
+
<BlockRenderer
|
|
191
|
+
blocks={sidebarBlocks}
|
|
192
|
+
contentMap={content}
|
|
193
|
+
region="sidebar"
|
|
194
|
+
/>
|
|
195
|
+
</section>
|
|
196
|
+
</aside>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</StandardLayout>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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 || "website";
|
|
30
|
+
const treeId = getTreeId() || import.meta.env.PUBLIC_TREE_ID || "main-navigation";
|
|
31
|
+
|
|
32
|
+
const rawOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
|
|
33
|
+
const siteOrigin = String(rawOrigin).replace(/\/$/, "");
|
|
34
|
+
const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 2. Fetch Home Node (for global context) and Navigation Tree
|
|
38
|
+
const [homeData, rawNavTree] = await Promise.all([
|
|
39
|
+
getNodeByPath("home", channelId, false, customFetch).catch(() => null),
|
|
40
|
+
getTree(treeId, channelId, customFetch).catch(() => []),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const navTree = Array.isArray(rawNavTree)
|
|
44
|
+
? rawNavTree
|
|
45
|
+
: (rawNavTree?.tree || rawNavTree?.items || rawNavTree?.data || []);
|
|
46
|
+
|
|
47
|
+
const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Full Application Documentation Corpus";
|
|
48
|
+
|
|
49
|
+
// 3. Header section matching llmstxt.org spec
|
|
50
|
+
let fullText = `# ${brandName} - Full Corpus\n\n`;
|
|
51
|
+
fullText += `> ${siteSummary}\n\n`;
|
|
52
|
+
fullText += `This file contains the complete unabridged text content for all pages across the application.\n\n`;
|
|
53
|
+
|
|
54
|
+
// Helper to extract text from block data (handles TipTap AST & raw strings)
|
|
55
|
+
const parseBlockText = (blockData: any): string => {
|
|
56
|
+
if (!blockData) return "";
|
|
57
|
+
let rawText = "";
|
|
58
|
+
|
|
59
|
+
if (typeof blockData === "string") {
|
|
60
|
+
rawText = blockData;
|
|
61
|
+
} else if (blockData.type === "doc") {
|
|
62
|
+
rawText = renderTiptapNode(blockData);
|
|
63
|
+
} else if (blockData.body) {
|
|
64
|
+
rawText = parseBlockText(blockData.body);
|
|
65
|
+
} else if (blockData.text) {
|
|
66
|
+
rawText = parseBlockText(blockData.text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convert basic HTML block tags to plain text linebreaks
|
|
70
|
+
return rawText
|
|
71
|
+
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "## $1\n")
|
|
72
|
+
.replace(/<p[^>]*>(.*?)<\/p>/gi, "$1\n")
|
|
73
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
74
|
+
.replace(/<[^>]+>/g, ""); // Strip any remaining tags
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Recursive walker to fetch each page's full content
|
|
78
|
+
const processNodes = async (nodes: any[], depth = 1) => {
|
|
79
|
+
let sectionContent = "";
|
|
80
|
+
|
|
81
|
+
for (const item of nodes) {
|
|
82
|
+
const nodeData = item.node || item;
|
|
83
|
+
if (!nodeData || nodeData.isHidden) continue;
|
|
84
|
+
|
|
85
|
+
const rawPath = nodeData.path || nodeData.slug || "/";
|
|
86
|
+
const cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
87
|
+
const itemUrl = cleanPath === "/" ? siteOrigin : `${siteOrigin}${cleanPath}`;
|
|
88
|
+
const itemTitle = nodeData.title || nodeData.label || "Untitled Page";
|
|
89
|
+
|
|
90
|
+
// Fetch node's block contents
|
|
91
|
+
const pageData = await getNodeByPath(
|
|
92
|
+
cleanPath.replace(/^\//, "") || "home",
|
|
93
|
+
channelId,
|
|
94
|
+
false,
|
|
95
|
+
customFetch
|
|
96
|
+
).catch(() => null);
|
|
97
|
+
|
|
98
|
+
// Add Document Title Header
|
|
99
|
+
sectionContent += `\n---\n\n`;
|
|
100
|
+
sectionContent += `# ${itemTitle}\n`;
|
|
101
|
+
sectionContent += `**URL:** ${itemUrl}\n`;
|
|
102
|
+
if (nodeData.abstract || nodeData.summary) {
|
|
103
|
+
sectionContent += `**Abstract:** ${nodeData.abstract || nodeData.summary}\n`;
|
|
104
|
+
}
|
|
105
|
+
sectionContent += `\n`;
|
|
106
|
+
|
|
107
|
+
// Render all blocks on the node
|
|
108
|
+
const regions = pageData?.content || {};
|
|
109
|
+
let blockCount = 0;
|
|
110
|
+
|
|
111
|
+
Object.entries(regions).forEach(([regionName, blocks]) => {
|
|
112
|
+
if (!Array.isArray(blocks)) return;
|
|
113
|
+
|
|
114
|
+
blocks.forEach((block: any) => {
|
|
115
|
+
const data = block.data || {};
|
|
116
|
+
const renderedText = parseBlockText(data);
|
|
117
|
+
|
|
118
|
+
if (renderedText && renderedText.trim()) {
|
|
119
|
+
sectionContent += `${renderedText.trim()}\n\n`;
|
|
120
|
+
blockCount++;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (blockCount === 0) {
|
|
126
|
+
sectionContent += `*(No text content available for this node)*\n\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Recursively process child pages
|
|
130
|
+
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
131
|
+
sectionContent += await processNodes(item.children, depth + 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return sectionContent;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (navTree.length > 0) {
|
|
139
|
+
fullText += await processNodes(navTree);
|
|
140
|
+
} else {
|
|
141
|
+
fullText += `\n---\n\n# Home\n**URL:** ${siteOrigin}\n\nNo additional pages found in navigation tree.\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fullText += `\n---\n*Generated dynamically by Kama Engine.*`;
|
|
145
|
+
|
|
146
|
+
return new Response(fullText, {
|
|
147
|
+
status: 200,
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
150
|
+
"Cache-Control": "public, max-age=3600, s-maxage=86400"
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("[llms-full.txt Generator Error]:", error);
|
|
156
|
+
return new Response("# Error\nCould not generate full AI context corpus.", {
|
|
157
|
+
status: 500,
|
|
158
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
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 || "website";
|
|
31
|
+
const treeId = getTreeId() || import.meta.env.PUBLIC_TREE_ID || "main-navigation";
|
|
32
|
+
|
|
33
|
+
const rawOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
|
|
34
|
+
const siteOrigin = String(rawOrigin).replace(/\/$/, "");
|
|
35
|
+
const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// 3. Parallel fetch for Home Node summary and Navigation Tree
|
|
39
|
+
const [homeData, rawNavTree] = await Promise.all([
|
|
40
|
+
getNodeByPath("home", channelId, false, customFetch).catch(() => null),
|
|
41
|
+
getTree(treeId, channelId, customFetch).catch(() => []),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Normalize tree data payload across direct array or wrapped responses
|
|
45
|
+
const navTree = Array.isArray(rawNavTree)
|
|
46
|
+
? rawNavTree
|
|
47
|
+
: (rawNavTree?.tree || rawNavTree?.items || rawNavTree?.data || []);
|
|
48
|
+
|
|
49
|
+
const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Global Content Delivery at the Edge";
|
|
50
|
+
|
|
51
|
+
// 4. Build standard-compliant llms.txt Markdown
|
|
52
|
+
let markdown = `# ${brandName}\n\n`;
|
|
53
|
+
markdown += `> ${siteSummary}\n\n`;
|
|
54
|
+
markdown += `This file serves as a structured directory for AI agents and language models.\n\n`;
|
|
55
|
+
markdown += `## Site Directory\n\n`;
|
|
56
|
+
|
|
57
|
+
// Recursive tree flattener with link annotations
|
|
58
|
+
const buildList = (nodes: any[], depth = 0) => {
|
|
59
|
+
let list = "";
|
|
60
|
+
const indent = " ".repeat(depth);
|
|
61
|
+
|
|
62
|
+
for (const item of nodes) {
|
|
63
|
+
const nodeData = item.node || item;
|
|
64
|
+
if (!nodeData || nodeData.isHidden) continue;
|
|
65
|
+
|
|
66
|
+
const rawPath = nodeData.path || nodeData.slug || "/";
|
|
67
|
+
const cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
68
|
+
const itemUrl = cleanPath === "/" ? siteOrigin : `${siteOrigin}${cleanPath}`;
|
|
69
|
+
|
|
70
|
+
const itemTitle = nodeData.title || nodeData.label || nodeData.menuTitle || "Untitled";
|
|
71
|
+
const abstract = nodeData.abstract || nodeData.summary || nodeData.description;
|
|
72
|
+
const annotation = abstract ? `: ${abstract.trim()}` : "";
|
|
73
|
+
|
|
74
|
+
list += `${indent}- [${itemTitle}](${itemUrl})${annotation}\n`;
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
77
|
+
list += buildList(item.children, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return list;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (navTree.length > 0) {
|
|
84
|
+
markdown += buildList(navTree);
|
|
85
|
+
} else {
|
|
86
|
+
const homeTitle = homeData?.node?.title || brandName;
|
|
87
|
+
markdown += `- [${homeTitle}](${siteOrigin}): Main landing page.\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
markdown += `\n---\n*Generated dynamically by Kama Engine.*`;
|
|
91
|
+
|
|
92
|
+
return new Response(markdown, {
|
|
93
|
+
status: 200,
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
96
|
+
"Cache-Control": "public, max-age=3600, s-maxage=86400"
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("[llms.txt Generator Error]:", error);
|
|
102
|
+
return new Response("# Error\nCould not generate AI directory.", {
|
|
103
|
+
status: 500,
|
|
104
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/utils/tiptap.ts
|
|
2
|
+
|
|
3
|
+
export interface TiptapNode {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
attrs?: Record<string, any>;
|
|
7
|
+
marks?: Array<{ type: string; attrs?: Record<string, any> }>;
|
|
8
|
+
content?: TiptapNode[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Traverses a ProseMirror / Tiptap AST node and converts it into HTML.
|
|
13
|
+
*/
|
|
14
|
+
export function renderTiptapNode(node?: TiptapNode | null): string {
|
|
15
|
+
if (!node) return "";
|
|
16
|
+
if (typeof node === "string") return node;
|
|
17
|
+
|
|
18
|
+
// 1. Text Node Handling (with Marks)
|
|
19
|
+
if (node.type === "text") {
|
|
20
|
+
let text = escapeHtml(node.text || "");
|
|
21
|
+
if (node.marks && Array.isArray(node.marks)) {
|
|
22
|
+
node.marks.forEach((mark) => {
|
|
23
|
+
switch (mark.type) {
|
|
24
|
+
case "bold":
|
|
25
|
+
case "strong":
|
|
26
|
+
text = `<strong>${text}</strong>`;
|
|
27
|
+
break;
|
|
28
|
+
case "italic":
|
|
29
|
+
case "em":
|
|
30
|
+
text = `<em>${text}</em>`;
|
|
31
|
+
break;
|
|
32
|
+
case "underline":
|
|
33
|
+
text = `<u>${text}</u>`;
|
|
34
|
+
break;
|
|
35
|
+
case "strike":
|
|
36
|
+
text = `<s>${text}</s>`;
|
|
37
|
+
break;
|
|
38
|
+
case "code":
|
|
39
|
+
text = `<code>${text}</code>`;
|
|
40
|
+
break;
|
|
41
|
+
case "link": {
|
|
42
|
+
const href = mark.attrs?.href ? escapeHtml(mark.attrs.href) : "#";
|
|
43
|
+
const target = mark.attrs?.target ? ` target="${escapeHtml(mark.attrs.target)}"` : "";
|
|
44
|
+
const rel = mark.attrs?.target === "_blank" ? ' rel="noopener noreferrer"' : "";
|
|
45
|
+
text = `<a href="${href}"${target}${rel}>${text}</a>`;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Traversal of Child Nodes
|
|
55
|
+
const childrenHtml = Array.isArray(node.content)
|
|
56
|
+
? node.content.map(renderTiptapNode).join("")
|
|
57
|
+
: "";
|
|
58
|
+
|
|
59
|
+
// 3. Block Level Mapping
|
|
60
|
+
switch (node.type) {
|
|
61
|
+
case "doc":
|
|
62
|
+
return childrenHtml;
|
|
63
|
+
case "paragraph":
|
|
64
|
+
return childrenHtml.trim() ? `<p>${childrenHtml}</p>` : "";
|
|
65
|
+
case "heading": {
|
|
66
|
+
const level = Math.min(Math.max(node.attrs?.level || 2, 1), 6);
|
|
67
|
+
return `<h${level}>${childrenHtml}</h${level}>`;
|
|
68
|
+
}
|
|
69
|
+
case "bulletList":
|
|
70
|
+
return `<ul>${childrenHtml}</ul>`;
|
|
71
|
+
case "orderedList": {
|
|
72
|
+
const start = node.attrs?.start ? ` start="${node.attrs.start}"` : "";
|
|
73
|
+
return `<ol${start}>${childrenHtml}</ol>`;
|
|
74
|
+
}
|
|
75
|
+
case "listItem":
|
|
76
|
+
return `<li>${childrenHtml}</li>`;
|
|
77
|
+
case "blockquote":
|
|
78
|
+
return `<blockquote>${childrenHtml}</blockquote>`;
|
|
79
|
+
case "codeBlock": {
|
|
80
|
+
const lang = node.attrs?.language ? ` class="language-${escapeHtml(node.attrs.language)}"` : "";
|
|
81
|
+
return `<pre><code${lang}>${childrenHtml}</code></pre>`;
|
|
82
|
+
}
|
|
83
|
+
case "horizontalRule":
|
|
84
|
+
return "<hr />";
|
|
85
|
+
case "hardBreak":
|
|
86
|
+
return "<br />";
|
|
87
|
+
default:
|
|
88
|
+
return childrenHtml;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Universal helper that handles plain strings, legacy objects, and Tiptap ASTs safely.
|
|
94
|
+
*/
|
|
95
|
+
export function extractHtml(value: any): string {
|
|
96
|
+
if (!value) return "";
|
|
97
|
+
if (typeof value === "string") return value; // Already HTML/string
|
|
98
|
+
if (typeof value === "object") {
|
|
99
|
+
if (value.type === "doc") return renderTiptapNode(value);
|
|
100
|
+
if (value.html) return value.html;
|
|
101
|
+
if (value.markup) return value.markup;
|
|
102
|
+
}
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function escapeHtml(str: string): string {
|
|
107
|
+
return str
|
|
108
|
+
.replace(/&/g, "&")
|
|
109
|
+
.replace(/</g, "<")
|
|
110
|
+
.replace(/>/g, ">")
|
|
111
|
+
.replace(/"/g, """)
|
|
112
|
+
.replace(/'/g, "'");
|
|
113
|
+
}
|
|
File without changes
|
package/test-local.sh
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Exit immediately if any command fails
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Define absolute test directory path
|
|
7
|
+
TEST_DIR="$(cd .. && pwd)/npm-test"
|
|
8
|
+
|
|
9
|
+
echo "๐งน 1. Nuking old test environment to start fresh..."
|
|
10
|
+
rm -rf "$TEST_DIR"
|
|
11
|
+
mkdir -p "$TEST_DIR"
|
|
12
|
+
|
|
13
|
+
# ๐ฏ KILL THE NPX CACHE: Forces npx to unpack the fresh tarball for the CLI
|
|
14
|
+
echo "๐ Clearing NPX cache..."
|
|
15
|
+
rm -rf ~/.npm/_npx/
|
|
16
|
+
|
|
17
|
+
# Make sure the CLI file is actually executable before packing!
|
|
18
|
+
chmod +x bin/cli.js
|
|
19
|
+
|
|
20
|
+
echo "๐ฅ 2. Packing KAMA locally (this will trigger build scripts)..."
|
|
21
|
+
npm pack
|
|
22
|
+
|
|
23
|
+
# Find the newly created tarball
|
|
24
|
+
TARBALL=$(ls -1t *.tgz | head -n 1)
|
|
25
|
+
|
|
26
|
+
if [ -z "$TARBALL" ]; then
|
|
27
|
+
echo "โ Error: Failed to find the packed .tgz file."
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "โ
Packaged successfully: $TARBALL"
|
|
32
|
+
|
|
33
|
+
echo "๐ 3. Moving tarball to test environment..."
|
|
34
|
+
mv "$TARBALL" "$TEST_DIR/"
|
|
35
|
+
|
|
36
|
+
echo "๐ 4. Entering test environment..."
|
|
37
|
+
cd "$TEST_DIR"
|
|
38
|
+
|
|
39
|
+
echo "๐ 5. Simulating 'npx kama init' using the local tarball..."
|
|
40
|
+
echo "------------------------------------------------------"
|
|
41
|
+
|
|
42
|
+
# Explicitly define the package source and the binary to run
|
|
43
|
+
npx --yes --package "$TEST_DIR/$TARBALL" kama init
|
|
44
|
+
|
|
45
|
+
echo "------------------------------------------------------"
|
|
46
|
+
echo "๐ 6. Detecting generated workspace..."
|
|
47
|
+
|
|
48
|
+
# Figure out if user initialized in current folder (.) or a subfolder
|
|
49
|
+
if [ -f "package.json" ]; then
|
|
50
|
+
WORKSPACE_DIR="."
|
|
51
|
+
else
|
|
52
|
+
# Find the first created directory that contains a package.json
|
|
53
|
+
WORKSPACE_DIR=$(find . -mindepth 2 -maxdepth 2 -name "package.json" | head -n 1 | xargs dirname)
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Sanity check
|
|
57
|
+
if [ ! -f "$WORKSPACE_DIR/package.json" ]; then
|
|
58
|
+
echo "โ Could not find a package.json. Did the setup abort?"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
echo "โ
Found workspace at: $WORKSPACE_DIR"
|
|
63
|
+
cd "$WORKSPACE_DIR"
|
|
64
|
+
|
|
65
|
+
# ๐ฏ FULL INSTALL + LOCAL OVERRIDE:
|
|
66
|
+
echo "๐ฆ 7. Installing all project dependencies..."
|
|
67
|
+
npm install
|
|
68
|
+
|
|
69
|
+
echo "๐ Overriding @brightsideoy/kama with local tarball..."
|
|
70
|
+
npm install "$TEST_DIR/$TARBALL" --save
|
|
71
|
+
|
|
72
|
+
echo "๐ฅ 8. Booting up KAMA (CMS & Astro Site)..."
|
|
73
|
+
echo "๐ You should see localhost URLs below to click and test."
|
|
74
|
+
echo " (Press Ctrl+C to stop the servers. Running this script again will wipe everything and restart.)"
|
|
75
|
+
echo "------------------------------------------------------"
|
|
76
|
+
npm run dev
|