@brightsideoy/kama 1.0.0-alpha.2 → 1.0.0-alpha.21
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/bin/cli.js +604 -364
- package/content/definitions/block-kama-forms.yaml +5 -0
- package/content/definitions/blocks/button.yaml +15 -5
- package/content/definitions/blocks/image-gallery.yaml +34 -0
- package/content/definitions/blocks/node-list.yaml +75 -0
- package/content/definitions/blocks/youtube.yaml +8 -0
- package/content/definitions/field-sets/access.yaml +10 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/field-sets/page-base.yaml +16 -1
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/media-tag.yaml +15 -0
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -0
- package/definitions/editor/default.ts +15 -1
- package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
- package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
- package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
- package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
- package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
- package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
- package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
- package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
- package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
- package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-BQkr9KyU.js +1 -0
- package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
- package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
- package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
- package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
- package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
- package/dist/assets/CmsPublisher-XSCPrp5L.js +3 -0
- package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
- package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
- package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
- package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
- package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
- package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
- package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
- package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-CATe46WF.js +1 -0
- package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
- package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
- package/dist/assets/circle-check-A-QeFqt3.js +1 -0
- package/dist/assets/database-hg5SqKLG.js +1 -0
- package/dist/assets/folder-tree-BWYot72H.js +1 -0
- package/dist/assets/globe-BraoljaN.js +1 -0
- package/dist/assets/history-pqIbKK9u.js +1 -0
- package/dist/assets/index-CY0VYSyt.css +1 -0
- package/dist/assets/index-DdG9Pozx.js +14 -0
- package/dist/assets/layers-B6RgfpPR.js +1 -0
- package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
- package/dist/assets/monitor-CrpvInqG.js +1 -0
- package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
- package/dist/assets/plus-CX_GHzXs.js +1 -0
- package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
- package/dist/assets/save-Ctv8vW9H.js +1 -0
- package/dist/assets/schema-helpers-DvficKko.js +1 -0
- package/dist/assets/send-F-mJeL54.js +1 -0
- package/dist/assets/smartphone-EPce6VdP.js +1 -0
- package/dist/assets/trash-2-CAUF1KtI.js +1 -0
- package/dist/assets/useLocalStorage-C805uplI.js +1 -0
- package/dist/assets/useNodes-CqFlc72m.js +1 -0
- package/dist/assets/user-IFlT6mYq.js +1 -0
- package/dist/build-seed.js +495 -0
- package/dist/{dist-Dy7-HUEG.js → dist-3Xc2TTZw.js} +3189 -1394
- package/dist/index.html +34 -2
- package/dist/index.js +3116 -1315
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +13 -6
- package/schema.sql +44 -1
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
- package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
- package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +68 -49
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
- package/templates/astro-starter/src/lib/cms.ts +33 -11
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
- package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
- package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
- package/templates/astro-starter/src/middleware.ts +158 -100
- package/templates/astro-starter/src/pages/[...slug].astro +77 -3
- package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
- package/templates/kama-worker/index.js +38 -15
- package/templates/kama-worker/server.js +13 -5
- package/templates/kama-worker/setup-local-db.mjs +17 -0
- package/dist/assets/index-F2rfUymo.css +0 -1
- package/dist/assets/index-lXxH3VFK.js +0 -164
package/dist/schema-compiler.js
CHANGED
|
@@ -30,19 +30,19 @@ async function f() {
|
|
|
30
30
|
if (n(f)) l = (await import(t(f).href)).compileDefinitions;
|
|
31
31
|
else if (n(p)) l = (await import(t(p).href)).compileDefinitions;
|
|
32
32
|
else try {
|
|
33
|
-
l = (await import("./dist-
|
|
33
|
+
l = (await import("./dist-3Xc2TTZw.js")).compileDefinitions;
|
|
34
34
|
} catch (e) {
|
|
35
|
-
console.error("
|
|
35
|
+
console.error("\x1B[31m ✖\x1B[0m \x1B[31mCould not resolve Kama Schema Engine:\x1B[0m", e), process.exit(1);
|
|
36
36
|
}
|
|
37
|
-
l || (console.error("
|
|
37
|
+
l || (console.error("\x1B[31m ✖\x1B[0m \x1B[31mcompileDefinitions function missing from resolved module\x1B[0m"), process.exit(1));
|
|
38
38
|
let m = l(), h = Object.keys(m?.nodes || {}).length, g = Object.keys(m?.blocks || {}).length, _ = Object.keys(m?.taxonomies || {}).length, v = `// AUTO-GENERATED BUNDLE - DO NOT EDIT
|
|
39
39
|
const defs = ${JSON.stringify(m, null, 2)};
|
|
40
40
|
export const BUNDLED_DEFINITIONS = defs;
|
|
41
41
|
export default defs;
|
|
42
42
|
`;
|
|
43
|
-
a(c, v, "utf-8"), console.log(
|
|
43
|
+
a(c, v, "utf-8"), console.log(`\x1b[32m ✓\x1b[0m \x1b[2mSchemas bundled (${h} nodes, ${g} blocks, ${_} taxonomies)\x1b[0m`);
|
|
44
44
|
}
|
|
45
45
|
f().catch((e) => {
|
|
46
|
-
console.error("
|
|
46
|
+
console.error("\x1B[31m ✖\x1B[0m \x1B[31mSchema Compiler Runtime Error:\x1B[0m", e), process.exit(1);
|
|
47
47
|
});
|
|
48
48
|
//#endregion
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# KAMA CMS — Extension Development Guide
|
|
2
|
+
|
|
3
|
+
KAMA features a **Drop-In Extension Architecture**. Extensions are built using native TypeScript, Vue, and YAML, and are automatically discovered, registered, and compiled into the Cloudflare Worker and Vite SPA at build time.
|
|
4
|
+
|
|
5
|
+
This guarantees zero core bloat, perfect native performance, and a completely isolated development environment.
|
|
6
|
+
|
|
7
|
+
## 1. How It Works (The Auto-Discovery Pipeline)
|
|
8
|
+
|
|
9
|
+
Extensions live in the `extensions/` directory at the root of your workspace.
|
|
10
|
+
|
|
11
|
+
When you run `npm run dev` or `npm run build:schema`, the KAMA CLI automatically:
|
|
12
|
+
|
|
13
|
+
1. Scans the `extensions/` folder.
|
|
14
|
+
2. Generates an `extension-registry.js` file.
|
|
15
|
+
3. Injects your Database tables into the SQLite/D1 initialization sequence.
|
|
16
|
+
4. Mounts your Hono API routes securely behind KAMA's authentication.
|
|
17
|
+
5. Injects your Vue `.vue` components into the SPA routing and sidebars.
|
|
18
|
+
6. Merges your extension's `.yaml` schemas natively into the core bundle.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Directory Structure
|
|
23
|
+
|
|
24
|
+
A standard extension uses the following structure:
|
|
25
|
+
|
|
26
|
+
kama-workspace/
|
|
27
|
+
├─ extensions/
|
|
28
|
+
│ └─ forms/ <-- Your Extension Folder
|
|
29
|
+
│ ├─ index.ts <-- The Manifest (Exports UI, DB, and Routes)
|
|
30
|
+
│ ├─ api/
|
|
31
|
+
│ │ ├─ admin.ts <-- Hono routes for the Admin UI
|
|
32
|
+
│ │ └─ public.ts <-- Hono routes for the Public API
|
|
33
|
+
│ ├─ ui/
|
|
34
|
+
│ │ ├─ SidebarPane.vue <-- Custom Left Drawer Vue Component
|
|
35
|
+
│ │ └─ Dashboard.vue <-- Custom Main Workspace Vue Component
|
|
36
|
+
│ └─ content/ <-- 🎯 Schema folder matching Core
|
|
37
|
+
│ └─ blocks/
|
|
38
|
+
│ └─ form-block.yaml <-- YAML Schemas (Auto-merged into KAMA)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. The Extension Manifest (`index.ts`)
|
|
43
|
+
|
|
44
|
+
Every extension must export a default object serving as the contract between your code and the KAMA core.
|
|
45
|
+
|
|
46
|
+
// extensions/forms/index.ts
|
|
47
|
+
import { adminFormsRouter } from "./api/admin.js";
|
|
48
|
+
import { publicFormsRouter } from "./api/public.js";
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
id: "kama-forms",
|
|
52
|
+
name: "Form Builder",
|
|
53
|
+
|
|
54
|
+
// 🎯 1. DATABASE HOOKS
|
|
55
|
+
// KAMA will automatically CREATE TABLE IF NOT EXISTS on boot.
|
|
56
|
+
db: {
|
|
57
|
+
tables: {
|
|
58
|
+
kama_forms_table: ` id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
form_id TEXT NOT NULL,
|
|
60
|
+
payload_json TEXT NOT NULL,
|
|
61
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 🎯 2. API ROUTING HOOKS
|
|
67
|
+
// Handled natively by Hono. KAMA injects c.env.DB automatically.
|
|
68
|
+
routes: {
|
|
69
|
+
public: publicFormsRouter, // Mounted at: /api/ext/kama-forms/_
|
|
70
|
+
admin: adminFormsRouter, // Mounted at: /api/admin/ext/kama-forms/_
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// 🎯 3. FRONTEND UI HOOKS
|
|
74
|
+
ui: {
|
|
75
|
+
sidebar: [
|
|
76
|
+
{
|
|
77
|
+
id: "forms-nav",
|
|
78
|
+
label: "Forms",
|
|
79
|
+
icon: "Inbox", // Uses KAMA's global icon resolver
|
|
80
|
+
sidebarViewId: "forms-sidebar", // 🎯 Renders in the left drawer panel
|
|
81
|
+
viewId: "forms-dashboard", // 🎯 Renders in the main workspace canvas
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
// Map your view IDs to your Vue component chunks
|
|
86
|
+
views: [
|
|
87
|
+
{ id: "forms-sidebar", url: "/api/ext/kama-forms/ui-pane.js" },
|
|
88
|
+
{ id: "forms-dashboard", url: "/api/ext/kama-forms/ui.js" }
|
|
89
|
+
],
|
|
90
|
+
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. API Routing (`api/admin.ts`)
|
|
97
|
+
|
|
98
|
+
Extension APIs are built using standard Hono syntax.
|
|
99
|
+
|
|
100
|
+
**Important:** Your extension routes do not need to connect to the database manually. KAMA intercepts the request and injects the active database connection directly into the request context.
|
|
101
|
+
|
|
102
|
+
// extensions/forms/api/admin.ts
|
|
103
|
+
import { Hono } from "hono";
|
|
104
|
+
export const adminFormsRouter = new Hono();
|
|
105
|
+
|
|
106
|
+
adminFormsRouter.get("/submissions", async (c) => {
|
|
107
|
+
// Access the core database connection
|
|
108
|
+
const db = (c as any).env?.DB;
|
|
109
|
+
const formId = c.req.query("formId");
|
|
110
|
+
|
|
111
|
+
const stmt = db.prepare("SELECT \* FROM kama_forms_table WHERE form_id = ? ORDER BY createdAt DESC");
|
|
112
|
+
const results = typeof stmt.all === "function" ? await stmt.bind(formId).all() : [];
|
|
113
|
+
|
|
114
|
+
return c.json({ submissions: results?.results || results || [] });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Frontend UI & Cross-Component Communication
|
|
120
|
+
|
|
121
|
+
KAMA enforces strict decoupling. Extensions shouldn't mutate core routing states directly. Instead, KAMA provides generic Event Buses and State Stores.
|
|
122
|
+
|
|
123
|
+
### Triggering Workspace Navigation
|
|
124
|
+
|
|
125
|
+
If a user clicks an item in your left `SidebarPane.vue` and you want to open your main `Dashboard.vue` in the primary workspace, dispatch a standard custom event. Core KAMA listens to this universally:
|
|
126
|
+
|
|
127
|
+
// Inside extensions/forms/ui/SidebarPane.vue
|
|
128
|
+
const openWorkspaceView = () => {
|
|
129
|
+
window.dispatchEvent(new CustomEvent('cms-open-extension-view', {
|
|
130
|
+
detail: { viewId: 'forms-dashboard' }
|
|
131
|
+
}));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
### Passing State Between Extension Components
|
|
135
|
+
|
|
136
|
+
To pass data (like a selected Form ID) from your Sidebar to your Dashboard, use the global `window.Kama` SDK:
|
|
137
|
+
|
|
138
|
+
**Setting State & Emitting Events (Sidebar):**
|
|
139
|
+
|
|
140
|
+
// Set state synchronously
|
|
141
|
+
(window as any).Kama?.state?.set('kama-forms', 'activeFormId', 'contact-form');
|
|
142
|
+
|
|
143
|
+
// Notify other components live
|
|
144
|
+
(window as any).Kama?.events?.emit('kama-forms:select-form', { formId: 'contact-form' });
|
|
145
|
+
|
|
146
|
+
**Listening for State (Dashboard):**
|
|
147
|
+
|
|
148
|
+
// Read initial state on mount
|
|
149
|
+
const initialId = (window as any).Kama?.state?.get('kama-forms', 'activeFormId');
|
|
150
|
+
|
|
151
|
+
// Listen for live changes
|
|
152
|
+
(window as any).Kama?.events?.on('kama-forms:select-form', (payload) => {
|
|
153
|
+
console.log("Form Selected:", payload.formId);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. Extension YAML Schemas (`content/`)
|
|
159
|
+
|
|
160
|
+
Extensions can bundle their own content schemas (Nodes, Blocks, Taxonomies). Place them in your extension's `content/` folder (e.g., `extensions/forms/content/blocks/form.yaml`).
|
|
161
|
+
|
|
162
|
+
Run `npm run build:schema` to compile them automatically into KAMA.
|
|
163
|
+
|
|
164
|
+
### 🎯 NEW: Dynamic API Dropdowns (`optionsEndpoint`)
|
|
165
|
+
|
|
166
|
+
KAMA's frontend features Universal Async Field fetching. You can define a `select` or `pills` field that dynamically pulls its options from your extension's API endpoint!
|
|
167
|
+
|
|
168
|
+
**Example YAML Block Definition:**
|
|
169
|
+
|
|
170
|
+
# extensions/forms/content/blocks/form-block.yaml
|
|
171
|
+
|
|
172
|
+
id: form
|
|
173
|
+
label: Dynamic Form
|
|
174
|
+
description: Embeds a form definition into the page.
|
|
175
|
+
category: Interactive
|
|
176
|
+
icon: ClipboardList
|
|
177
|
+
|
|
178
|
+
# You can inherit standard KAMA fieldsets!
|
|
179
|
+
|
|
180
|
+
fieldSets:
|
|
181
|
+
|
|
182
|
+
- access
|
|
183
|
+
|
|
184
|
+
fields:
|
|
185
|
+
|
|
186
|
+
# This dropdown populates automatically via your API!
|
|
187
|
+
|
|
188
|
+
formId:
|
|
189
|
+
label: Select Form
|
|
190
|
+
type: select
|
|
191
|
+
required: true
|
|
192
|
+
optionsEndpoint: /api/admin/ext/kama-forms/forms
|
|
193
|
+
optionsValueKey: id # Maps to the option value
|
|
194
|
+
optionsLabelKey: title # Maps to the visible label
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 7. Caching and Extension Updates
|
|
199
|
+
|
|
200
|
+
Because KAMA caches HTML at the Edge (Cloudflare CDN) and inside Worker KV:
|
|
201
|
+
|
|
202
|
+
If your extension API modifies public-facing content (e.g., approving a review, updating inventory), your extension's Hono router must manually trigger KAMA's cache purger to drop the cached Edge pages.
|
|
203
|
+
|
|
204
|
+
_(Note: KAMA's `EdgeCacheManager` is currently scoped to the core, so for now, extensions should rely on KAMA's internal APIs or direct node updates if immediate Edge Purging is required)._
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightsideoy/kama",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
|
-
}
|
|
11
|
+
},
|
|
12
|
+
"./schema.sql": "./schema.sql",
|
|
13
|
+
"./package.json": "./package.json"
|
|
12
14
|
},
|
|
13
15
|
"bin": {
|
|
14
16
|
"kama": "bin/cli.js",
|
|
@@ -22,6 +24,8 @@
|
|
|
22
24
|
"bundle:definitions": "node bin/cli.js bundle",
|
|
23
25
|
"build:schema": "tsx scripts/build-schema.ts",
|
|
24
26
|
"build:seed": "tsx scripts/build-seed.ts",
|
|
27
|
+
"seed": "npm run seed:local",
|
|
28
|
+
"seed:local": "tsx scripts/build-seed.ts && sqlite3 kama.db < seed.sql",
|
|
25
29
|
"build:server": "vite build",
|
|
26
30
|
"build:ui": "vite build --config vite.config.ui.ts",
|
|
27
31
|
"build": "npm run bundle:definitions && npm run build:schema && npm run build:server && npm run build:ui",
|
|
@@ -33,6 +37,7 @@
|
|
|
33
37
|
"access": "public"
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^1.8.1",
|
|
36
41
|
"@heroicons/vue": "^2.2.0",
|
|
37
42
|
"@hono/node-server": "^2.1.1",
|
|
38
43
|
"@hono/swagger-ui": "^0.6.1",
|
|
@@ -40,15 +45,16 @@
|
|
|
40
45
|
"@tiptap/core": "^3.30.0",
|
|
41
46
|
"@tiptap/extension-subscript": "^3.30.1",
|
|
42
47
|
"@tiptap/extension-superscript": "^3.30.1",
|
|
43
|
-
"@tiptap/extension-table": "^3.30.
|
|
44
|
-
"@tiptap/extension-table-cell": "^3.30.
|
|
45
|
-
"@tiptap/extension-table-header": "^3.30.
|
|
46
|
-
"@tiptap/extension-table-row": "^3.30.
|
|
48
|
+
"@tiptap/extension-table": "^3.30.1",
|
|
49
|
+
"@tiptap/extension-table-cell": "^3.30.1",
|
|
50
|
+
"@tiptap/extension-table-header": "^3.30.1",
|
|
51
|
+
"@tiptap/extension-table-row": "^3.30.1",
|
|
47
52
|
"@tiptap/pm": "^3.30.0",
|
|
48
53
|
"@tiptap/starter-kit": "^3.30.0",
|
|
49
54
|
"@tiptap/vue-3": "^3.30.0",
|
|
50
55
|
"drizzle-orm": "^0.45.2",
|
|
51
56
|
"hono": "^4.13.2",
|
|
57
|
+
"js-yaml": "^5.5.0",
|
|
52
58
|
"locale-codes": "^1.3.1",
|
|
53
59
|
"lru-cache": "^11.5.2",
|
|
54
60
|
"lucide-vue-next": "^1.0.0",
|
|
@@ -63,6 +69,7 @@
|
|
|
63
69
|
"@iconify-json/heroicons": "^1.2.3",
|
|
64
70
|
"@iconify-json/lucide": "^1.2.123",
|
|
65
71
|
"@libsql/client": "^0.14.0",
|
|
72
|
+
"@types/js-yaml": "^4.0.9",
|
|
66
73
|
"@types/node": "^26.2.0",
|
|
67
74
|
"@types/sharp": "^0.31.1",
|
|
68
75
|
"@types/yaml": "^1.9.6",
|
package/schema.sql
CHANGED
|
@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS "content_schemas" (
|
|
|
13
13
|
|
|
14
14
|
CREATE TABLE IF NOT EXISTS "users" (
|
|
15
15
|
id TEXT PRIMARY KEY,
|
|
16
|
-
email
|
|
16
|
+
email UNIQUE NOT NULL,
|
|
17
17
|
name TEXT NOT NULL,
|
|
18
18
|
globalRole TEXT DEFAULT 'editor',
|
|
19
19
|
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -92,6 +92,8 @@ CREATE TABLE IF NOT EXISTS "content_nodes" (
|
|
|
92
92
|
language TEXT,
|
|
93
93
|
isHidden INTEGER NOT NULL DEFAULT 0,
|
|
94
94
|
status TEXT DEFAULT 'draft',
|
|
95
|
+
startShowingAt TEXT,
|
|
96
|
+
stopShowingAt TEXT,
|
|
95
97
|
hasUnpublishedChanges INTEGER DEFAULT 0,
|
|
96
98
|
disabledRegionsJson TEXT DEFAULT '{}',
|
|
97
99
|
dataJson TEXT,
|
|
@@ -201,6 +203,40 @@ CREATE TABLE IF NOT EXISTS "node_terms" (
|
|
|
201
203
|
FOREIGN KEY (termId) REFERENCES taxonomy_terms(id) ON DELETE CASCADE
|
|
202
204
|
);
|
|
203
205
|
|
|
206
|
+
CREATE TABLE IF NOT EXISTS "asset_terms" (
|
|
207
|
+
assetId TEXT NOT NULL,
|
|
208
|
+
termId INTEGER NOT NULL,
|
|
209
|
+
PRIMARY KEY (assetId, termId),
|
|
210
|
+
FOREIGN KEY (assetId) REFERENCES assets(id) ON DELETE CASCADE,
|
|
211
|
+
FOREIGN KEY (termId) REFERENCES taxonomy_terms(id) ON DELETE CASCADE
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
CREATE TABLE IF NOT EXISTS "node_relations" (
|
|
215
|
+
sourceNodeId INTEGER NOT NULL,
|
|
216
|
+
targetNodeId INTEGER NOT NULL,
|
|
217
|
+
fieldKey TEXT NOT NULL,
|
|
218
|
+
sortOrder INTEGER DEFAULT 0,
|
|
219
|
+
PRIMARY KEY (sourceNodeId, targetNodeId, fieldKey),
|
|
220
|
+
FOREIGN KEY (sourceNodeId) REFERENCES content_nodes(id) ON DELETE CASCADE,
|
|
221
|
+
FOREIGN KEY (targetNodeId) REFERENCES content_nodes(id) ON DELETE CASCADE
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
CREATE TABLE IF NOT EXISTS "node_routes" (
|
|
225
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
226
|
+
path TEXT NOT NULL,
|
|
227
|
+
channelId INTEGER NOT NULL,
|
|
228
|
+
nodeId INTEGER,
|
|
229
|
+
treeId INTEGER,
|
|
230
|
+
isCanonical INTEGER DEFAULT 1,
|
|
231
|
+
redirectUrl TEXT,
|
|
232
|
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
233
|
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
234
|
+
FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE,
|
|
235
|
+
FOREIGN KEY (nodeId) REFERENCES content_nodes(id) ON DELETE CASCADE,
|
|
236
|
+
FOREIGN KEY (treeId) REFERENCES trees(id) ON DELETE CASCADE,
|
|
237
|
+
UNIQUE(path, channelId)
|
|
238
|
+
);
|
|
239
|
+
|
|
204
240
|
CREATE INDEX IF NOT EXISTS idx_tree_nodes_tree_parent ON tree_nodes(treeId, parentId);
|
|
205
241
|
CREATE INDEX IF NOT EXISTS idx_tree_nodes_contentId ON tree_nodes(contentId);
|
|
206
242
|
CREATE INDEX IF NOT EXISTS idx_content_nodes_slug_status ON content_nodes(slug, status);
|
|
@@ -233,3 +269,10 @@ CREATE INDEX IF NOT EXISTS idx_node_terms_node ON node_terms(nodeId);
|
|
|
233
269
|
CREATE INDEX IF NOT EXISTS idx_node_terms_term ON node_terms(termId);
|
|
234
270
|
CREATE INDEX IF NOT EXISTS idx_node_terms_node_term ON node_terms(nodeId, termId);
|
|
235
271
|
CREATE INDEX IF NOT EXISTS idx_node_terms_term_node ON node_terms(termId, nodeId);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_asset_terms_asset ON asset_terms(assetId);
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_asset_terms_term ON asset_terms(termId);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_node_relations_source ON node_relations(sourceNodeId);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_node_relations_target ON node_relations(targetNodeId);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_node_routes_lookup ON node_routes(path, channelId);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_node_routes_node ON node_routes(nodeId);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_content_nodes_public_feed ON content_nodes(contentType, isHidden, status, startShowingAt, stopShowingAt, updatedAt DESC);
|
|
@@ -5,6 +5,7 @@ import ImageBlock from "@/components/blocks/ImageBlock.astro";
|
|
|
5
5
|
import HeaderImage from "@/components/blocks/HeaderImage.astro";
|
|
6
6
|
import ColumnLayout from "@/components/blocks/ColumnLayout.astro";
|
|
7
7
|
import HtmlBlock from "@/components/blocks/HtmlBlock.astro";
|
|
8
|
+
import NodeList from "@/components/blocks/NodeList.astro"; // 🎯 NEW IMPORT
|
|
8
9
|
import { extractHtml } from "@/utils/tiptap";
|
|
9
10
|
|
|
10
11
|
interface Block {
|
|
@@ -61,6 +62,14 @@ const visibleBlocks = safeBlocks.filter(b => {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
switch (componentId) {
|
|
65
|
+
// 🎯 ADDED NODE LIST CASES
|
|
66
|
+
case "node-list":
|
|
67
|
+
case "nodelist":
|
|
68
|
+
case "article-list":
|
|
69
|
+
case "content-feed": {
|
|
70
|
+
return <NodeList data={data} />;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
case "header":
|
|
65
74
|
case "headline":
|
|
66
75
|
case "header-block":
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/Form.astro
|
|
3
|
+
interface Props {
|
|
4
|
+
data: {
|
|
5
|
+
formId: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { data } = Astro.props;
|
|
10
|
+
const formId = data?.formId;
|
|
11
|
+
|
|
12
|
+
const kamaApiUrl = import.meta.env.KAMA_API_URL || 'http://localhost:8787';
|
|
13
|
+
const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY;
|
|
14
|
+
|
|
15
|
+
// Fetch form YAML schema at render time
|
|
16
|
+
let schema = null;
|
|
17
|
+
if (formId) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${kamaApiUrl}/api/ext/kama-forms/render/${formId}`);
|
|
20
|
+
if (res.ok) schema = await res.json();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`Failed to fetch schema for ${formId}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const requiresTurnstile = schema?.settings?.requireTurnstile === true;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
{schema ? (
|
|
30
|
+
<div class="kama-form-wrapper" data-form-id={formId} data-api-url={kamaApiUrl}>
|
|
31
|
+
|
|
32
|
+
<!-- 🎯 Conditionally load Cloudflare Turnstile Script -->
|
|
33
|
+
{requiresTurnstile && (
|
|
34
|
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
<div class="form-header">
|
|
38
|
+
<h3>{schema.title}</h3>
|
|
39
|
+
{schema.description && <p>{schema.description}</p>}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="form-alert success" style="display: none;"></div>
|
|
43
|
+
<div class="form-alert error" style="display: none;"></div>
|
|
44
|
+
|
|
45
|
+
<form class="kama-public-form">
|
|
46
|
+
{schema.fields?.map((field: any) => (
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label for={`field-${field.name}`}>
|
|
49
|
+
{field.label}
|
|
50
|
+
{field.required && <span class="required">*</span>}
|
|
51
|
+
</label>
|
|
52
|
+
|
|
53
|
+
{field.type === 'textarea' ? (
|
|
54
|
+
<textarea id={`field-${field.name}`} name={field.name} placeholder={field.placeholder || ''} required={field.required} rows="4" />
|
|
55
|
+
) : field.type === 'select' ? (
|
|
56
|
+
<select id={`field-${field.name}`} name={field.name} required={field.required}>
|
|
57
|
+
<option value="" disabled selected>{field.placeholder || 'Select...'}</option>
|
|
58
|
+
{field.options?.map((opt: any) => (
|
|
59
|
+
<option value={opt.value}>{opt.label}</option>
|
|
60
|
+
))}
|
|
61
|
+
</select>
|
|
62
|
+
) : (
|
|
63
|
+
<input type={field.type || 'text'} id={`field-${field.name}`} name={field.name} placeholder={field.placeholder || ''} required={field.required} />
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
|
|
68
|
+
<!-- 🎯 Turnstile Widget renders here -->
|
|
69
|
+
{requiresTurnstile && turnstileSiteKey && (
|
|
70
|
+
<div class="form-group turnstile-container">
|
|
71
|
+
<div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<button type="submit" class="submit-btn">Submit</button>
|
|
76
|
+
</form>
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<p class="form-error">Form definition "{formId}" could not be loaded.</p>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
document.querySelectorAll('.kama-form-wrapper').forEach((container) => {
|
|
84
|
+
const form = container.querySelector<HTMLFormElement>('.kama-public-form');
|
|
85
|
+
const formId = container.getAttribute('data-form-id');
|
|
86
|
+
const apiUrl = container.getAttribute('data-api-url');
|
|
87
|
+
const successBox = container.querySelector<HTMLDivElement>('.form-alert.success');
|
|
88
|
+
const errorBox = container.querySelector<HTMLDivElement>('.form-alert.error');
|
|
89
|
+
const submitBtn = container.querySelector<HTMLButtonElement>('.submit-btn');
|
|
90
|
+
|
|
91
|
+
if (!form || !formId || !apiUrl) return;
|
|
92
|
+
|
|
93
|
+
form.addEventListener('submit', async (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
96
|
+
if (successBox) successBox.style.display = 'none';
|
|
97
|
+
if (errorBox) errorBox.style.display = 'none';
|
|
98
|
+
|
|
99
|
+
// FormData automatically picks up the hidden "cf-turnstile-response" input
|
|
100
|
+
const formData = new FormData(form);
|
|
101
|
+
const payload = Object.fromEntries(formData.entries());
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${apiUrl}/api/ext/kama-forms/submit/${formId}`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify(payload)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await res.json();
|
|
111
|
+
|
|
112
|
+
if (res.ok && result.success) {
|
|
113
|
+
if (successBox) {
|
|
114
|
+
successBox.textContent = result.message || 'Form submitted successfully!';
|
|
115
|
+
successBox.style.display = 'block';
|
|
116
|
+
}
|
|
117
|
+
form.reset();
|
|
118
|
+
|
|
119
|
+
// 🎯 Reset Turnstile after successful submission if the user wants to submit again
|
|
120
|
+
if (window.turnstile) {
|
|
121
|
+
window.turnstile.reset();
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (errorBox) {
|
|
125
|
+
errorBox.textContent = result.error || 'Form submission failed.';
|
|
126
|
+
errorBox.style.display = 'block';
|
|
127
|
+
}
|
|
128
|
+
// 🎯 Reset Turnstile on failure so they get a fresh token
|
|
129
|
+
if (window.turnstile) {
|
|
130
|
+
window.turnstile.reset();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (errorBox) {
|
|
135
|
+
errorBox.textContent = 'Server connection error.';
|
|
136
|
+
errorBox.style.display = 'block';
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
@@ -45,13 +45,13 @@ if (region.includes("_")) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Default single-column layout.
|
|
48
|
+
* Default single-column layout (aligned to backend CDN buckets).
|
|
49
49
|
*/
|
|
50
50
|
let maxWidth = "1352";
|
|
51
51
|
let targetColumnWidth = 1352;
|
|
52
52
|
|
|
53
|
-
// 1-col:
|
|
54
|
-
let optimizedWidths = [
|
|
53
|
+
// 1-col: 1x/2x desktop and retina buckets
|
|
54
|
+
let optimizedWidths = [768, 1352, 1920, 2560];
|
|
55
55
|
let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -61,8 +61,7 @@ if (columnCount === 2) {
|
|
|
61
61
|
maxWidth = "660";
|
|
62
62
|
targetColumnWidth = 660;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
optimizedWidths = [400, 660, 800, 1320];
|
|
64
|
+
optimizedWidths = [640, 768, 1024, 1352];
|
|
66
65
|
sizes =
|
|
67
66
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
68
67
|
"(max-width: 1399px) calc((100vw - 80px) / 2), " +
|
|
@@ -76,8 +75,7 @@ else if (columnCount === 3) {
|
|
|
76
75
|
maxWidth = "429";
|
|
77
76
|
targetColumnWidth = 429;
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
optimizedWidths = [300, 429, 858, 1024];
|
|
78
|
+
optimizedWidths = [320, 640, 768, 1024];
|
|
81
79
|
sizes =
|
|
82
80
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
83
81
|
"(max-width: 1399px) calc((100vw - 112px) / 3), " +
|
|
@@ -91,8 +89,7 @@ else if (columnCount === 4) {
|
|
|
91
89
|
maxWidth = "314";
|
|
92
90
|
targetColumnWidth = 314;
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
optimizedWidths = [264, 314, 628, 1024];
|
|
92
|
+
optimizedWidths = [320, 640, 768];
|
|
96
93
|
sizes =
|
|
97
94
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
98
95
|
"(max-width: 1399px) calc((100vw - 144px) / 4), " +
|
|
@@ -106,12 +103,9 @@ else if (columnCount === 4) {
|
|
|
106
103
|
sizes={sizes}
|
|
107
104
|
desktopWidths={optimizedWidths}
|
|
108
105
|
idealWidth={targetColumnWidth}
|
|
109
|
-
width={data.width || data.image_meta?.width}
|
|
110
|
-
height={data.height || data.image_meta?.height}
|
|
111
106
|
class="responsive-header-image"
|
|
112
107
|
loading={isLcp ? "eager" : "lazy"}
|
|
113
108
|
fetchpriority={isLcp ? "high" : "auto"}
|
|
114
|
-
decoding={isLcp ? "sync" : "async"}
|
|
115
109
|
/>
|
|
116
110
|
</div>
|
|
117
111
|
|
|
@@ -26,31 +26,31 @@ if (region.includes("_")) {
|
|
|
26
26
|
if (region.includes("_col4")) columnCount = 4;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// 🎯 Column Config Lookup (
|
|
29
|
+
// 🎯 Column Config Lookup (Target width, aligned CDN breakpoint widths, sizes)
|
|
30
30
|
const COLUMN_CONFIGS: Record<
|
|
31
31
|
number,
|
|
32
32
|
{ targetWidth: number; widths: number[]; sizes: string }
|
|
33
33
|
> = {
|
|
34
34
|
1: {
|
|
35
35
|
targetWidth: 1352,
|
|
36
|
-
widths: [768, 1352],
|
|
36
|
+
widths: [768, 1352, 1920, 2560],
|
|
37
37
|
sizes: "(max-width: 1399px) calc(100vw - 48px), 1352px",
|
|
38
38
|
},
|
|
39
39
|
2: {
|
|
40
40
|
targetWidth: 660,
|
|
41
|
-
widths: [
|
|
41
|
+
widths: [640, 768, 1024, 1352],
|
|
42
42
|
sizes:
|
|
43
43
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 80px) / 2), 660px",
|
|
44
44
|
},
|
|
45
45
|
3: {
|
|
46
46
|
targetWidth: 429,
|
|
47
|
-
widths: [
|
|
47
|
+
widths: [320, 640, 768, 1024],
|
|
48
48
|
sizes:
|
|
49
49
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 112px) / 3), 429px",
|
|
50
50
|
},
|
|
51
51
|
4: {
|
|
52
52
|
targetWidth: 314,
|
|
53
|
-
widths: [
|
|
53
|
+
widths: [320, 640, 768],
|
|
54
54
|
sizes:
|
|
55
55
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 144px) / 4), 314px",
|
|
56
56
|
},
|
|
@@ -67,7 +67,6 @@ const layout = COLUMN_CONFIGS[columnCount] || COLUMN_CONFIGS[1];
|
|
|
67
67
|
sizes={layout.sizes}
|
|
68
68
|
desktopWidths={layout.widths}
|
|
69
69
|
idealWidth={layout.targetWidth}
|
|
70
|
-
maxWidth={layout.targetWidth}
|
|
71
70
|
class="free-crop-image"
|
|
72
71
|
loading={isLcp ? "eager" : "lazy"}
|
|
73
72
|
fetchpriority={isLcp ? "high" : "auto"}
|