@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.
Files changed (86) hide show
  1. package/README.md +195 -0
  2. package/bin/cli.js +602 -0
  3. package/content/definitions/block-layouts/three-column.yaml +20 -0
  4. package/content/definitions/block-layouts/two-column.yaml +29 -0
  5. package/content/definitions/blocks/button.yaml +24 -0
  6. package/content/definitions/blocks/header.yaml +13 -0
  7. package/content/definitions/blocks/header_image.yaml +21 -0
  8. package/content/definitions/blocks/hero.yaml +28 -0
  9. package/content/definitions/blocks/html.yaml +12 -0
  10. package/content/definitions/blocks/image_card.yaml +56 -0
  11. package/content/definitions/blocks/single_image.yaml +27 -0
  12. package/content/definitions/blocks/text.yaml +15 -0
  13. package/content/definitions/channels/ecommerce.yaml +28 -0
  14. package/content/definitions/channels/website.yaml +17 -0
  15. package/content/definitions/field-sets/access.yaml +27 -0
  16. package/content/definitions/field-sets/channel-base.yaml +50 -0
  17. package/content/definitions/field-sets/channel-seo.yaml +21 -0
  18. package/content/definitions/field-sets/comments.yaml +11 -0
  19. package/content/definitions/field-sets/content-block.yaml +41 -0
  20. package/content/definitions/field-sets/page-base.yaml +44 -0
  21. package/content/definitions/field-sets/page-seo.yaml +34 -0
  22. package/content/definitions/field-sets/page-social.yaml +78 -0
  23. package/content/definitions/field-sets/schema-type.yaml +43 -0
  24. package/content/definitions/field-sets/tree-base.yaml +31 -0
  25. package/content/definitions/field-sets/visibility.yaml +8 -0
  26. package/content/definitions/node-layouts/content.yml +12 -0
  27. package/content/definitions/node-layouts/landing-page.yml +25 -0
  28. package/content/definitions/node-layouts/single-column.yml +44 -0
  29. package/content/definitions/node-layouts/two-column.yml +49 -0
  30. package/content/definitions/nodes/address.yaml +40 -0
  31. package/content/definitions/nodes/article.yaml +48 -0
  32. package/content/definitions/nodes/event.yaml +55 -0
  33. package/content/definitions/nodes/external-link.yaml +29 -0
  34. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  35. package/content/definitions/nodes/page.yaml +57 -0
  36. package/content/definitions/nodes/person.yaml +41 -0
  37. package/content/definitions/nodes/product.yaml +89 -0
  38. package/content/definitions/trees/navigation.yaml +24 -0
  39. package/definitions/editor/button.ts +11 -0
  40. package/definitions/editor/default.ts +21 -0
  41. package/definitions/editor/header.ts +36 -0
  42. package/definitions/editor/minimal.ts +15 -0
  43. package/definitions/validators/iban.ts +12 -0
  44. package/definitions/validators/magic-word.ts +7 -0
  45. package/dist/assets/index-C6GoXL-R.js +162 -0
  46. package/dist/assets/index-Drmp3h6P.css +1 -0
  47. package/dist/index.html +13 -0
  48. package/dist/index.js +12209 -2045
  49. package/documentation/SCHEMAS.md +182 -0
  50. package/package.json +14 -11
  51. package/schema.sql +204 -0
  52. package/templates/astro-starter/astro.config.mjs +40 -0
  53. package/templates/astro-starter/middleware/api-purge.ts +130 -0
  54. package/templates/astro-starter/middleware/index.ts +10 -0
  55. package/templates/astro-starter/middleware/kv-cache.ts +76 -0
  56. package/templates/astro-starter/middleware/response-headers.ts +43 -0
  57. package/templates/astro-starter/package.json.template +20 -0
  58. package/templates/astro-starter/public/css/standard-theme.css +157 -0
  59. package/templates/astro-starter/public/robots.txt +23 -0
  60. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +133 -0
  61. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
  62. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
  63. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  64. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +24 -0
  65. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
  66. package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
  67. package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
  68. package/templates/astro-starter/src/components/global/Footer.astro +48 -0
  69. package/templates/astro-starter/src/components/global/Header.astro +92 -0
  70. package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
  71. package/templates/astro-starter/src/components/seo/SeoHead.astro +150 -0
  72. package/templates/astro-starter/src/components/ui/KamaImage.astro +149 -0
  73. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +45 -0
  74. package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
  75. package/templates/astro-starter/src/lib/cms.ts +149 -0
  76. package/templates/astro-starter/src/lib/media/kama-image-service.ts +41 -0
  77. package/templates/astro-starter/src/lib/media/media-proxy.ts +127 -0
  78. package/templates/astro-starter/src/lib/media/media-url.ts +34 -0
  79. package/templates/astro-starter/src/pages/404.astro +46 -0
  80. package/templates/astro-starter/src/pages/[...slug].astro +201 -0
  81. package/templates/astro-starter/src/pages/llms-full.txt.ts +161 -0
  82. package/templates/astro-starter/src/pages/llms.txt.ts +107 -0
  83. package/templates/astro-starter/src/pages/media/[...path].ts +7 -0
  84. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  85. package/templates/astro-starter/tsconfig.json +0 -0
  86. 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,7 @@
1
+ // src/pages/media/[...path].ts
2
+ import type { APIRoute } from "astro";
3
+ import { handleMediaProxy } from "../../lib/media/media-proxy";
4
+
5
+ export const GET: APIRoute = async (context) => {
6
+ return handleMediaProxy(context);
7
+ };
@@ -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, "&amp;")
109
+ .replace(/</g, "&lt;")
110
+ .replace(/>/g, "&gt;")
111
+ .replace(/"/g, "&quot;")
112
+ .replace(/'/g, "&#039;");
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