@brightsideoy/kama 0.1.0-alpha.120 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightsideoy/kama",
3
- "version": "0.1.0-alpha.120",
3
+ "version": "0.1.0-alpha.121",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,157 @@
1
+ /* =======================================
2
+ 1. GLOBAL RESETS & TYPOGRAPHY
3
+ ======================================= */
4
+ *,
5
+ *::before,
6
+ *::after {
7
+ box-sizing: border-box;
8
+ border-radius: 0 !important;
9
+ }
10
+
11
+ html {
12
+ font-size: 16px;
13
+ -webkit-text-size-adjust: 100%;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
16
+ text-rendering: optimizeLegibility;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ padding: 0;
22
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
23
+ font-display: swap;
24
+ background-color: #ffffff;
25
+ color: #1e293b;
26
+ min-height: 100vh;
27
+ display: flex;
28
+ flex-direction: column;
29
+ line-height: 1.65;
30
+ letter-spacing: -0.011em;
31
+ }
32
+
33
+ h1,
34
+ h2,
35
+ h3,
36
+ h4,
37
+ h5,
38
+ h6 {
39
+ color: #0f172a;
40
+ font-weight: 700;
41
+ line-height: 1.25;
42
+ letter-spacing: -0.022em;
43
+ margin-top: 1.6em;
44
+ margin-bottom: 0.6em;
45
+ }
46
+
47
+ h1 {
48
+ font-size: 2.25rem;
49
+ }
50
+
51
+ h2 {
52
+ font-size: 1.65rem;
53
+ }
54
+
55
+ h3 {
56
+ font-size: 1.35rem;
57
+ }
58
+
59
+ p {
60
+ margin-top: 0;
61
+ margin-bottom: 1.25em;
62
+ color: #334155;
63
+ font-size: 1.03rem;
64
+ }
65
+
66
+ a {
67
+ color: #0284c7;
68
+ text-decoration-thickness: 1px;
69
+ text-underline-offset: 3px;
70
+ transition: color 0.15s ease;
71
+ }
72
+
73
+ a:hover {
74
+ color: #0369a1;
75
+ }
76
+
77
+ li>p:last-child {
78
+ margin-bottom: 0;
79
+ }
80
+
81
+ /* =======================================
82
+ 2. MAIN LAYOUT SHELL
83
+ ======================================= */
84
+ .content-block {
85
+ flex: 1;
86
+ padding: 40px 0 64px 0;
87
+ }
88
+
89
+ .content-container {
90
+ max-width: 1400px;
91
+ margin: 0 auto;
92
+ padding: 0 24px;
93
+ width: 100%;
94
+ box-sizing: border-box;
95
+ }
96
+
97
+ /* =======================================
98
+ 3. ROUTER REGIONS (from slug.astro)
99
+ ======================================= */
100
+ .region-header {
101
+ margin-bottom: 24px;
102
+ }
103
+
104
+ .breadcrumb-wrapper {
105
+ margin-bottom: 24px;
106
+ }
107
+
108
+ .content-layout {
109
+ width: 100%;
110
+ display: block;
111
+ }
112
+
113
+ .main-column {
114
+ width: 100%;
115
+ min-width: 0;
116
+ }
117
+
118
+ @media (min-width: 768px) {
119
+ .content-layout.has-sidebar {
120
+ display: grid !important;
121
+ grid-template-columns: calc(100% - 360px) 310px !important;
122
+ column-gap: 48px !important;
123
+ align-items: start !important;
124
+ }
125
+
126
+ .main-column {
127
+ grid-column: 1 / 2 !important;
128
+ }
129
+
130
+ .sidebar-column {
131
+ grid-column: 2 / 3 !important;
132
+ position: sticky;
133
+ top: 32px;
134
+ }
135
+ }
136
+
137
+ .sidebar-column {
138
+ background: #f8fafc;
139
+ border: 1px solid #e2e8f0;
140
+ padding: 28px;
141
+ box-sizing: border-box;
142
+ }
143
+
144
+ @media (max-width: 767px) {
145
+ .sidebar-column {
146
+ margin-top: 40px;
147
+ }
148
+ }
149
+
150
+ .empty-content {
151
+ padding: 32px;
152
+ border: 1px dashed #cbd5e1;
153
+ background: #f8fafc;
154
+ color: #64748b;
155
+ font-size: 0.95rem;
156
+ text-align: center;
157
+ }
@@ -1 +1,23 @@
1
- <link rel="author" type="text/plain" href="/llms.txt" title="LLM Directory" />
1
+ # Allow standard search engine crawlers
2
+ User-agent: *
3
+ Allow: /
4
+
5
+ # Block sensitive or irrelevant paths
6
+ Disallow: /api/
7
+ Disallow: /*?*search=
8
+
9
+ # Explicitly block AI scrapers / Training Bots (Optional)
10
+ # User-agent: GPTBot
11
+ # Disallow: /
12
+
13
+ # User-agent: CCBot
14
+ # Disallow: /
15
+
16
+ # User-agent: Anthropic-ai
17
+ # Disallow: /
18
+
19
+ # User-agent: Google-Extended
20
+ # Disallow: /
21
+
22
+ # XML Sitemap location
23
+ # Sitemap: https://yourdomain.com/sitemap-index.xml
@@ -4,6 +4,7 @@ import ImageBlock from "@/components/blocks/ImageBlock.astro";
4
4
  import HeaderImage from "@/components/blocks/HeaderImage.astro";
5
5
  import ColumnLayout from "@/components/blocks/ColumnLayout.astro";
6
6
  import HtmlBlock from "@/components/blocks/HtmlBlock.astro";
7
+ import { extractHtml } from "@/utils/tiptap";
7
8
 
8
9
  interface Block {
9
10
  instanceId: number;
@@ -63,8 +64,8 @@ const visibleBlocks = safeBlocks.filter(b => {
63
64
  case "headline":
64
65
  case "header-block":
65
66
  case "header_block": {
66
- const headline = data.headline || data.title || "";
67
- const subhead = data.subhead || data.body || "";
67
+ const headline = extractHtml(data.headline || data.title);
68
+ const subhead = extractHtml(data.subhead || data.body);
68
69
  return (
69
70
  <header class="cms-block cms-block-header">
70
71
  {headline && <Fragment set:html={headline} />}
@@ -76,7 +77,7 @@ const visibleBlocks = safeBlocks.filter(b => {
76
77
  case "text":
77
78
  case "text-block":
78
79
  case "text_block": {
79
- const bodyText = data.body || data.text || data.content || "";
80
+ const bodyText = extractHtml(data.body || data.text || data.content);
80
81
  return (
81
82
  <div class="cms-block cms-block-text">
82
83
  <Fragment set:html={bodyText} />
@@ -84,7 +85,6 @@ const visibleBlocks = safeBlocks.filter(b => {
84
85
  );
85
86
  }
86
87
 
87
- // 🎯 Handles all HTML block component identifiers
88
88
  case "html":
89
89
  case "html-block":
90
90
  case "html_block":
@@ -99,7 +99,6 @@ const visibleBlocks = safeBlocks.filter(b => {
99
99
  );
100
100
  }
101
101
 
102
- // 🎯 Handles all image component identifiers from the payload
103
102
  case "image":
104
103
  case "image-block":
105
104
  case "image_block":
@@ -0,0 +1,52 @@
1
+ ---
2
+ import Header from "@/components/global/Header.astro";
3
+ import Footer from "@/components/global/Footer.astro";
4
+ import SeoHead from "@/components/seo/SeoHead.astro";
5
+ import type { TreeNode, ContentBlock } from "@/lib/cms";
6
+
7
+ interface Props {
8
+ node: Record<string, any>;
9
+ content: Record<string, ContentBlock[]>;
10
+ normalizedSlug: string;
11
+ isHomePage: boolean;
12
+ hasSidebar?: boolean;
13
+ headerBlocks?: ContentBlock[];
14
+ tree?: TreeNode[];
15
+ }
16
+
17
+ const {
18
+ node = {},
19
+ content = {},
20
+ normalizedSlug,
21
+ isHomePage,
22
+ headerBlocks = [],
23
+ tree,
24
+ } = Astro.props;
25
+
26
+ const footerBlocks = content?.footer || [];
27
+ ---
28
+
29
+ <html lang="en">
30
+ <head>
31
+ <SeoHead
32
+ node={node}
33
+ content={content}
34
+ normalizedSlug={normalizedSlug}
35
+ isHomePage={isHomePage}
36
+ headerBlocks={headerBlocks}
37
+ />
38
+ <!-- 🎯 Standard CSS is decoupled from the bundler -->
39
+ <link rel="stylesheet" href="/css/standard-theme.css" />
40
+ </head>
41
+ <body>
42
+ <Header node={node} tree={tree} />
43
+
44
+ <main class="content-block">
45
+ <div class="content-container">
46
+ <slot />
47
+ </div>
48
+ </main>
49
+
50
+ <Footer blocks={footerBlocks} contentMap={content} />
51
+ </body>
52
+ </html>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  // src/pages/404.astro
3
- import Layout from "../layouts/Layout.astro";
3
+ import StandardLayout from "../layouts/StandardLayout.astro";
4
4
 
5
5
  const node = {
6
6
  title: "Page Not Found",
@@ -8,7 +8,7 @@ const node = {
8
8
  };
9
9
  ---
10
10
 
11
- <Layout
11
+ <StandardLayout
12
12
  node={node}
13
13
  isHomePage={false}
14
14
  hasSidebar={false}
@@ -22,7 +22,7 @@ const node = {
22
22
  <p>Sorry, the page you are looking for doesn't exist or has been moved.</p>
23
23
  <a href="/" class="button">Go back home</a>
24
24
  </div>
25
- </Layout>
25
+ </StandardLayout>
26
26
 
27
27
  <style>
28
28
  .error-page {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  // src/pages/[...slug].astro
3
- import Layout from "../layouts/Layout.astro";
3
+ import StandardLayout from "../layouts/StandardLayout.astro";
4
4
  import SkeletonLayout from "../layouts/SkeletonLayout.astro";
5
5
  import BlockRenderer from "../components/blocks/BlockRenderer.astro";
6
6
  import Breadcrumb from "../components/global/Breadcrumb.astro";
@@ -142,7 +142,7 @@ const isSkeleton = node?.nodeType === "page-skeleton";
142
142
  <BlockRenderer blocks={mainBlocks} contentMap={content} region="main" />
143
143
  </SkeletonLayout>
144
144
  ) : (
145
- <Layout
145
+ <StandardLayout
146
146
  node={node}
147
147
  content={content}
148
148
  normalizedSlug={normalizedSlug}
@@ -196,58 +196,6 @@ const isSkeleton = node?.nodeType === "page-skeleton";
196
196
  </aside>
197
197
  )}
198
198
  </div>
199
- </Layout>
199
+ </StandardLayout>
200
200
  )
201
201
  }
202
-
203
- <style>
204
- .region-header {
205
- margin-bottom: 24px;
206
- }
207
- .breadcrumb-wrapper {
208
- margin-bottom: 24px;
209
- }
210
- .content-layout {
211
- width: 100%;
212
- display: block;
213
- }
214
- .main-column {
215
- width: 100%;
216
- min-width: 0;
217
- }
218
- @media (min-width: 768px) {
219
- .content-layout.has-sidebar {
220
- display: grid !important;
221
- grid-template-columns: calc(100% - 360px) 310px !important;
222
- column-gap: 48px !important;
223
- align-items: start !important;
224
- }
225
- .main-column {
226
- grid-column: 1 / 2 !important;
227
- }
228
- .sidebar-column {
229
- grid-column: 2 / 3 !important;
230
- position: sticky;
231
- top: 32px;
232
- }
233
- }
234
- .sidebar-column {
235
- background: #f8fafc;
236
- border: 1px solid #e2e8f0;
237
- padding: 28px;
238
- box-sizing: border-box;
239
- }
240
- @media (max-width: 767px) {
241
- .sidebar-column {
242
- margin-top: 40px;
243
- }
244
- }
245
- .empty-content {
246
- padding: 32px;
247
- border: 1px dashed #cbd5e1;
248
- background: #f8fafc;
249
- color: #64748b;
250
- font-size: 0.95rem;
251
- text-align: center;
252
- }
253
- </style>
@@ -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
+ };
@@ -1,16 +1,18 @@
1
1
  // src/pages/llms.txt.ts
2
2
  import type { APIRoute } from "astro";
3
3
  import { getTree, getTreeId, getChannelId, getNodeByPath } from "../lib/cms";
4
- import { env } from "cloudflare:workers";
4
+
5
+ // Ensure this route is dynamically rendered (SSR)
6
+ export const prerender = false;
5
7
 
6
8
  export const GET: APIRoute = async (context) => {
7
- // 1. Setup Worker Fetch Binding (Same as your [...slug].astro)
9
+ // 1. Setup Cloudflare Worker Fetch Binding safely
8
10
  const locals = (context.locals as any) || {};
9
- const workerEnv = env || process.env || {};
10
- const cmsWorker = locals.cfContext?.env?.CMS_WORKER || workerEnv.CMS_WORKER;
11
+ const cfEnv = locals.cfContext?.env || {};
12
+ const cmsWorker = cfEnv.CMS_WORKER;
11
13
 
12
14
  const envApiUrl = import.meta.env.PUBLIC_API_URL;
13
- const workerApiUrl = workerEnv.PUBLIC_API_URL;
15
+ const workerApiUrl = cfEnv.PUBLIC_API_URL || (typeof process !== "undefined" ? process.env?.PUBLIC_API_URL : undefined);
14
16
  const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787").replace(/\/$/, "");
15
17
 
16
18
  const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
@@ -24,71 +26,79 @@ export const GET: APIRoute = async (context) => {
24
26
  return fetch(req);
25
27
  };
26
28
 
27
- const channelId = getChannelId();
28
- const treeId = getTreeId();
29
- const siteOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
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(/\/$/, "");
30
35
  const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
31
36
 
32
37
  try {
33
- // 2. Fetch the Home Page (for global context) and the Navigation Tree (for the directory)
34
- const [homeData, navTree] = await Promise.all([
35
- getNodeByPath("home", channelId, false, customFetch),
36
- getTree(treeId, channelId, customFetch),
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(() => []),
37
42
  ]);
38
43
 
39
- const siteDescription = homeData?.node?.summary || "Corporate Web Application.";
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 || []);
40
48
 
41
- // 3. Build the Markdown Output for the AI
42
- let markdown = `# ${brandName}\n\n`;
43
- markdown += `> ${siteDescription}\n\n`;
44
- markdown += `## About This Site\n`;
45
- markdown += `This file serves as a directory for AI agents and language models. The links below contain the primary content and structure of the application.\n\n`;
49
+ const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Global Content Delivery at the Edge";
46
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`;
47
55
  markdown += `## Site Directory\n\n`;
48
56
 
49
- // Recursively flatten the CMS tree into a Markdown list
57
+ // Recursive tree flattener with link annotations
50
58
  const buildList = (nodes: any[], depth = 0) => {
51
59
  let list = "";
52
60
  const indent = " ".repeat(depth);
53
61
 
54
62
  for (const item of nodes) {
55
- if (!item.node) continue;
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}`;
56
69
 
57
- // Build the absolute URL for the AI to crawl if needed
58
- const itemUrl = item.node.path === "/"
59
- ? siteOrigin
60
- : `${siteOrigin}${item.node.path.startsWith('/') ? '' : '/'}${item.node.path}`;
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()}` : "";
61
73
 
62
- list += `${indent}- [${item.node.title}](${itemUrl})\n`;
74
+ list += `${indent}- [${itemTitle}](${itemUrl})${annotation}\n`;
63
75
 
64
- if (item.children && item.children.length > 0) {
76
+ if (Array.isArray(item.children) && item.children.length > 0) {
65
77
  list += buildList(item.children, depth + 1);
66
78
  }
67
79
  }
68
80
  return list;
69
81
  };
70
82
 
71
- if (navTree && navTree.length > 0) {
83
+ if (navTree.length > 0) {
72
84
  markdown += buildList(navTree);
73
85
  } else {
74
- // 🎯 Use the actual CMS title for the home page, or fallback to the brand name
75
86
  const homeTitle = homeData?.node?.title || brandName;
76
- markdown += `- [${homeTitle}](${siteOrigin})\n`;
87
+ markdown += `- [${homeTitle}](${siteOrigin}): Main landing page.\n`;
77
88
  }
78
89
 
79
90
  markdown += `\n---\n*Generated dynamically by Kama Engine.*`;
80
91
 
81
- // 4. Return as raw text
82
92
  return new Response(markdown, {
83
93
  status: 200,
84
- // Your middleware will attach the caching headers, but we set the content type here
85
94
  headers: {
86
95
  "Content-Type": "text/plain; charset=utf-8",
96
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
87
97
  },
88
98
  });
89
99
 
90
100
  } catch (error) {
91
- console.error("Failed to generate llms.txt:", error);
101
+ console.error("[llms.txt Generator Error]:", error);
92
102
  return new Response("# Error\nCould not generate AI directory.", {
93
103
  status: 500,
94
104
  headers: { "Content-Type": "text/plain; charset=utf-8" }