@brightsideoy/kama 1.0.0-alpha.2 → 1.0.0-alpha.20
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/node-list.yaml +75 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -0
- package/dist/assets/AssetEditor-D5j7pwcK.css +1 -0
- package/dist/assets/AssetEditor-Dt1zCR7_.js +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/CmsHelp-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-DyPJLGGi.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-DNLwjirk.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-CiXQKXK1.js +147 -0
- package/dist/assets/NodeEditor-pZI3gu4a.css +1 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-DgIg1v6t.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-DQ6_RmCI.js +14 -0
- package/dist/assets/index-DcKiaRjI.css +1 -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-FQRsZBoJ.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-D2-VxsxR.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-D_Q1y4Kh.js} +2314 -1180
- package/dist/index.html +34 -2
- package/dist/index.js +2298 -1158
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +9 -2
- package/schema.sql +34 -1
- package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +143 -0
- package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +126 -0
- package/templates/astro-starter/old_src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +25 -0
- package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +91 -0
- package/templates/astro-starter/old_src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +108 -0
- package/templates/astro-starter/old_src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/old_src/components/global/Header.astro +171 -0
- package/templates/astro-starter/old_src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +413 -0
- package/templates/astro-starter/old_src/components/ui/KamaImage.astro +180 -0
- package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/old_src/layouts/StandardLayout.astro +63 -0
- package/templates/astro-starter/old_src/lib/cms.ts +219 -0
- package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +67 -0
- package/templates/astro-starter/old_src/lib/media/media-proxy.ts +148 -0
- package/templates/astro-starter/old_src/lib/media/media-url.ts +112 -0
- package/templates/astro-starter/old_src/middleware.ts +536 -0
- package/templates/astro-starter/old_src/pages/404.astro +46 -0
- package/templates/astro-starter/old_src/pages/[...slug].astro +311 -0
- package/templates/astro-starter/old_src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +101 -0
- package/templates/astro-starter/old_src/pages/llms-full.txt.ts +171 -0
- package/templates/astro-starter/old_src/pages/llms.txt.ts +113 -0
- package/templates/astro-starter/old_src/pages/media/[...path].ts +14 -0
- package/templates/astro-starter/old_src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/old_src/pages/partials/nav.astro +141 -0
- package/templates/astro-starter/old_src/styles/standard-theme.css +157 -0
- package/templates/astro-starter/old_src/utils/tiptap.ts +113 -0
- 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 +57 -46
- 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
|
@@ -15,7 +15,6 @@ const props = Astro.props as Props;
|
|
|
15
15
|
const currentPropsNode = props.node || {};
|
|
16
16
|
let rawTree: TreeNode[] = props.tree || [];
|
|
17
17
|
|
|
18
|
-
// 🎯 Read the safely extracted environment from middleware (Fixes Astro 7 crash)
|
|
19
18
|
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
20
19
|
const cmsWorker = safeEnv.CMS_WORKER;
|
|
21
20
|
|
|
@@ -41,29 +40,27 @@ const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
|
41
40
|
return fetch(req);
|
|
42
41
|
};
|
|
43
42
|
|
|
44
|
-
// 🎯
|
|
45
|
-
const channelId = getChannelId()
|
|
46
|
-
const treeId = getTreeId()
|
|
43
|
+
// 🎯 Resolve Channel ID dynamically from env/lib without hardcoding "website"
|
|
44
|
+
const channelId = getChannelId();
|
|
45
|
+
const treeId = getTreeId();
|
|
47
46
|
|
|
48
47
|
let channelData: Record<string, any> | null = null;
|
|
49
48
|
|
|
50
49
|
try {
|
|
51
|
-
const [fetchedChannel, fetchedTree] = await Promise.all([
|
|
52
|
-
getChannel(channelId, customFetch),
|
|
53
|
-
!rawTree || rawTree.length === 0
|
|
54
|
-
? getTree(treeId, channelId, customFetch)
|
|
55
|
-
: Promise.resolve(rawTree),
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
channelData = fetchedChannel;
|
|
59
50
|
if (!rawTree || rawTree.length === 0) {
|
|
51
|
+
const [fetchedChannel, fetchedTree] = await Promise.all([
|
|
52
|
+
getChannel(channelId, customFetch),
|
|
53
|
+
getTree(treeId, channelId, customFetch),
|
|
54
|
+
]);
|
|
55
|
+
channelData = fetchedChannel;
|
|
60
56
|
rawTree = fetchedTree || [];
|
|
57
|
+
} else {
|
|
58
|
+
channelData = await getChannel(channelId, customFetch);
|
|
61
59
|
}
|
|
62
60
|
} catch (e) {
|
|
63
61
|
console.error("[Header] Failed to load layout data:", e);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
// 🎯 1. Resolve Logo Text (Strict waterfall prioritizing explicit strings)
|
|
67
64
|
const rawLogoText = channelData?.logoText;
|
|
68
65
|
const rawSiteTitle = channelData?.siteTitle;
|
|
69
66
|
const rawLabel = channelData?.label;
|
|
@@ -82,11 +79,9 @@ if (typeof rawLogoText === "string" && rawLogoText.trim() !== "") {
|
|
|
82
79
|
logoText = import.meta.env.PUBLIC_SITE_NAME || "KAMA";
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
// 🎯 2. Extract Raw Logo Asset (Unwraps array wrappers if present)
|
|
86
82
|
const rawLogoAsset = channelData?.logoImage || channelData?.logo;
|
|
87
83
|
const targetLogo = Array.isArray(rawLogoAsset) ? rawLogoAsset[0] : rawLogoAsset;
|
|
88
84
|
|
|
89
|
-
// 🎯 3. Strict Image Validation Guard (Guarantees non-empty string or populated object)
|
|
90
85
|
const hasValidLogoImage = Boolean(
|
|
91
86
|
targetLogo &&
|
|
92
87
|
((typeof targetLogo === "string" && targetLogo.trim() !== "") ||
|
|
@@ -152,7 +147,6 @@ const hasValidLogoImage = Boolean(
|
|
|
152
147
|
z-index: 1001;
|
|
153
148
|
}
|
|
154
149
|
|
|
155
|
-
/* 🎯 Strict 16px Height Constraint for KamaImage output */
|
|
156
150
|
:global(.logo-image) {
|
|
157
151
|
height: 16px !important;
|
|
158
152
|
max-height: 16px !important;
|
|
@@ -8,25 +8,28 @@ export interface Props {
|
|
|
8
8
|
|
|
9
9
|
const { tree = [] } = Astro.props;
|
|
10
10
|
|
|
11
|
-
// 🎯 Grab the current path directly from Astro's server request
|
|
12
11
|
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
13
|
-
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "index").trim();
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
item
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
// 🎯 FIX: Identify the root node in the tree structure
|
|
14
|
+
const rootNode =
|
|
15
|
+
tree.find(
|
|
16
|
+
item =>
|
|
17
|
+
item.path === "/" ||
|
|
18
|
+
item.path === "" ||
|
|
19
|
+
item.slug === "index" ||
|
|
20
|
+
(item as any).isRoot === true ||
|
|
21
|
+
(item as any).isRootNode === 1,
|
|
22
|
+
) || tree[0];
|
|
22
23
|
|
|
24
|
+
// 🎯 FIX: If rootNode has children, display those children as top-level menu items.
|
|
25
|
+
// If not, filter out the root node by ID AND path.
|
|
23
26
|
const topLevelNavItems =
|
|
24
|
-
|
|
25
|
-
?
|
|
27
|
+
rootNode?.children && rootNode.children.length > 0
|
|
28
|
+
? rootNode.children
|
|
26
29
|
: tree.filter(
|
|
27
30
|
item =>
|
|
28
|
-
item.
|
|
29
|
-
item.slug !==
|
|
31
|
+
item.id !== rootNode?.id &&
|
|
32
|
+
item.slug !== rootNode?.slug &&
|
|
30
33
|
item.path !== "/" &&
|
|
31
34
|
item.path !== "",
|
|
32
35
|
);
|
|
@@ -125,58 +128,66 @@ const isExactActive = (itemPath?: string) => {
|
|
|
125
128
|
</ul>
|
|
126
129
|
</nav>
|
|
127
130
|
|
|
128
|
-
<script>
|
|
129
|
-
|
|
130
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
131
|
+
<script is:inline>
|
|
132
|
+
const initMobileMenu = () => {
|
|
131
133
|
const toggleBtn = document.getElementById("menu-toggle");
|
|
132
134
|
const navOverlay = document.getElementById("nav-overlay");
|
|
133
135
|
|
|
136
|
+
if (!toggleBtn || !navOverlay) return;
|
|
137
|
+
|
|
138
|
+
// Remove old listeners by replacing the nodes (prevents duplicate bindings)
|
|
139
|
+
const newToggleBtn = toggleBtn.cloneNode(true);
|
|
140
|
+
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
|
|
141
|
+
|
|
134
142
|
const closeMenu = () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
document.body.classList.remove("menu-lock-scroll");
|
|
140
|
-
}
|
|
143
|
+
newToggleBtn.setAttribute("aria-expanded", "false");
|
|
144
|
+
newToggleBtn.classList.remove("is-open");
|
|
145
|
+
navOverlay.classList.remove("is-open");
|
|
146
|
+
document.body.classList.remove("menu-lock-scroll");
|
|
141
147
|
};
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
newToggleBtn.addEventListener("click", e => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
const isOpen = newToggleBtn.getAttribute("aria-expanded") === "true";
|
|
152
|
+
newToggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
|
|
153
|
+
newToggleBtn.classList.toggle("is-open");
|
|
154
|
+
navOverlay.classList.toggle("is-open");
|
|
155
|
+
document.body.classList.toggle("menu-lock-scroll");
|
|
156
|
+
});
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (targetUrl.pathname === currentUrl.pathname) closeMenu();
|
|
158
|
-
});
|
|
158
|
+
// Close menu when clicking nav links
|
|
159
|
+
const navLinks = navOverlay.querySelectorAll("a");
|
|
160
|
+
navLinks.forEach(link => {
|
|
161
|
+
link.addEventListener("click", () => {
|
|
162
|
+
closeMenu();
|
|
159
163
|
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
window.addEventListener("pageshow", closeMenu);
|
|
164
|
+
});
|
|
163
165
|
|
|
166
|
+
// Close details dropdowns when clicking outside
|
|
164
167
|
document.addEventListener("click", event => {
|
|
165
168
|
const allDetails = document.querySelectorAll("details.nav-details");
|
|
166
169
|
allDetails.forEach(details => {
|
|
167
|
-
if (
|
|
168
|
-
details.hasAttribute("open") &&
|
|
169
|
-
!details.contains(event.target as Node)
|
|
170
|
-
) {
|
|
170
|
+
if (details.hasAttribute("open") && !details.contains(event.target)) {
|
|
171
171
|
details.removeAttribute("open");
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
|
-
|
|
175
|
+
|
|
176
|
+
window.addEventListener("pageshow", closeMenu);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Run on first load
|
|
180
|
+
if (document.readyState === "loading") {
|
|
181
|
+
document.addEventListener("DOMContentLoaded", initMobileMenu);
|
|
182
|
+
} else {
|
|
183
|
+
initMobileMenu();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Run on every Astro View Transition page navigation
|
|
187
|
+
document.addEventListener("astro:page-load", initMobileMenu);
|
|
176
188
|
</script>
|
|
177
189
|
|
|
178
190
|
<style>
|
|
179
|
-
/* Keeps all of your exact original CSS styles intact! */
|
|
180
191
|
.mobile-toggle {
|
|
181
192
|
display: flex;
|
|
182
193
|
flex-direction: column;
|
|
@@ -20,7 +20,7 @@ interface Props {
|
|
|
20
20
|
const {
|
|
21
21
|
image = {},
|
|
22
22
|
sizes = "100vw",
|
|
23
|
-
desktopWidths = [768, 1352],
|
|
23
|
+
desktopWidths = [768, 1352, 1920, 2560],
|
|
24
24
|
idealWidth = 1352,
|
|
25
25
|
class: className = "",
|
|
26
26
|
loading = "lazy",
|
|
@@ -90,18 +90,26 @@ function buildAgnosticSrcset(
|
|
|
90
90
|
.join(", ");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// 🎯 High-DPI / Retina Optimization:
|
|
94
|
+
// - Mobile: Includes 320, 640, 768 (covers 2x and 3x Retina mobile displays)
|
|
95
|
+
// - Tablet: Includes 768, 1024, 1352 (covers iPad Pro Retina density)
|
|
96
|
+
// - Desktop: Uses desktopWidths (defaults to 768, 1352, 1920, 2560 for 1440p and 4K)
|
|
94
97
|
const mobileSrcset =
|
|
95
|
-
!isSvg && rawMobileUrl
|
|
98
|
+
!isSvg && rawMobileUrl
|
|
99
|
+
? buildAgnosticSrcset(rawMobileUrl, [320, 640, 768])
|
|
100
|
+
: null;
|
|
96
101
|
|
|
97
102
|
const tabletSrcset =
|
|
98
103
|
!isSvg && rawTabletUrl
|
|
99
|
-
? buildAgnosticSrcset(rawTabletUrl, [768, 1024])
|
|
104
|
+
? buildAgnosticSrcset(rawTabletUrl, [768, 1024, 1352])
|
|
100
105
|
: null;
|
|
101
106
|
|
|
102
107
|
const desktopSrcset =
|
|
103
108
|
!isSvg && rawDesktopUrl
|
|
104
|
-
? buildAgnosticSrcset(
|
|
109
|
+
? buildAgnosticSrcset(
|
|
110
|
+
rawDesktopUrl,
|
|
111
|
+
desktopWidths.length ? desktopWidths : [768, 1352, 1920, 2560],
|
|
112
|
+
)
|
|
105
113
|
: null;
|
|
106
114
|
|
|
107
115
|
const fallbackUrl =
|
|
@@ -32,8 +32,8 @@ const {
|
|
|
32
32
|
// (MetaHead will resolve channel.defaultLanguage if this remains undefined)
|
|
33
33
|
const pageLang = language || node?.language || undefined;
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
35
|
+
// 🎯 FIX: Extract the footer blocks from the payload again
|
|
36
|
+
const footerBlocks = content?.footer || [];
|
|
37
37
|
---
|
|
38
38
|
|
|
39
39
|
<!doctype html>
|
|
@@ -57,6 +57,7 @@ const pageLang = language || node?.language || undefined;
|
|
|
57
57
|
</div>
|
|
58
58
|
</main>
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
{/* 🎯 FIX: Pass the blocks and contentMap into the Footer */}
|
|
61
|
+
<Footer blocks={footerBlocks} contentMap={content} />
|
|
61
62
|
</body>
|
|
62
63
|
</html>
|
|
@@ -93,6 +93,14 @@ export function getTreeId(): string {
|
|
|
93
93
|
return import.meta.env.PUBLIC_TREE_ID || 'navigation';
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// 🎯 Safely resolve global authorization header for default CMS fetches
|
|
97
|
+
function getAuthHeaders(): Record<string, string> {
|
|
98
|
+
const secret = import.meta.env.FRONTEND_API_SECRET;
|
|
99
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
100
|
+
if (secret) headers['Authorization'] = `Bearer ${secret}`;
|
|
101
|
+
return headers;
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
/**
|
|
97
105
|
* Fetch channel metadata by key/ID.
|
|
98
106
|
* Serves global site branding, logos, favicons, and schema-defined public fields.
|
|
@@ -106,13 +114,17 @@ export async function getChannel(
|
|
|
106
114
|
|
|
107
115
|
try {
|
|
108
116
|
const res = await customFetch(requestUrl, {
|
|
109
|
-
headers:
|
|
110
|
-
Accept: 'application/json',
|
|
111
|
-
},
|
|
117
|
+
headers: getAuthHeaders(),
|
|
112
118
|
});
|
|
113
119
|
|
|
114
120
|
if (!res.ok) return null;
|
|
115
121
|
|
|
122
|
+
// 🎯 Defensive HTML check
|
|
123
|
+
if (res.headers.get("content-type")?.includes("text/html")) {
|
|
124
|
+
console.error(`❌ [CMS Client] API returned HTML instead of JSON for Channel: ${requestUrl}`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
return (await res.json()) as ChannelResponse;
|
|
117
129
|
} catch (err) {
|
|
118
130
|
console.error('[CMS Client] Failed to fetch public channel:', err);
|
|
@@ -134,13 +146,17 @@ export async function getTree(
|
|
|
134
146
|
|
|
135
147
|
try {
|
|
136
148
|
const res = await customFetch(requestUrl, {
|
|
137
|
-
headers:
|
|
138
|
-
Accept: 'application/json',
|
|
139
|
-
},
|
|
149
|
+
headers: getAuthHeaders(),
|
|
140
150
|
});
|
|
141
151
|
|
|
142
152
|
if (!res.ok) return [];
|
|
143
153
|
|
|
154
|
+
// 🎯 Defensive HTML check
|
|
155
|
+
if (res.headers.get("content-type")?.includes("text/html")) {
|
|
156
|
+
console.error(`❌ [CMS Client] API returned HTML instead of JSON for Tree: ${requestUrl}`);
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
const data = await res.json();
|
|
145
161
|
return data.items || [];
|
|
146
162
|
} catch (err) {
|
|
@@ -159,22 +175,28 @@ export async function getNodeByPath(
|
|
|
159
175
|
preview = false,
|
|
160
176
|
customFetch: typeof fetch = fetch
|
|
161
177
|
): Promise<NodeResponse | null> {
|
|
162
|
-
|
|
178
|
+
// 🎯 We keep 'index' as the fallback string for Hono routing compatibility,
|
|
179
|
+
// because the backend dynamically intercepts 'index' and returns the channelRoot!
|
|
180
|
+
const cleanPath = path.replace(/^\/+|\/+$/g, '') || 'index';
|
|
163
181
|
const baseUrl = getCmsUrl();
|
|
164
182
|
const requestUrl = `${baseUrl}/api/v1/node/${cleanPath}?channelId=${channel}&preview=${preview}`;
|
|
165
183
|
|
|
166
184
|
try {
|
|
167
185
|
const res = await customFetch(requestUrl, {
|
|
168
|
-
headers:
|
|
169
|
-
Accept: 'application/json',
|
|
170
|
-
},
|
|
186
|
+
headers: getAuthHeaders(),
|
|
171
187
|
});
|
|
172
188
|
|
|
173
189
|
if (!res.ok) return null;
|
|
174
190
|
|
|
191
|
+
// 🎯 Defensive HTML check (Prevents the dreaded "<!DOCTYPE html" JSON parse crash)
|
|
192
|
+
if (res.headers.get("content-type")?.includes("text/html")) {
|
|
193
|
+
console.error(`❌ [CMS Client] API returned HTML instead of JSON for Node: ${requestUrl}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
return (await res.json()) as NodeResponse;
|
|
176
198
|
} catch (err) {
|
|
177
|
-
console.error(
|
|
199
|
+
console.error(`[CMS Client] Failed to fetch node by path (${cleanPath}):`, err);
|
|
178
200
|
return null;
|
|
179
201
|
}
|
|
180
202
|
}
|
|
@@ -28,10 +28,21 @@ const kamaImageService: LocalImageService = {
|
|
|
28
28
|
|
|
29
29
|
const url = new URL(src, 'http://localhost');
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
if (options.
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// 🎯 Pass width if defined (snapped on backend by assets.ts)
|
|
32
|
+
if (options.width && !isNaN(options.width)) {
|
|
33
|
+
url.searchParams.set('w', Math.round(options.width).toString());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 🎯 Pass height only if explicitly provided to maintain intrinsic aspect ratios
|
|
37
|
+
if (options.height && !isNaN(options.height)) {
|
|
38
|
+
url.searchParams.set('h', Math.round(options.height).toString());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 🎯 Default quality to 75 if unsupplied
|
|
42
|
+
url.searchParams.set('q', (options.quality || 75).toString());
|
|
43
|
+
|
|
44
|
+
// 🎯 Default format to 'auto' for backend WebP/AVIF negotiation
|
|
45
|
+
url.searchParams.set('f', options.format ? options.format.toString() : 'auto');
|
|
35
46
|
|
|
36
47
|
return src.startsWith('http') ? url.toString() : (url.pathname + url.search);
|
|
37
48
|
} catch (e) {
|
|
@@ -28,12 +28,31 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
28
28
|
// 🎯 Detect if Astro is requesting a resized variant
|
|
29
29
|
const hasResizeParams = url.searchParams.has('w') || url.searchParams.has('h') || url.searchParams.has('q') || url.searchParams.has('f');
|
|
30
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
|
+
|
|
31
50
|
// 1. Cloudflare Edge Cache API
|
|
32
51
|
let edgeCache: Cache | null = null;
|
|
33
52
|
try {
|
|
34
53
|
if (typeof caches !== "undefined" && caches.default) {
|
|
35
54
|
edgeCache = caches.default;
|
|
36
|
-
const cachedResponse = await edgeCache.match(
|
|
55
|
+
const cachedResponse = await edgeCache.match(cacheKey);
|
|
37
56
|
if (cachedResponse) return cachedResponse;
|
|
38
57
|
}
|
|
39
58
|
} catch (err) {
|
|
@@ -67,7 +86,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
67
86
|
const r2Response = new Response(r2Object.body, { status: 200, headers });
|
|
68
87
|
|
|
69
88
|
if (edgeCache) {
|
|
70
|
-
const cachePromise = edgeCache.put(
|
|
89
|
+
const cachePromise = edgeCache.put(cacheKey, r2Response.clone());
|
|
71
90
|
if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
|
|
72
91
|
else cachePromise.catch(e => console.warn(e));
|
|
73
92
|
}
|
|
@@ -112,7 +131,7 @@ export async function handleMediaProxy({ request, params, url, locals }: APICont
|
|
|
112
131
|
});
|
|
113
132
|
|
|
114
133
|
if (edgeCache) {
|
|
115
|
-
const cachePromise = edgeCache.put(
|
|
134
|
+
const cachePromise = edgeCache.put(cacheKey, proxyResponse.clone());
|
|
116
135
|
if (cfContext?.waitUntil) cfContext.waitUntil(cachePromise);
|
|
117
136
|
else cachePromise.catch(e => console.warn(e));
|
|
118
137
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
// src/lib/media/media-url.ts
|
|
2
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
|
+
*/
|
|
3
7
|
export function toRelativeMediaPath(rawUrl: string): string {
|
|
4
8
|
if (!rawUrl) return "";
|
|
5
9
|
|
|
6
|
-
let clean = String(rawUrl);
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|
|
9
20
|
|
|
10
21
|
const mediaIndex = clean.indexOf("/media/");
|
|
11
22
|
if (mediaIndex !== -1) {
|
|
@@ -26,6 +37,9 @@ export function isSvgImage(urlOrMime: string): boolean {
|
|
|
26
37
|
return lower.endsWith('.svg') || lower.includes('image/svg+xml') || lower.includes('.svg?');
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Constructs an agnostic /media/... URL with standard optimization query parameters.
|
|
42
|
+
*/
|
|
29
43
|
export function buildAgnosticImageUrl(
|
|
30
44
|
url: string,
|
|
31
45
|
width: number,
|