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 qs(root, sel) {
16 return root ? root.querySelector(sel) : null;
19 function setStatus(root, msg, type) {
20 const el = qs(root, "[data-seo-status]");
22 el.textContent = msg || "";
23 el.className = "dbx-seo-status small" + (type === "error" ? " text-danger" : type === "success" ? " text-success" : " text-muted");
26 function apiUrl(url, params) {
27 const out = new URL(String(url || window.location.href), window.location.href);
28 Object.keys(params || {}).forEach(key => out.searchParams.set(key, params[key]));
29 out.searchParams.set("_", Date.now());
30 return out.toString();
33 function fetchJson(url, opt) {
35 return dbx.ajax.request({
37 method: opt.method || "GET",
39 body: (typeof opt.body !== "undefined") ? opt.body : null,
40 headers: opt.headers || {},
45 function getField(root, name) {
46 const el = root.querySelector('[data-cms-field="' + name + '"]');
51 function setField(root, name, value) {
52 const el = root.querySelector('[data-cms-field="' + name + '"]');
54 el.value = value == null ? "" : String(value);
57 function state(root) {
58 if (!root.__dbxSeoState) {
59 root.__dbxSeoState = { previewRow: null, loading: false };
61 return root.__dbxSeoState;
64 function renderPreview(root) {
65 const preview = qs(root, "[data-seo-preview]");
67 const id = Number(getField(root, "seo_image_id") || 0);
68 const s = state(root);
69 const row = (s.previewRow && Number(s.previewRow.id || 0) === id) ? s.previewRow : null;
71 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">Kein OG-Bild ausgewaehlt (Hero-Fallback).</div>';
75 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">OG-Bild wird geladen.</div>';
78 const url = row.preview_url || row.thumb_url || "";
79 if (!url || String(row.mime || "").indexOf("image/") !== 0 && row.media_type !== "image") {
80 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">Das gewaehlte Medium ist kein Bild.</div>';
83 const title = row.title || "OG-Bild";
84 preview.innerHTML = '<img src="' + url + '" alt="' + title.replace(/"/g, "") + '" class="img-fluid rounded border">';
87 function loadPage(root, cfg, cid) {
88 const pageId = Number(cid || 0);
89 if (!cfg.page || pageId <= 0) return Promise.resolve();
90 const s = state(root);
91 if (s.loading) return Promise.resolve();
93 setStatus(root, "Lade SEO-Daten...", "info");
95 return fetchJson(apiUrl(cfg.page, { id: pageId }))
98 if (!data || !data.ok) throw new Error("load failed");
99 const row = data.row || {};
100 setField(root, "id", row.id || pageId);
101 setField(root, "keywords", row.keywords || "");
102 setField(root, "meta_robots", row.meta_robots || "index,follow");
103 setField(root, "seo_title", row.seo_title || "");
104 setField(root, "seo_image_id", row.seo_image_id || 0);
105 s.previewRow = data.seo_preview_media || null;
107 setStatus(root, "", "info");
111 dbx.error("[seoAdmin] load failed", err);
112 setStatus(root, "SEO-Daten konnten nicht geladen werden.", "error");
116 function savePage(root, cfg) {
117 if (!cfg.save) return Promise.resolve();
119 id: Number(getField(root, "id") || 0),
120 keywords: getField(root, "keywords"),
121 meta_robots: getField(root, "meta_robots") || "index,follow",
122 seo_title: getField(root, "seo_title"),
123 seo_image_id: Number(getField(root, "seo_image_id") || 0)
125 if (payload.id <= 0) {
126 setStatus(root, "Keine Seite gewaehlt.", "error");
127 return Promise.resolve();
130 setStatus(root, "Speichere...", "info");
131 return fetchJson(cfg.save, {
133 headers: { "Content-Type": "application/json" },
134 body: JSON.stringify(payload)
136 if (!data || !data.ok) throw new Error("save failed");
137 state(root).previewRow = data.seo_preview_media || null;
139 setStatus(root, "SEO gespeichert.", "success");
141 dbx.error("[seoAdmin] save failed", err);
142 setStatus(root, "Speichern fehlgeschlagen.", "error");
146 function openSimpleMediaPicker(root, cfg, onPick) {
147 if (!cfg.media) return Promise.resolve();
148 return fetchJson(apiUrl(cfg.media, { images: 1, sync: 1, media_type: "image" }))
150 const rows = (data && data.rows) ? data.rows : [];
151 const images = rows.filter(row => row && row.id && (row.media_type === "image" || String(row.mime || "").indexOf("image/") === 0));
152 if (!images.length) {
153 setStatus(root, "Keine Bilder im Medienbestand.", "error");
156 let modal = document.getElementById("dbxSeoMediaPicker");
158 modal = document.createElement("div");
159 modal.id = "dbxSeoMediaPicker";
160 modal.className = "dbx-seo-media-picker modal fade";
161 modal.innerHTML = '<div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">OG-Bild waehlen</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>';
162 document.body.appendChild(modal);
164 const grid = modal.querySelector("[data-seo-media-grid]");
165 grid.innerHTML = images.map(row => {
166 const url = row.preview_url || row.thumb_url || ("index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=" + row.id);
167 const title = String(row.title || row.file_name || ("#" + row.id)).replace(/"/g, "");
168 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>";
170 const close = () => {
171 modal.classList.remove("show");
172 modal.style.display = "none";
173 document.body.classList.remove("modal-open");
175 modal.querySelector("[data-seo-media-close]").onclick = close;
176 modal.onclick = e => { if (e.target === modal) close(); };
177 grid.onclick = e => {
178 const btn = e.target.closest("[data-seo-media-pick]");
180 const id = Number(btn.getAttribute("data-seo-media-pick") || 0);
181 const row = images.find(item => Number(item.id) === id);
182 if (row && typeof onPick === "function") onPick(row);
185 modal.classList.add("show");
186 modal.style.display = "block";
187 document.body.classList.add("modal-open");
191 function ensureCmsMediaBrowser() {
192 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
193 return Promise.resolve(true);
195 return new Promise(function (resolve) {
201 ["js", "lib", "ajax.js"],
202 ["js", "lib", "openWin.js"],
203 ["js", "lib", "cms.js"]
205 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
210 function bind(root, cfg) {
211 const select = qs(root, "[data-seo-page-select]");
213 select.addEventListener("change", () => {
214 const cid = Number(select.value || 0);
215 if (cid > 0) loadPage(root, cfg, cid);
219 root.addEventListener("click", e => {
220 const btn = e.target.closest("[data-seo-action]");
221 if (!btn || !root.contains(btn)) return;
222 const action = btn.getAttribute("data-seo-action");
224 if (action === "save") {
230 if (action === "assign-media") {
232 ensureCmsMediaBrowser().then(function (ok) {
233 if (!ok || !dbx.cmsMediaBrowser) {
235 setStatus(root, "Medien-API nicht konfiguriert.", "error");
238 openSimpleMediaPicker(root, cfg, row => {
239 state(root).previewRow = row;
240 setField(root, "seo_image_id", row.id);
242 setStatus(root, "OG-Bild ausgewaehlt (noch speichern).", "success");
244 dbx.error("[seoAdmin] media list failed", err);
245 setStatus(root, "Medien konnten nicht geladen werden.", "error");
249 dbx.cmsMediaBrowser.open(root, cfg, {
253 if (!row || !row.id) return;
254 state(root).previewRow = row;
255 setField(root, "seo_image_id", row.id);
257 setStatus(root, "OG-Bild ausgewaehlt (noch speichern).", "success");
264 if (action === "clear-media") {
266 state(root).previewRow = null;
267 setField(root, "seo_image_id", 0);
269 setStatus(root, "OG-Bild entfernt (noch speichern).", "info");
278 ["css", "design", "c-cms.css"]
281 ["js", "lib", "ajax.js"]
285 if (!el || el.__dbxSeoReady) return;
286 el.__dbxSeoReady = true;
289 const select = qs(el, "[data-seo-page-select]");
290 const cid = Number((select && select.value) || cfg.cid || 0);
291 if (cid > 0) loadPage(el, cfg, cid);
295 (root || document).querySelectorAll(".dbx-seo-admin[data-dbx]").forEach(el => {
296 if (el.__dbxSeoReady) return;
297 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
298 const cfg = cfgList.find(item => item.lib === LIB) || {};
304 dbx.feature.register(LIB, seoFeature);