2 * dbxapp seoAdmin.js — SEO settings per content page
4(function (window, document) {
7 if (!window.dbx || !window.dbx.feature) {
8 console.error("[dbx][seoAdmin] dbx core missing");
12 const dbx = window.dbx;
13 const LIB = "seoAdmin";
15 function text(root, key, fallback) {
16 if (!root.__dbxSeoMessages) {
18 root.__dbxSeoMessages = JSON.parse(root.getAttribute("data-seo-messages") || "{}");
20 root.__dbxSeoMessages = {};
23 const value = root.__dbxSeoMessages[key];
24 return typeof value === "string" && value !== "" ? value : fallback;
27 function qs(root, sel) {
28 return root ? root.querySelector(sel) : null;
31 function setStatus(root, msg, type) {
32 const el = qs(root, "[data-seo-status]");
34 el.textContent = msg || "";
35 el.className = "dbx-seo-status small" + (type === "error" ? " text-danger" : type === "success" ? " text-success" : " text-muted");
38 function apiUrl(url, params) {
39 const out = new URL(String(url || window.location.href), window.location.href);
40 Object.keys(params || {}).forEach(key => out.searchParams.set(key, params[key]));
41 out.searchParams.set("_", Date.now());
42 return out.toString();
45 function fetchJson(url, opt) {
47 return dbx.ajax.request({
49 method: opt.method || "GET",
51 body: (typeof opt.body !== "undefined") ? opt.body : null,
52 headers: opt.headers || {},
57 function getField(root, name) {
58 const el = root.querySelector('[data-cms-field="' + name + '"]');
63 function setField(root, name, value) {
64 const el = root.querySelector('[data-cms-field="' + name + '"]');
66 el.value = value == null ? "" : String(value);
69 function state(root) {
70 if (!root.__dbxSeoState) {
71 root.__dbxSeoState = { previewRow: null, loading: false };
73 return root.__dbxSeoState;
76 function renderPreview(root) {
77 const preview = qs(root, "[data-seo-preview]");
79 const id = Number(getField(root, "seo_image_id") || 0);
80 const s = state(root);
81 const row = (s.previewRow && Number(s.previewRow.id || 0) === id) ? s.previewRow : null;
83 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_empty", "No OG image selected.") + '</div>';
87 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_loading", "Loading OG image.") + '</div>';
90 const url = row.preview_url || row.thumb_url || "";
91 if (!url || String(row.mime || "").indexOf("image/") !== 0 && row.media_type !== "image") {
92 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_not_image", "The selected medium is not an image.") + '</div>';
95 const title = row.title || "OG-Bild";
96 preview.innerHTML = '<img src="' + url + '" alt="' + title.replace(/"/g, "") + '" class="img-fluid rounded border">';
99 function loadPage(root, cfg, cid) {
100 const pageId = Number(cid || 0);
101 if (!cfg.page || pageId <= 0) return Promise.resolve();
102 const s = state(root);
103 if (s.loading) return Promise.resolve();
105 setStatus(root, text(root, "load_start", "Loading SEO data..."), "info");
107 return fetchJson(apiUrl(cfg.page, { id: pageId }))
110 if (!data || !data.ok) throw new Error("load failed");
111 const row = data.row || {};
112 setField(root, "id", row.id || pageId);
113 setField(root, "keywords", row.keywords || "");
114 setField(root, "meta_robots", row.meta_robots || "index,follow");
115 setField(root, "seo_title", row.seo_title || "");
116 setField(root, "seo_image_id", row.seo_image_id || 0);
117 s.previewRow = data.seo_preview_media || null;
119 setStatus(root, "", "info");
123 dbx.error("[seoAdmin] load failed", err);
124 setStatus(root, text(root, "load_error", "SEO data could not be loaded."), "error");
128 function savePage(root, cfg) {
129 if (!cfg.save) return Promise.resolve();
131 id: Number(getField(root, "id") || 0),
132 keywords: getField(root, "keywords"),
133 meta_robots: getField(root, "meta_robots") || "index,follow",
134 seo_title: getField(root, "seo_title"),
135 seo_image_id: Number(getField(root, "seo_image_id") || 0)
137 if (payload.id <= 0) {
138 setStatus(root, text(root, "no_page_selected", "No page selected."), "error");
139 return Promise.resolve();
142 setStatus(root, text(root, "save_start", "Saving SEO data..."), "info");
143 return fetchJson(cfg.save, {
145 headers: { "Content-Type": "application/json" },
146 body: JSON.stringify(payload)
148 if (!data || !data.ok) throw new Error("save failed");
149 state(root).previewRow = data.seo_preview_media || null;
151 setStatus(root, text(root, "seo_saved", "SEO saved."), "success");
153 dbx.error("[seoAdmin] save failed", err);
154 setStatus(root, text(root, "save_error", "Data could not be saved."), "error");
158 function openSimpleMediaPicker(root, cfg, onPick) {
159 if (!cfg.media) return Promise.resolve();
160 return fetchJson(apiUrl(cfg.media, { images: 1, sync: 1, media_type: "image" }))
162 const rows = (data && data.rows) ? data.rows : [];
163 const images = rows.filter(row => row && row.id && (row.media_type === "image" || String(row.mime || "").indexOf("image/") === 0));
164 if (!images.length) {
165 setStatus(root, text(root, "no_media_images", "There are no images in the media library."), "error");
168 let modal = document.getElementById("dbxSeoMediaPicker");
170 modal = document.createElement("div");
171 modal.id = "dbxSeoMediaPicker";
172 modal.className = "dbx-seo-media-picker modal fade";
173 modal.innerHTML = '<div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">' + text(root, "picker_title", "Select OG image") + '</h5><button type="button" class="btn-close" data-seo-media-close></button></div><div class="modal-body"><div class="row g-2" data-seo-media-grid></div></div></div></div>';
174 document.body.appendChild(modal);
176 const grid = modal.querySelector("[data-seo-media-grid]");
177 grid.innerHTML = images.map(row => {
178 const url = row.preview_url || row.thumb_url || ("index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=" + row.id);
179 const title = String(row.title || row.file_name || ("#" + row.id)).replace(/"/g, "");
180 return '<div class="col-6 col-md-4 col-lg-3"><button type="button" class="btn btn-light border w-100 p-2 text-start" data-seo-media-pick="' + row.id + '"><img src="' + url + '" alt="" class="img-fluid rounded mb-2"><span class="small d-block text-truncate">' + title + "</span></button></div>";
182 const close = () => {
183 modal.classList.remove("show");
184 modal.style.display = "none";
185 document.body.classList.remove("modal-open");
187 modal.querySelector("[data-seo-media-close]").onclick = close;
188 modal.onclick = e => { if (e.target === modal) close(); };
189 grid.onclick = e => {
190 const btn = e.target.closest("[data-seo-media-pick]");
192 const id = Number(btn.getAttribute("data-seo-media-pick") || 0);
193 const row = images.find(item => Number(item.id) === id);
194 if (row && typeof onPick === "function") onPick(row);
197 modal.classList.add("show");
198 modal.style.display = "block";
199 document.body.classList.add("modal-open");
203 function ensureCmsMediaBrowser() {
204 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
205 return Promise.resolve(true);
207 return new Promise(function (resolve) {
213 ["js", "lib", "ajax.js"],
214 ["js", "lib", "openWin.js"],
215 ["js", "lib", "cms.js"]
217 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
222 function bind(root, cfg) {
223 const select = qs(root, "[data-seo-page-select]");
225 select.addEventListener("change", () => {
226 const cid = Number(select.value || 0);
227 if (cid > 0) loadPage(root, cfg, cid);
231 root.addEventListener("click", e => {
232 const btn = e.target.closest("[data-seo-action]");
233 if (!btn || !root.contains(btn)) return;
234 const action = btn.getAttribute("data-seo-action");
236 if (action === "save") {
242 if (action === "assign-media") {
244 ensureCmsMediaBrowser().then(function (ok) {
245 if (!ok || !dbx.cmsMediaBrowser) {
247 setStatus(root, text(root, "media_api_missing", "The media API is not configured."), "error");
250 openSimpleMediaPicker(root, cfg, row => {
251 state(root).previewRow = row;
252 setField(root, "seo_image_id", row.id);
254 setStatus(root, text(root, "og_selected_pending", "OG image selected (save still required)."), "success");
256 dbx.error("[seoAdmin] media list failed", err);
257 setStatus(root, text(root, "media_load_error", "Media could not be loaded."), "error");
261 dbx.cmsMediaBrowser.open(root, cfg, {
265 if (!row || !row.id) return;
266 state(root).previewRow = row;
267 setField(root, "seo_image_id", row.id);
269 setStatus(root, text(root, "og_selected_pending", "OG image selected (save still required)."), "success");
276 if (action === "clear-media") {
278 state(root).previewRow = null;
279 setField(root, "seo_image_id", 0);
281 setStatus(root, text(root, "og_removed_pending", "OG image removed (save still required)."), "info");
290 ["css", "design", "c-cms.css"]
293 ["js", "lib", "ajax.js"]
297 if (!el || el.__dbxSeoReady) return;
298 el.__dbxSeoReady = true;
301 const select = qs(el, "[data-seo-page-select]");
302 const cid = Number((select && select.value) || cfg.cid || 0);
303 if (cid > 0) loadPage(el, cfg, cid);
307 (root || document).querySelectorAll(".dbx-seo-admin[data-dbx]").forEach(el => {
308 if (el.__dbxSeoReady) return;
309 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
310 const cfg = cfgList.find(item => item.lib === LIB) || {};
316 dbx.feature.register(LIB, seoFeature);