PageSourceSearch

https://www.vitamind.net/feature-loader.js

js vitamind.net collected 2026-09-26 02:57:54 UTC 7,235 bytes, 224 lines download raw bytes

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.