@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/dist/assets/{index-BlmOgxC3.js → index-C6GoXL-R.js} +2 -2
- package/dist/assets/{index-RJKUlAE_.css → index-Drmp3h6P.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +994 -999
- package/package.json +1 -1
- package/templates/astro-starter/public/css/standard-theme.css +157 -0
- package/templates/astro-starter/public/robots.txt +23 -1
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +4 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
- package/templates/astro-starter/src/pages/404.astro +3 -3
- package/templates/astro-starter/src/pages/[...slug].astro +3 -55
- package/templates/astro-starter/src/pages/llms-full.txt.ts +161 -0
- package/templates/astro-starter/src/pages/llms.txt.ts +42 -32
- package/templates/astro-starter/src/utils/tiptap.ts +113 -0
- package/definitions/_page.yaml +0 -52
- package/templates/astro-starter/src/layouts/Layout.astro +0 -151
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
9
|
+
// 1. Setup Cloudflare Worker Fetch Binding safely
|
|
8
10
|
const locals = (context.locals as any) || {};
|
|
9
|
-
const
|
|
10
|
-
const cmsWorker =
|
|
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 =
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
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
|
-
//
|
|
34
|
-
const [homeData,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
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}- [${
|
|
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
|
|
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})
|
|
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("
|
|
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" }
|