1(function () { 2 const DEFAULT_MOUNT_ID = 'featureSpot'; 3 const STYLE_ID = 'featureSpot-styles'; 4 const UNIT_CLASS = 'featureUnit'; 5 const LINK_CLASS = 'featureUnit-link'; 6 const IMG_CLASS = 'featureUnit-img'; 7 const SIDEBAR_SELECTOR = '.sidebar'; 8 9 /** 10 * Public API 11 * @param {Object} options 12 * @param {string} options.jsonUrl 13 * @param {string} [options.mountId] // optional: fester Sidebar-Slot 14 */ 15 async function initFeature(options) { 16 if (!options || !options.jsonUrl) { 17 console.error('[feature-loader] Missing jsonUrl option.'); 18 return; 19 } 20 injectStyles(); 21 22 // JSON laden 23 let list = []; 24 try { 25 const res = await fetch(options.jsonUrl, { cache: 'no-store' }); 26 if (!res.ok) throw new Error(`HTTP ${res.status}`); 27 const data = await res.json(); 28 list = Array.isArray(data?.items) ? data.items : []; 29 } catch (e) { 30 console.error('[feature-loader] Failed to load JSON:', e); 31 return; 32 } 33 if (!list.length) { 34 console.warn('[feature-loader] No items in JSON.'); 35 return; 36 } 37 38 // ---- SIDEBAR: exakt 1x (ganz oben) ---- 39 const mountId = options.mountId || DEFAULT_MOUNT_ID; 40 const fixedMount = document.getElementById(mountId); 41 42 // Baue Pool für Sidebar 43 let usedIndex = null; 44 const sidebarPool = buildPool(list, 'sidebar'); 45 if (fixedMount) { 46 const pickSB = pickFromPool(sidebarPool); 47 if (pickSB) { 48 renderIntoMount(fixedMount, pickSB.item); 49 usedIndex = pickSB.index; 50 } 51 } else { 52 const sidebar = document.querySelector(SIDEBAR_SELECTOR); 53 if (sidebar) { 54 const pickSB = pickFromPool(sidebarPool); 55 if (pickSB) { 56 sidebar.insertBefore(buildUnit(pickSB.item), sidebar.firstChild); 57 usedIndex = pickSB.index; 58 } 59 } else { 60 console.warn(`[feature-loader] Sidebar "${SIDEBAR_SELECTOR}" not found.`); 61 } 62 } 63 64 // ---- CONTENT: exakt 1x nach dem 3. Absatz (nur auf Page/Single) ---- 65 if (isPageOrSingle() && !isExcludedListing()) { 66 const contentRoot = getContentContainer(); 67 if (contentRoot) { 68 const paragraphs = Array.from(contentRoot.querySelectorAll('p')); 69 if (paragraphs.length >= 3) { 70 const after = paragraphs[2]; 71 72 // Pool für Content, möglichst ohne das bereits verwendete Sidebar-Item 73 let contentPool = buildPool(list, 'content', usedIndex); 74 // Falls der Ausschluss den Pool leert, ohne Ausschluss neu bilden 75 if (!contentPool.length) contentPool = buildPool(list, 'content', null); 76 77 const pickCT = pickFromPool(contentPool); 78 if (pickCT) insertAfter(buildUnit(pickCT.item), after); 79 } 80 } 81 } 82 } 83 84 // ---- Hilfsfunktionen: Zonen-Handling & Pooling ---- 85 function getZone(item) { 86 // nur "content" oder "sidebar" gültig; Rest => "both" 87 const z = (item && typeof item.zone === 'string') ? item.zone.toLowerCase().trim() : ''; 88 if (z === 'content' || z === 'sidebar') return z; 89 return 'both'; 90 } 91 92 /** 93 * Erstellt einen Pool für die gewünschte Zone ("sidebar" oder "content"). 94 * Items ohne gültige zone gelten als "both" und werden aufgenommen. 95 * @param {Array} list 96 * @param {('sidebar'|'content')} targetZone 97 * @param {number|null} excludeIndex // optional: Index ausschlieÃen (Duplikatsvermeidung) 98 */ 99 function buildPool(list, targetZone, excludeIndex = null) { 100 const pool = []; 101 for (let i = 0; i < list.length; i++) { 102 if (excludeIndex !== null && i === excludeIndex) continue; 103 const zone = getZone(list[i]); 104 if (zone === 'both' || zone === targetZone) { 105 // Item muss minimal gültig sein 106 if (list[i] && list[i].href && list[i].src) { 107 pool.push({ item: list[i], index: i }); 108 } 109 } 110 } 111 // Fallback: wenn gar nichts in der Zone passt, nimm alle gültigen Items (mit Ausschluss) 112 if (!pool.length) { 113 for (let i = 0; i < list.length; i++) { 114 if (excludeIndex !== null && i === excludeIndex) continue; 115 if (list[i] && list[i].href && list[i].src) { 116 pool.push({ item: list[i], index: i }); 117 } 118 } 119 } 120 return pool; 121 } 122 123 function pickFromPool(pool) { 124 if (!pool || !pool.length) return null; 125 const r = Math.floor(Math.random() * pool.length); 126 return pool[r]; 127 } 128 129 /** Einblendung in konkreten Mount */ 130 function renderIntoMount(mount, item) { 131 if (!mount || !item || !item.href || !item.src) return null; 132 mount.innerHTML = ''; 133 mount.appendChild(buildUnit(item)); 134 return true; 135 } 136 137 /** DOM-Baustein */ 138 function buildUnit(item) { 139 const wrap = document.createElement('div'); 140 wrap.className = UNIT_CLASS; 141 142 const a = document.createElement('a'); 143 a.className = LINK_CLASS; 144 a.href = item.href; 145 a.target = '_blank'; 146 a.rel = 'noopener noreferrer'; 147 148 const img = document.createElement('img'); 149 img.className = IMG_CLASS; 150 img.src = item.src; 151 img.alt = item.alt || ''; 152 img.decoding = 'async'; 153 img.loading = 'lazy'; 154 if (Number.isFinite(item.w)) img.width = Number(item.w);
155 if (Number.isFinite(item.h)) img.height = Number(item.h); 156 157 a.appendChild(img); 158 wrap.appendChild(a); 159 return wrap; 160 } 161 162 /** Nach Knoten einfügen */ 163 function insertAfter(node, reference) { 164 if (!reference || !reference.parentNode) return; 165 if (reference.nextSibling) reference.parentNode.insertBefore(node, reference.nextSibling); 166 else reference.parentNode.appendChild(node); 167 } 168 169 /** WP-Kontext prüfen */ 170 function isPageOrSingle() { 171 const b = document.body; 172 return b.classList.contains('page') || b.classList.contains('single'); 173 } 174 function isExcludedListing() { 175 const b = document.body; 176 return ( 177 b.classList.contains('home') || 178 b.classList.contains('blog') || 179 b.classList.contains('archive') || 180 b.classList.contains('search') || 181 b.classList.contains('category') || 182 b.classList.contains('tag') || 183 b.classList.contains('author') 184 ); 185 } 186 187 /** Content-Container heuristisch bestimmen */ 188 function getContentContainer() { 189 const selectors = [ 190 '.entry-content', 191 'article .entry-content', 192 '.post-content', 193 '.content .entry-content', 194 '.singlepost .entry-content', 195 '.content', 196 'article' 197 ]; 198 for (const sel of selectors) { 199 const el = document.querySelector(sel); 200 if (el) return el; 201 } 202 return null; 203 } 204 205 /** Styles injizieren */ 206 function injectStyles() { 207 if (document.getElementById(STYLE_ID)) return; 208 const css = ` 209 #${DEFAULT_MOUNT_ID} { display:block; max-width:100%; width:100%; } 210 211 .${UNIT_CLASS} { display:block; width:100%; max-width:100%; margin:0 0 16px 0; } 212 .${LINK_CLASS} { display:block; text-decoration:none; line-height:0; } 213 .${IMG_CLASS} { display:block; max-width:100%; height:auto; border:0; } 214 `; 215 const style = document.createElement('style'); 216 style.id = STYLE_ID; 217 style.type = 'text/css'; 218 style.appendChild(document.createTextNode(css)); 219 document.head.appendChild(style); 220 } 221 222 // Expose 223 window.initFeature = initFeature; 224})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.