@brightsideoy/kama 0.1.0-alpha.99 → 1.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +169 -0
  2. package/bin/cli.js +738 -261
  3. package/content/definitions/blocks/button.yaml +34 -0
  4. package/content/definitions/blocks/header.yaml +13 -0
  5. package/content/definitions/blocks/header_image.yaml +21 -0
  6. package/content/definitions/blocks/hero.yaml +28 -0
  7. package/content/definitions/blocks/html.yaml +13 -0
  8. package/content/definitions/blocks/image_card.yaml +56 -0
  9. package/content/definitions/blocks/node-list.yaml +61 -0
  10. package/content/definitions/blocks/single_image.yaml +27 -0
  11. package/content/definitions/blocks/text.yaml +15 -0
  12. package/content/definitions/channels/ecommerce.yaml +28 -0
  13. package/content/definitions/channels/website.yaml +41 -0
  14. package/{defaults → content}/definitions/field-sets/access.yaml +3 -5
  15. package/content/definitions/field-sets/channel-base.yaml +57 -0
  16. package/{defaults/definitions/field-sets/page-meta.yaml → content/definitions/field-sets/channel-seo.yaml} +3 -11
  17. package/content/definitions/field-sets/content-block.yaml +41 -0
  18. package/content/definitions/field-sets/page-base.yaml +44 -0
  19. package/content/definitions/field-sets/page-seo.yaml +34 -0
  20. package/{defaults → content}/definitions/field-sets/page-social.yaml +6 -8
  21. package/content/definitions/field-sets/schema-type.yaml +43 -0
  22. package/content/definitions/field-sets/tree-base.yaml +31 -0
  23. package/content/definitions/node-layouts/content.yml +12 -0
  24. package/{defaults → content}/definitions/node-layouts/landing-page.yml +8 -3
  25. package/{defaults → content}/definitions/node-layouts/single-column.yml +10 -5
  26. package/{defaults → content}/definitions/node-layouts/two-column.yml +14 -6
  27. package/content/definitions/nodes/address.yaml +40 -0
  28. package/content/definitions/nodes/article.yaml +65 -0
  29. package/content/definitions/nodes/event.yaml +55 -0
  30. package/content/definitions/nodes/external-link.yaml +29 -0
  31. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  32. package/{defaults → content}/definitions/nodes/page.yaml +9 -20
  33. package/content/definitions/nodes/person.yaml +37 -0
  34. package/content/definitions/nodes/product.yaml +89 -0
  35. package/content/definitions/taxonomies/categories.yaml +15 -0
  36. package/content/definitions/taxonomies/tags.yaml +12 -0
  37. package/content/definitions/trees/navigation.yaml +24 -0
  38. package/definitions/editor/default.ts +1 -0
  39. package/dist/assets/index-BVkEzCNs.js +164 -0
  40. package/dist/assets/index-CGLx9oqF.css +1 -0
  41. package/dist/build-seed.js +486 -0
  42. package/dist/dist-kIJlDMvm.js +27162 -0
  43. package/dist/index.html +4 -2
  44. package/dist/index.js +26186 -8576
  45. package/dist/schema-compiler.js +48 -0
  46. package/documentation/SCHEMAS.md +182 -0
  47. package/package.json +24 -25
  48. package/schema.sql +67 -23
  49. package/templates/astro-starter/astro.config.mjs +27 -0
  50. package/templates/astro-starter/package.json.template +20 -0
  51. package/templates/astro-starter/public/robots.txt +23 -0
  52. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +143 -0
  53. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +67 -0
  54. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +126 -0
  55. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  56. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +25 -0
  57. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +91 -0
  58. package/templates/astro-starter/src/components/blocks/NodeList.astro +278 -0
  59. package/templates/astro-starter/src/components/blocks/TextBlock.astro +15 -0
  60. package/templates/astro-starter/src/components/global/Breadcrumb.astro +108 -0
  61. package/templates/astro-starter/src/components/global/Footer.astro +50 -0
  62. package/templates/astro-starter/src/components/global/Header.astro +171 -0
  63. package/templates/astro-starter/src/components/global/MetaHead.astro +266 -0
  64. package/templates/astro-starter/src/components/global/NavigationMenu.astro +413 -0
  65. package/templates/astro-starter/src/components/ui/KamaImage.astro +180 -0
  66. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +56 -0
  67. package/templates/astro-starter/src/layouts/StandardLayout.astro +63 -0
  68. package/templates/astro-starter/src/lib/cms.ts +219 -0
  69. package/templates/astro-starter/src/lib/media/kama-image-service.ts +67 -0
  70. package/templates/astro-starter/src/lib/media/media-proxy.ts +148 -0
  71. package/templates/astro-starter/src/lib/media/media-url.ts +112 -0
  72. package/templates/astro-starter/src/middleware.ts +508 -0
  73. package/templates/astro-starter/src/pages/404.astro +46 -0
  74. package/templates/astro-starter/src/pages/[...slug].astro +318 -0
  75. package/templates/astro-starter/src/pages/api/load-nodes.ts +40 -0
  76. package/templates/astro-starter/src/pages/llms-full.txt.ts +171 -0
  77. package/templates/astro-starter/src/pages/llms.txt.ts +113 -0
  78. package/templates/astro-starter/src/pages/media/[...path].ts +14 -0
  79. package/templates/astro-starter/src/pages/partials/footer.astro +69 -0
  80. package/templates/astro-starter/src/pages/partials/nav.astro +141 -0
  81. package/templates/astro-starter/src/styles/standard-theme.css +157 -0
  82. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  83. package/templates/astro-starter/tsconfig.json +0 -0
  84. package/templates/kama-worker/index.js +19 -0
  85. package/templates/kama-worker/server.js +104 -0
  86. package/wrangler.json +22 -0
  87. package/defaults/definitions/blocks/button.yaml +0 -15
  88. package/defaults/definitions/blocks/header.yaml +0 -15
  89. package/defaults/definitions/blocks/header_image.yaml +0 -22
  90. package/defaults/definitions/blocks/hero.yaml +0 -27
  91. package/defaults/definitions/blocks/image_card.yaml +0 -57
  92. package/defaults/definitions/blocks/single_image.yaml +0 -28
  93. package/defaults/definitions/blocks/text.yaml +0 -16
  94. package/defaults/definitions/channels/ecommerce.yaml +0 -24
  95. package/defaults/definitions/channels/website.yaml +0 -32
  96. package/defaults/definitions/field-sets/channel-base.yaml +0 -31
  97. package/defaults/definitions/field-sets/content-block.yaml +0 -46
  98. package/defaults/definitions/field-sets/page-base.yaml +0 -49
  99. package/defaults/definitions/field-sets/tree-base.yaml +0 -31
  100. package/defaults/definitions/node-layouts/three-column.yml +0 -41
  101. package/defaults/definitions/nodes/address.yaml +0 -32
  102. package/defaults/definitions/nodes/article.yaml +0 -43
  103. package/defaults/definitions/nodes/event.yaml +0 -49
  104. package/defaults/definitions/nodes/external-link.yaml +0 -26
  105. package/defaults/definitions/nodes/person.yaml +0 -37
  106. package/defaults/definitions/nodes/product.yaml +0 -86
  107. package/defaults/definitions/trees/navigation.yaml +0 -29
  108. package/defaults/definitions/trees/social-links.yaml +0 -9
  109. package/definitions/_page.yaml +0 -52
  110. package/dist/assets/index-D6qQsWOj.js +0 -1392
  111. package/dist/assets/index-TdW5F0pX.css +0 -1
  112. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  113. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  114. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  115. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
  116. package/scripts/bundle-definitions.js +0 -176
  117. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  118. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  119. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  120. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
@@ -0,0 +1,148 @@
1
+ // src/lib/media/media-proxy.ts
2
+
3
+ import type { APIContext } from "astro";
4
+
5
+ export function getMimeType(filePath: string): string {
6
+ const ext = filePath.split(".").pop()?.toLowerCase();
7
+ switch (ext) {
8
+ case "jpg":
9
+ case "jpeg": return "image/jpeg";
10
+ case "png": return "image/png";
11
+ case "webp": return "image/webp";
12
+ case "avif": return "image/avif";
13
+ case "svg": return "image/svg+xml";
14
+ case "gif": return "image/gif";
15
+ case "pdf": return "application/pdf";
16
+ case "mp4": return "video/mp4";
17
+ case "webm": return "video/webm";
18
+ default: return "application/octet-stream";
19
+ }
20
+ }
21
+
22
+ export async function handleMediaProxy({ request, params, url, locals }: APIContext): Promise<Response> {
23
+ // 🎯 Safely extract Cloudflare context from Astro locals dual-environment
24
+ const runtime = locals.runtime || (locals as any).cfContext;
25
+ const workerEnv = runtime?.env || {};
26
+ const cfContext = runtime?.ctx || {};
27
+
28
+ // 🎯 Detect if Astro is requesting a resized variant
29
+ const hasResizeParams = url.searchParams.has('w') || url.searchParams.has('h') || url.searchParams.has('q') || url.searchParams.has('f');
30
+
31
+ // 🎯 Resolve Negotiated Format for Edge Cache Key
32
+ const acceptHeader = request.headers.get('Accept') || '';
33
+ const fParam = url.searchParams.get('f');
34
+ let resolvedFormat = fParam && fParam !== 'auto' ? fParam : '';
35
+
36
+ if (!resolvedFormat && hasResizeParams) {
37
+ if (acceptHeader.includes('image/avif')) resolvedFormat = 'avif';
38
+ else if (acceptHeader.includes('image/webp')) resolvedFormat = 'webp';
39
+ }
40
+
41
+ // 🎯 Construct a safe Cache Key that accounts for format negotiation
42
+ let cacheKeyUrl = request.url;
43
+ if (hasResizeParams && resolvedFormat && !fParam) {
44
+ const cacheUrlObj = new URL(cacheKeyUrl);
45
+ cacheUrlObj.searchParams.set('_resolvedFormat', resolvedFormat);
46
+ cacheKeyUrl = cacheUrlObj.toString();
47
+ }
48
+ const cacheKey = new Request(cacheKeyUrl, request);
49
+
50
+ // 1. Cloudflare Edge Cache API
51
+ let edgeCache: Cache | null = null;
52
+ try {
53
+ if (typeof caches !== "undefined" && caches.default) {
54
+ edgeCache = caches.default;
55
+ const cachedResponse = await edgeCache.match(cacheKey);
56
+ if (cachedResponse) return cachedResponse;
57
+ }
58
+ } catch (err) {
59
+ console.warn("[Media Proxy] Edge cache match failed:", err);
60
+ }
61
+
62
+ const rawPath = params.path || "";
63
+ const cleanPath = rawPath.replace(/^\/+|\/+$/g, "");
64
+
65
+ // 2. Direct R2 Lookup (ONLY IF NO RESIZING PARAMS ARE PRESENT AND WE ARE IN PROD)
66
+ const mediaBucket = workerEnv.MEDIA_BUCKET;
67
+ if (mediaBucket && cleanPath && !hasResizeParams) {
68
+ try {
69
+ let r2Object = await mediaBucket.get(cleanPath);
70
+ if (!r2Object) r2Object = await mediaBucket.get(cleanPath.startsWith("media/") ? cleanPath.replace(/^media\//, "") : `media/${cleanPath}`);
71
+
72
+ if (r2Object) {
73
+ if (request.headers.get("if-none-match") === r2Object.httpEtag) {
74
+ return new Response(null, { status: 304 });
75
+ }
76
+
77
+ const headers = new Headers();
78
+ r2Object.writeHttpMetadata(headers);
79
+ headers.set("etag", r2Object.httpEtag);
80
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
81
+ headers.set("Access-Control-Allow-Origin", "*");
82
+ if (!headers.get("content-type") || headers.get("content-type") === "application/octet-stream") {
83
+ headers.set("content-type", getMimeType(cleanPath));
84
+ }
85
+
86
+ const r2Response = new Response(r2Object.body, { status: 200, headers });
87
+
88
+ if (edgeCache) {
89
+ const cachePromise = edgeCache.put(cacheKey, r2Response.clone());
90
+ if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
91
+ else cachePromise.catch(e => console.warn(e));
92
+ }
93
+
94
+ return r2Response;
95
+ }
96
+ } catch (r2Err) {
97
+ console.warn("[Media Proxy] R2 lookup error:", r2Err);
98
+ }
99
+ }
100
+
101
+ // 3. Forward to Backend (Handles Local Sharp Resizing AND Cloudflare Images Resizing)
102
+ try {
103
+ let backendResponse: Response;
104
+
105
+ // A. Production Mode (Cloudflare Service Binding)
106
+ if (workerEnv.CMS_WORKER) {
107
+ backendResponse = await workerEnv.CMS_WORKER.fetch(request);
108
+ }
109
+ // B. Local Dev Mode (Standard HTTP Fetch to local Hono Server)
110
+ else {
111
+ const cmsUrl = import.meta.env.PUBLIC_CMS_URL || 'http://localhost:8787';
112
+ const targetUrl = `${cmsUrl}${url.pathname}${url.search}`;
113
+
114
+ // Forward request headers (like Accept for WebP/AVIF negotiation)
115
+ backendResponse = await fetch(targetUrl, {
116
+ headers: {
117
+ 'Accept': request.headers.get('Accept') || 'image/webp,image/avif,image/*,*/*;q=0.8'
118
+ }
119
+ });
120
+ }
121
+
122
+ if (backendResponse.ok) {
123
+ const newHeaders = new Headers(backendResponse.headers);
124
+ newHeaders.set("Cache-Control", "public, max-age=31536000, immutable");
125
+ newHeaders.set("Access-Control-Allow-Origin", "*");
126
+
127
+ const proxyResponse = new Response(backendResponse.body, {
128
+ status: backendResponse.status,
129
+ statusText: backendResponse.statusText,
130
+ headers: newHeaders,
131
+ });
132
+
133
+ if (edgeCache) {
134
+ const cachePromise = edgeCache.put(cacheKey, proxyResponse.clone());
135
+ if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
136
+ else cachePromise.catch(e => console.warn(e));
137
+ }
138
+
139
+ return proxyResponse;
140
+ }
141
+
142
+ return backendResponse;
143
+ } catch (error) {
144
+ console.error("❌ Media Proxy Forwarding Error:", error);
145
+ }
146
+
147
+ return new Response("Not found", { status: 404 });
148
+ }
@@ -0,0 +1,112 @@
1
+ // src/lib/media/media-url.ts
2
+
3
+ /**
4
+ * Normalizes absolute CMS URLs into clean relative /media/... paths.
5
+ * Ensures CSP compliance by routing requests through the Astro frontend proxy.
6
+ */
7
+ export function toRelativeMediaPath(rawUrl: string): string {
8
+ if (!rawUrl) return "";
9
+
10
+ let clean = String(rawUrl).trim();
11
+
12
+ try {
13
+ // Parse absolute or relative URLs safely using a dummy origin fallback
14
+ const urlObj = new URL(clean, "http://localhost");
15
+ clean = urlObj.pathname;
16
+ } catch {
17
+ // Fallback cleanup for malformed strings
18
+ clean = clean.replace(/^https?:\/\/[^/]+/, "").split("?")[0].split("#")[0];
19
+ }
20
+
21
+ const mediaIndex = clean.indexOf("/media/");
22
+ if (mediaIndex !== -1) {
23
+ return `/media/${clean
24
+ .slice(mediaIndex + "/media/".length)
25
+ .replace(/^\/+/, "")}`;
26
+ }
27
+
28
+ return clean.startsWith("/") ? clean : `/${clean}`;
29
+ }
30
+
31
+ /**
32
+ * Checks whether a given path or MIME type represents a vector SVG image.
33
+ */
34
+ export function isSvgImage(urlOrMime: string): boolean {
35
+ if (!urlOrMime) return false;
36
+ const lower = urlOrMime.toLowerCase();
37
+ return lower.endsWith('.svg') || lower.includes('image/svg+xml') || lower.includes('.svg?');
38
+ }
39
+
40
+ /**
41
+ * Constructs an agnostic /media/... URL with standard optimization query parameters.
42
+ */
43
+ export function buildAgnosticImageUrl(
44
+ url: string,
45
+ width: number,
46
+ quality = 75,
47
+ ): string {
48
+ const cleanPath = toRelativeMediaPath(url);
49
+ if (!cleanPath) return "";
50
+
51
+ // 🎯 Vector Guard: Return pristine path for SVGs without width/quality query parameters
52
+ if (isSvgImage(cleanPath)) {
53
+ return cleanPath;
54
+ }
55
+
56
+ const urlObj = new URL(cleanPath, "http://localhost");
57
+ urlObj.searchParams.set("w", String(width));
58
+ urlObj.searchParams.set("q", String(quality));
59
+ urlObj.searchParams.set("f", "auto");
60
+
61
+ return urlObj.pathname + urlObj.search;
62
+ }
63
+
64
+ export interface GetImageUriOptions {
65
+ width?: number;
66
+ quality?: number;
67
+ fallback?: string;
68
+ absoluteOrigin?: string; // e.g. siteOrigin for OG/Twitter tags or JSON-LD
69
+ }
70
+
71
+ /**
72
+ * Universal CMS Image Helper
73
+ * Safely resolves strings, asset objects, and array-wrapped media into relative or absolute CSP-compliant URLs.
74
+ */
75
+ export function getImageUri(
76
+ imgObj: any,
77
+ options: GetImageUriOptions = {}
78
+ ): string | null {
79
+ const { width, quality = 75, fallback = null, absoluteOrigin } = options;
80
+
81
+ if (!imgObj) return fallback;
82
+
83
+ // 1. Unpack arrays (e.g. favicon: [{ url: '/media/...' }])
84
+ const target = Array.isArray(imgObj) ? imgObj[0] : imgObj;
85
+ if (!target) return fallback;
86
+
87
+ // 2. Detect mimeType if present on asset object
88
+ const isSvgAsset = isSvgImage(target?.mimeType || "") || isSvgImage(target?.filename || "");
89
+
90
+ // 3. Extract raw URL path across standard CMS field variations
91
+ const rawPath =
92
+ typeof target === "string"
93
+ ? target
94
+ : target?.variants?.standard?.url || target?.url || target?.path || null;
95
+
96
+ if (!rawPath) return fallback;
97
+
98
+ // 4. Process URL through buildAgnosticImageUrl (SVGs bypass raster params automatically)
99
+ let finalPath = (width && !isSvgAsset && !isSvgImage(rawPath))
100
+ ? buildAgnosticImageUrl(rawPath, width, quality)
101
+ : toRelativeMediaPath(rawPath);
102
+
103
+ if (!finalPath) return fallback;
104
+
105
+ // 5. Prepend absolute origin if requested (for Open Graph, Twitter cards, or JSON-LD)
106
+ if (absoluteOrigin) {
107
+ const cleanOrigin = absoluteOrigin.replace(/\/+$/, "");
108
+ return `${cleanOrigin}${finalPath.startsWith("/") ? "" : "/"}${finalPath}`;
109
+ }
110
+
111
+ return finalPath;
112
+ }