@brightsideoy/kama 0.1.0-alpha.98 → 1.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/bin/cli.js +726 -261
- package/content/definitions/blocks/button.yaml +34 -0
- package/content/definitions/blocks/header.yaml +13 -0
- package/content/definitions/blocks/header_image.yaml +21 -0
- package/content/definitions/blocks/hero.yaml +28 -0
- package/content/definitions/blocks/html.yaml +13 -0
- package/content/definitions/blocks/image_card.yaml +56 -0
- package/content/definitions/blocks/node-list.yaml +61 -0
- package/content/definitions/blocks/single_image.yaml +27 -0
- package/content/definitions/blocks/text.yaml +15 -0
- package/content/definitions/channels/ecommerce.yaml +28 -0
- package/content/definitions/channels/website.yaml +41 -0
- package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
- package/content/definitions/field-sets/channel-base.yaml +57 -0
- package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
- package/content/definitions/field-sets/content-block.yaml +41 -0
- package/content/definitions/field-sets/page-base.yaml +44 -0
- package/content/definitions/field-sets/page-seo.yaml +34 -0
- package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
- package/content/definitions/field-sets/schema-type.yaml +43 -0
- package/content/definitions/field-sets/tree-base.yaml +31 -0
- package/content/definitions/node-layouts/content.yml +12 -0
- package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
- package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
- package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
- package/content/definitions/nodes/address.yaml +40 -0
- package/content/definitions/nodes/article.yaml +65 -0
- package/content/definitions/nodes/event.yaml +55 -0
- package/content/definitions/nodes/external-link.yaml +29 -0
- package/content/definitions/nodes/page-skeleton.yaml +48 -0
- package/{defaults → content}/definitions/nodes/page.yaml +9 -20
- package/content/definitions/nodes/person.yaml +37 -0
- package/content/definitions/nodes/product.yaml +89 -0
- package/content/definitions/taxonomies/categories.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +12 -0
- package/content/definitions/trees/navigation.yaml +24 -0
- package/definitions/editor/default.ts +1 -0
- package/dist/assets/index-Bzbme3Ob.css +1 -0
- package/dist/assets/index-DX98CiK-.js +164 -0
- package/dist/dist-CErcWOwp.js +27136 -0
- package/dist/index.html +4 -2
- package/dist/index.js +26159 -8575
- package/dist/schema-compiler.js +48 -0
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +23 -25
- package/schema.sql +67 -23
- package/templates/astro-starter/astro.config.mjs +27 -0
- package/templates/astro-starter/package.json.template +20 -0
- package/templates/astro-starter/public/robots.txt +23 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
- package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
- package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
- package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
- package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
- package/templates/astro-starter/src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/src/components/global/Header.astro +171 -0
- package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
- package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
- package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
- package/templates/astro-starter/src/lib/cms.ts +219 -0
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
- package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
- package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
- package/templates/astro-starter/src/middleware.ts +508 -0
- package/templates/astro-starter/src/pages/404.astro +46 -0
- package/templates/astro-starter/src/pages/[...slug].astro +318 -0
- package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
- package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
- package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
- package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
- package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
- package/templates/astro-starter/src/styles/standard-theme.css +157 -0
- package/templates/astro-starter/src/utils/tiptap.ts +113 -0
- package/templates/astro-starter/tsconfig.json +0 -0
- package/templates/kama-worker/index.js +19 -0
- package/templates/kama-worker/server.js +104 -0
- package/wrangler.json +22 -0
- package/defaults/definitions/blocks/button.yaml +0 -15
- package/defaults/definitions/blocks/header.yaml +0 -15
- package/defaults/definitions/blocks/header_image.yaml +0 -22
- package/defaults/definitions/blocks/hero.yaml +0 -27
- package/defaults/definitions/blocks/image_card.yaml +0 -57
- package/defaults/definitions/blocks/single_image.yaml +0 -28
- package/defaults/definitions/blocks/text.yaml +0 -16
- package/defaults/definitions/channels/ecommerce.yaml +0 -24
- package/defaults/definitions/channels/website.yaml +0 -32
- package/defaults/definitions/field-sets/channel-base.yaml +0 -31
- package/defaults/definitions/field-sets/content-block.yaml +0 -46
- package/defaults/definitions/field-sets/page-base.yaml +0 -49
- package/defaults/definitions/field-sets/tree-base.yaml +0 -31
- package/defaults/definitions/node-layouts/three-column.yml +0 -41
- package/defaults/definitions/nodes/address.yaml +0 -32
- package/defaults/definitions/nodes/article.yaml +0 -43
- package/defaults/definitions/nodes/event.yaml +0 -49
- package/defaults/definitions/nodes/external-link.yaml +0 -26
- package/defaults/definitions/nodes/person.yaml +0 -37
- package/defaults/definitions/nodes/product.yaml +0 -86
- package/defaults/definitions/trees/navigation.yaml +0 -29
- package/defaults/definitions/trees/social-links.yaml +0 -9
- package/definitions/_page.yaml +0 -52
- package/dist/assets/index-TdW5F0pX.css +0 -1
- package/dist/assets/index-u9OkBauM.js +0 -1392
- package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
- package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
- package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
- package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
- package/scripts/bundle-definitions.js +0 -176
- /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
- /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
- /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
- /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/pages/partials/footer.astro
|
|
3
|
+
export const partial = true;
|
|
4
|
+
|
|
5
|
+
import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
|
|
6
|
+
import { getChannelId } from "@/lib/cms";
|
|
7
|
+
|
|
8
|
+
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
9
|
+
const cmsWorker = safeEnv.CMS_WORKER;
|
|
10
|
+
const channelId = getChannelId() || "website";
|
|
11
|
+
|
|
12
|
+
const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
13
|
+
const reqInit = { ...init };
|
|
14
|
+
delete reqInit.cache;
|
|
15
|
+
|
|
16
|
+
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
17
|
+
if (urlStr.startsWith("/")) {
|
|
18
|
+
const baseUrl =
|
|
19
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
20
|
+
safeEnv.PUBLIC_API_URL ||
|
|
21
|
+
"http://127.0.0.1:8787";
|
|
22
|
+
urlStr = `${baseUrl}${urlStr}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const req = new Request(urlStr, reqInit);
|
|
26
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function")
|
|
27
|
+
return cmsWorker.fetch(req);
|
|
28
|
+
return fetch(req);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 🎯 Resolve the Home Page slug
|
|
32
|
+
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
|
|
33
|
+
|
|
34
|
+
let footerBlocks: any[] = [];
|
|
35
|
+
let contentMap: Record<string, any[]> = {};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// 🎯 Fetch the Home Page node from Kama CMS Public API
|
|
39
|
+
const res = await customFetch(
|
|
40
|
+
`/api/v1/node/${homeSlug}?channelId=${channelId}`,
|
|
41
|
+
);
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
|
|
45
|
+
// Kama CMS public node endpoints usually organize blocks inside a 'content' object mapped by region
|
|
46
|
+
const allContent = data.content || {};
|
|
47
|
+
|
|
48
|
+
// Extract specifically the footer region from the Home page
|
|
49
|
+
footerBlocks = allContent.footer || [];
|
|
50
|
+
contentMap = allContent;
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("[Partials/Footer] Failed to fetch home page blocks:", e);
|
|
54
|
+
}
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
footerBlocks && footerBlocks.length > 0 ? (
|
|
59
|
+
<BlockRenderer
|
|
60
|
+
blocks={footerBlocks}
|
|
61
|
+
contentMap={contentMap}
|
|
62
|
+
region="footer"
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<div class="footer-default">
|
|
66
|
+
<p>© {new Date().getFullYear()} all rights reserved.</p>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/pages/partials/nav.astro
|
|
3
|
+
export const partial = true; // 🎯 Tells Astro not to wrap this in <html><body>
|
|
4
|
+
|
|
5
|
+
import { getTree, getTreeId, getChannelId } from "@/lib/cms";
|
|
6
|
+
import type { TreeNode } from "@/lib/cms";
|
|
7
|
+
|
|
8
|
+
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
9
|
+
const cmsWorker = safeEnv.CMS_WORKER;
|
|
10
|
+
|
|
11
|
+
const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
12
|
+
const reqInit = { ...init };
|
|
13
|
+
delete reqInit.cache;
|
|
14
|
+
|
|
15
|
+
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
16
|
+
if (urlStr.startsWith("/")) {
|
|
17
|
+
const baseUrl =
|
|
18
|
+
import.meta.env.PUBLIC_API_URL ||
|
|
19
|
+
safeEnv.PUBLIC_API_URL ||
|
|
20
|
+
"http://127.0.0.1:8787";
|
|
21
|
+
urlStr = `${baseUrl}${urlStr}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const req = new Request(urlStr, reqInit);
|
|
25
|
+
if (cmsWorker && typeof cmsWorker.fetch === "function")
|
|
26
|
+
return cmsWorker.fetch(req);
|
|
27
|
+
return fetch(req);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Fetch fresh tree from CMS
|
|
31
|
+
const channelId = getChannelId() || "website";
|
|
32
|
+
const treeId = getTreeId() || "main-nav";
|
|
33
|
+
let tree: TreeNode[] = [];
|
|
34
|
+
try {
|
|
35
|
+
tree = (await getTree(treeId, channelId, customFetch)) || [];
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error("[Partials/Nav] Failed to fetch tree:", e);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get the current path from the client's fetch request
|
|
41
|
+
const currentPath =
|
|
42
|
+
(Astro.url.searchParams.get("path") || "/").replace(/\/$/, "") || "/";
|
|
43
|
+
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
|
|
44
|
+
|
|
45
|
+
const homeNode = tree.find(
|
|
46
|
+
item =>
|
|
47
|
+
item.slug === homeSlug ||
|
|
48
|
+
item.slug === "" ||
|
|
49
|
+
item.path === "/" ||
|
|
50
|
+
item.path === "",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const topLevelNavItems =
|
|
54
|
+
homeNode?.children && homeNode.children.length > 0
|
|
55
|
+
? homeNode.children
|
|
56
|
+
: tree.filter(
|
|
57
|
+
item =>
|
|
58
|
+
item.slug !== homeSlug &&
|
|
59
|
+
item.slug !== "" &&
|
|
60
|
+
item.path !== "/" &&
|
|
61
|
+
item.path !== "",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const isPathActive = (itemPath?: string) => {
|
|
65
|
+
const normItem = (itemPath || "").replace(/\/$/, "") || "/";
|
|
66
|
+
if (normItem === "/") return currentPath === "/";
|
|
67
|
+
return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isExactActive = (itemPath?: string) => {
|
|
71
|
+
return currentPath === ((itemPath || "").replace(/\/$/, "") || "/");
|
|
72
|
+
};
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
<ul class="nav-list">
|
|
76
|
+
{
|
|
77
|
+
topLevelNavItems.map(item => {
|
|
78
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
79
|
+
const isParentActive = isPathActive(item.path);
|
|
80
|
+
|
|
81
|
+
if (hasChildren) {
|
|
82
|
+
return (
|
|
83
|
+
<li class="nav-item">
|
|
84
|
+
<details class="nav-details">
|
|
85
|
+
<summary
|
|
86
|
+
class:list={[
|
|
87
|
+
"nav-button",
|
|
88
|
+
"dropdown-summary",
|
|
89
|
+
{ active: isParentActive },
|
|
90
|
+
]}
|
|
91
|
+
>
|
|
92
|
+
<span>{item.title}</span>
|
|
93
|
+
<span class="indicator">▼</span>
|
|
94
|
+
</summary>
|
|
95
|
+
<div class="submenu-container">
|
|
96
|
+
<ul class="dropdown-panel">
|
|
97
|
+
<li>
|
|
98
|
+
<a
|
|
99
|
+
href={item.path}
|
|
100
|
+
class:list={[
|
|
101
|
+
"dropdown-item",
|
|
102
|
+
"overview-item",
|
|
103
|
+
{ active: isExactActive(item.path) },
|
|
104
|
+
]}
|
|
105
|
+
>
|
|
106
|
+
{item.title} Overview
|
|
107
|
+
</a>
|
|
108
|
+
</li>
|
|
109
|
+
{item.children?.map(child => (
|
|
110
|
+
<li>
|
|
111
|
+
<a
|
|
112
|
+
href={child.path}
|
|
113
|
+
class:list={[
|
|
114
|
+
"dropdown-item",
|
|
115
|
+
{ active: isExactActive(child.path) },
|
|
116
|
+
]}
|
|
117
|
+
>
|
|
118
|
+
{child.title}
|
|
119
|
+
</a>
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
</div>
|
|
124
|
+
</details>
|
|
125
|
+
</li>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<li class="nav-item">
|
|
131
|
+
<a
|
|
132
|
+
href={item.path}
|
|
133
|
+
class:list={["nav-button", { active: isExactActive(item.path) }]}
|
|
134
|
+
>
|
|
135
|
+
{item.title}
|
|
136
|
+
</a>
|
|
137
|
+
</li>
|
|
138
|
+
);
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
</ul>
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import kamaWorker from '@brightsideoy/kama';
|
|
2
|
+
import * as localBundle from './bundled-definitions.js';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
async fetch(request, env, ctx) {
|
|
6
|
+
const schemas = localBundle.BUNDLED_DEFINITIONS || localBundle.default || localBundle;
|
|
7
|
+
const customEnv = { ...env, BUNDLED_DEFINITIONS: schemas, KAMA_RUNTIME: 'cloudflare' };
|
|
8
|
+
|
|
9
|
+
if (kamaWorker && typeof kamaWorker.fetch === 'function') {
|
|
10
|
+
return kamaWorker.fetch(request, customEnv, ctx);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (kamaWorker && kamaWorker.app && typeof kamaWorker.app.fetch === 'function') {
|
|
14
|
+
return kamaWorker.app.fetch(request, customEnv, ctx);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Response('KAMA • ENGINE Ready', { status: 200 });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { serveStatic } from '@hono/node-server/serve-static';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import kamaApp from '@brightsideoy/kama';
|
|
5
|
+
import * as localBundle from './bundled-definitions.js';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
const possiblePaths = [
|
|
14
|
+
'./node_modules/@brightsideoy/kama/dist',
|
|
15
|
+
'../node_modules/@brightsideoy/kama/dist',
|
|
16
|
+
'../../node_modules/@brightsideoy/kama/dist'
|
|
17
|
+
];
|
|
18
|
+
const distPath = possiblePaths.find(p => existsSync(p));
|
|
19
|
+
|
|
20
|
+
if (!distPath) {
|
|
21
|
+
console.warn('\x1b[33m ⚠\x1b[0m \x1b[2mCould not locate @brightsideoy/kama/dist folder. Admin UI will not load.\x1b[0m');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const app = new Hono();
|
|
25
|
+
|
|
26
|
+
let cachedSchemas = localBundle.BUNDLED_DEFINITIONS || localBundle.default || localBundle;
|
|
27
|
+
let lastModified = 0;
|
|
28
|
+
|
|
29
|
+
async function getCustomEnv(c) {
|
|
30
|
+
try {
|
|
31
|
+
const bundlePath = join(process.cwd(), 'bundled-definitions.js');
|
|
32
|
+
if (existsSync(bundlePath)) {
|
|
33
|
+
const mtime = statSync(bundlePath).mtimeMs;
|
|
34
|
+
if (mtime > lastModified) {
|
|
35
|
+
if (lastModified !== 0) {
|
|
36
|
+
console.log('\x1b[36m ⟳\x1b[0m \x1b[2mHot-reloading schema definitions\x1b[0m');
|
|
37
|
+
}
|
|
38
|
+
const mod = await import(`${pathToFileURL(bundlePath).href}?t=${mtime}`);
|
|
39
|
+
cachedSchemas = mod.BUNDLED_DEFINITIONS || mod.default || mod;
|
|
40
|
+
lastModified = mtime;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (err) { }
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...process.env,
|
|
47
|
+
...(c.env || {}),
|
|
48
|
+
BUNDLED_DEFINITIONS: cachedSchemas,
|
|
49
|
+
KAMA_RUNTIME: (c.env && c.env.DB) ? 'cloudflare' : 'node'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
app.use('/media/*', serveStatic({ root: './' }));
|
|
54
|
+
if (distPath) {
|
|
55
|
+
app.use('/assets/*', serveStatic({ root: distPath }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveExecutionContext(c) {
|
|
59
|
+
try {
|
|
60
|
+
if (c.executionCtx) return c.executionCtx;
|
|
61
|
+
} catch (e) { }
|
|
62
|
+
return {
|
|
63
|
+
waitUntil: (promise) => Promise.resolve(promise).catch(console.error),
|
|
64
|
+
passThroughOnException: () => { }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
app.all('*', async (c) => {
|
|
69
|
+
const customEnv = await getCustomEnv(c);
|
|
70
|
+
const execCtx = resolveExecutionContext(c);
|
|
71
|
+
|
|
72
|
+
let response;
|
|
73
|
+
if (typeof kamaApp.fetch === 'function') {
|
|
74
|
+
response = await kamaApp.fetch(c.req.raw, customEnv, execCtx);
|
|
75
|
+
} else if (kamaApp.app && typeof kamaApp.app.fetch === 'function') {
|
|
76
|
+
response = await kamaApp.app.fetch(c.req.raw, customEnv, execCtx);
|
|
77
|
+
} else {
|
|
78
|
+
return c.text("Engine not configured correctly", 500);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (response.status === 404 && distPath && !c.req.path.startsWith('/api')) {
|
|
82
|
+
const indexPath = join(distPath, 'index.html');
|
|
83
|
+
if (existsSync(indexPath)) {
|
|
84
|
+
return c.html(readFileSync(indexPath, 'utf-8'));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export default app;
|
|
92
|
+
|
|
93
|
+
if (typeof process !== 'undefined' && process.argv && process.argv[1] && process.argv[1].endsWith('server.js')) {
|
|
94
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 8787;
|
|
95
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
96
|
+
console.log(`\n \x1b[36m\x1b[1mKAMA • ENGINE\x1b[0m`);
|
|
97
|
+
console.log(` \x1b[2m│\x1b[0m`);
|
|
98
|
+
console.log(` \x1b[2m│\x1b[0m Local \x1b[32mhttp://localhost:${info.port}/\x1b[0m`);
|
|
99
|
+
if (distPath) {
|
|
100
|
+
console.log(` \x1b[2m│\x1b[0m Admin UI \x1b[2mServing from local dist\x1b[0m`);
|
|
101
|
+
}
|
|
102
|
+
console.log(` \x1b[2m│\x1b[0m\n`);
|
|
103
|
+
});
|
|
104
|
+
}
|
package/wrangler.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "kama",
|
|
4
|
+
"main": "server/index.ts",
|
|
5
|
+
"compatibility_date": "2024-09-23",
|
|
6
|
+
"compatibility_flags": [
|
|
7
|
+
"nodejs_compat"
|
|
8
|
+
],
|
|
9
|
+
"d1_databases": [
|
|
10
|
+
{
|
|
11
|
+
"binding": "DB",
|
|
12
|
+
"database_name": "kama-db",
|
|
13
|
+
"database_id": "local-kama-db"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"r2_buckets": [
|
|
17
|
+
{
|
|
18
|
+
"binding": "MEDIA_BUCKET",
|
|
19
|
+
"bucket_name": "kama-media"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
id: header_image
|
|
2
|
-
label: Header Image
|
|
3
|
-
group: Media
|
|
4
|
-
description: Image for page header.
|
|
5
|
-
|
|
6
|
-
fields:
|
|
7
|
-
- image:
|
|
8
|
-
label: Image
|
|
9
|
-
type: asset
|
|
10
|
-
required: true
|
|
11
|
-
multiple: false
|
|
12
|
-
maxSizeMB: 15
|
|
13
|
-
allowedTypes: ["image/*"]
|
|
14
|
-
crops:
|
|
15
|
-
standard:
|
|
16
|
-
label: "Page header (21:9)"
|
|
17
|
-
aspectRatio: 2.333
|
|
18
|
-
public: true
|
|
19
|
-
|
|
20
|
-
- fieldSet: access
|
|
21
|
-
- hidden:
|
|
22
|
-
default: false
|