PageSourceSearch

https://www.importweine.de/theme/3cb9b05ff3396c400f2464d251fa0734/…/dne-custom-css-js.js?1791483956

js importweine.de collected 2026-10-09 00:28:58 UTC 23,188 bytes, 615 lines download raw bytes

1
2(function () {
3  "use strict";
4
5  var AKZENT = "#850000";
6
7  function bereit(fn) {
8    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn);
9    else fn();
10  }
11
12  /** Einmal pro Seitenaufruf. Schützt vor doppelter Einbindung des Snippets. */
13  function schonGelaufen() {
14    if (document.documentElement.hasAttribute("data-faq-aktiviert")) return true;
15    document.documentElement.setAttribute("data-faq-aktiviert", "1");
16    return false;
17  }
18
19  function stilEinmalig() {
20    if (document.getElementById("faq-aktivator-stil")) return;
21    var css = document.createElement("style");
22    css.id = "faq-aktivator-stil";
23    css.textContent =
24      ".faq-akkordeon{margin:0 0 .75rem;padding:.75rem 1rem;background:#fff;" +
25      "border:1px solid #f0ede8;border-radius:6px}" +
26      ".faq-akkordeon>summary{font-weight:600;cursor:pointer;color:#333;font-size:1.05rem;" +
27      "line-height:1.4;list-style:none}" +
28      ".faq-akkordeon>summary::-webkit-details-marker{display:none}" +
29      ".faq-akkordeon>summary::after{content:'+';float:right;color:" + AKZENT + ";font-weight:400}" +
30      ".faq-akkordeon[open]>summary::after{content:'\\2013'}" +
31      ".faq-akkordeon>p{margin:.75rem 0 0;color:#555;line-height:1.7;font-size:.95rem}" +
32      ".faq-rohdaten{display:none}";
33    document.head.appendChild(css);
34  }
35
36  /** Ein Frage/Antwort-Paar als <details>. Texte über textContent, nie über innerHTML. */
37  function akkordeon(frage, antwort) {
38    var d = document.createElement("details");
39    d.className = "faq-akkordeon";
40    var s = document.createElement("summary");
41    s.textContent = frage;
42    var p = document.createElement("p");
43    p.textContent = antwort;
44    d.appendChild(s);
45    d.appendChild(p);
46    return d;
47  }
48
49  /**
50   * Der Normalfall: eine fertige FAQ im HTML, die nur noch aufklappbar werden soll.
51   * Die Überschrift bleibt stehen — sie kommt aus dem Artikel und gehört nicht uns.
52   */
53  function ausMarkierung(sammlung) {
54    var behaelter = document.querySelectorAll(".cockpit-faq");
55    for (var i = 0; i < behaelter.length; i++) {
56      var box = behaelter[i];
57      if (box.getAttribute("data-faq-umgebaut")) continue;
58      var posten = box.querySelectorAll(".cockpit-faq-item");
59      if (!posten.length) continue;
60
61      for (var j = 0; j < posten.length; j++) {
62        var frageEl = posten[j].querySelector(".cockpit-faq-q");
63        var antwortEl = posten[j].querySelector(".cockpit-faq-a");
64        if (!frageEl || !antwortEl) continue;
65        var frage = frageEl.textContent.trim();
66        var antwort = antwortEl.textContent.trim();
67        if (!frage || !antwort) continue;
68
69        posten[j].parentNode.replaceChild(akkordeon(frage, antwort), posten[j]);
70        sammlung.push({ frage: frage, antwort: antwort });
71      }
72      box.setAttribute("data-faq-umgebaut", "1");
73    }
74  }
75
76  /**
77   * Altbestand: Produktseiten, deren FAQ als `Frage|||Antwort---Frage|||Antwort` aus
78   * einem Custom Field in einen Textblock gemappt wird. Bleibt drin, bis die Produkte
79   * auf die markierte Form umgestellt sind — vorher würde die FAQ dort verschwinden.
80   */
81  function ausRohform(sammlung) {
82    var bloecke = document.querySelectorAll(".cms-element-text");
83    for (var i = 0; i < bloecke.length; i++) {
84      var block = bloecke[i];
85      if (block.getAttribute("data-faq-umgebaut")) continue;
86      if (block.querySelector(".cockpit-faq")) continue; // gehört dem Normalfall
87      var text = block.textContent.trim();
88      if (text.indexOf("|||") === -1) continue;
89
90      var paare = text.split("---");
91      var gefunden = [];
92      for (var j = 0; j < paare.length; j++) {
93        var teile = paare[j].trim().split("|||");
94        if (teile.length !== 2) continue;
95        var frage = teile[0].trim();
96        var antwort = teile[1].trim();
97        if (frage && antwort) gefunden.push({ frage: frage, antwort: antwort });
98      }
99      if (!gefunden.length) continue;
100
101      // Rohtext erst leeren, wenn feststeht, dass Ersatz da ist.
102      block.textContent = "";
103      var huelle = document.createElement("div");
104      huelle.className = "wine-faq-section";
105      huelle.style.marginTop = "2rem";
106      huelle.style.paddingTop = "2rem";
107      huelle.style.borderTop = "2px solid " + AKZENT;
108
109      var h = document.createElement("h2");
110      h.textContent = "Häufig gestellte Fragen";
111      h.style.color = AKZENT;
112      h.style.fontSize = "1.5rem";
113      h.style.marginBottom = "1rem";
114      huelle.appendChild(h);
115
116      for (var k = 0; k < gefunden.length; k++) {
117        huelle.appendChild(akkordeon(gefunden[k].frage, gefunden[k].antwort));
118        sammlung.push(gefunden[k]);
119      }
120      block.appendChild(huelle);
121      block.setAttribute("data-faq-umgebaut", "1");
122    }
123  }
124
125  /**
126   * Ein einziges FAQPage-Schema je Seite, aus allen gefundenen Paaren.
127   *
128   * Hinweis für später: Google rendert JavaScript und liest das hier mit. Viele
129   * KI-Crawler tun das nicht — die sehen nur die FAQ im HTML, das Schema nicht.
130   * Serverseitig wäre besser, geht aber erst mit Zugriff auf das Theme.
131   */
132  function schema(sammlung) {
133    if (!sammlung.length) return;
134    if (document.getElementById("faq-schema")) return;
135    var daten = {
136      "@context": "https://schema.org",
137      "@type": "FAQPage",
138      mainEntity: sammlung.map(function (f) {
139        return {
140          "@type": "Question",
141          name: f.frage,
142          acceptedAnswer: { "@type": "Answer", text: f.antwort },
143        };
144      }),
145    };
146    var tag = document.createElement("script");
147    tag.id = "faq-schema";
148    tag.type = "application/ld+json";
149    tag.textContent = JSON.stringify(daten);
150    document.head.appendChild(tag);
151  }
152
153  bereit(function () {
154    if (schonGelaufen()) return;
155    var sammlung = [];
156    ausMarkierung(sammlung);
157    ausRohform(sammlung);
158    if (!sammlung.length) return;
159    stilEinmalig();
160    schema(sammlung);
161    if (window.console && console.debug) {
162      console.debug("[FAQ] " + sammlung.length + " Fragen aufklappbar gemacht, Schema gesetzt.");
163    }
164  });
165})();
166
167(function() {
168    'use strict';
169
170    const COOKIE_NAME = '__iw_click_id';
171    const COOKIE_DAYS = 90;
172    // Reihenfolge = Priorität (gclid > wbraid > gbraid > msclkid > fbclid)
173    const PASS_THROUGH_PARAMS = ['gclid', 'wbraid', 'gbraid', 'msclkid', 'fbclid'];
174
175    // ───── Teil 1: Cookie Capture (auf JEDER Seite) ─────
176    const urlParams = new URLSearchParams(window.location.search);
177    for (const param of PASS_THROUGH_PARAMS) {
178        const value = urlParams.get(param);
179        if (value) {
180            setCookie(COOKIE_NAME, value, COOKIE_DAYS);
181            break; // First match wins
182        }
183    }
184
185    // ───── Teil 2: Checkout Form Injection ─────
186    if (window.location.pathname.indexOf('/checkout/') === 0
187        || window.location.pathname === '/checkout') {
188        document.addEventListener('DOMContentLoaded', injectClickIdIntoCheckoutForm);
189    }
190
191    function injectClickIdIntoCheckoutForm() {
192        const clickId = getCookie(COOKIE_NAME);
193        if (!clickId) return;
194
195        // Shopware 6 Standard-Confirm-Form
196        const form = document.querySelector('form#confirmOrderForm')
197                  || document.querySelector('form[action*="checkout/finish"]')
198                  || document.querySelector('form[action*="checkout/order"]');
199        if (!form) {
200            console.warn('[click-id-tracker] Checkout form not found');
201            return;
202        }
203
204        // Verhindere Duplikate falls Script mehrfach läuft
205        if (form.querySelector('input[name="customFields[custom_gclid]"]')) return;
206
207        const input = document.createElement('input');
208        input.type = 'hidden';
209        input.name = 'customFields[custom_gclid]';
210        input.value = clickId;
211        form.appendChild(input);
212
213        console.log('[click-id-tracker] Injected click ID:', clickId.substring(0, 8) + '…');
214    }
215
216    // ───── Helpers ─────
217    function setCookie(name, value, days) {
218        const expires = new Date();
219        expires.setTime(expires.getTime() + days * 86400000);
220        document.cookie = name + '=' + encodeURIComponent(value)
221                       + '; expires=' + expires.toUTCString()
222                       + '; path=/; SameSite=Lax; Secure';
223    }
224
225    function getCookie(name) {
226        const match = document.cookie.match(new RegExp('(^|; )' + name + '=([^;]+)'));
227        return match ? decodeURIComponent(match[2]) : null;
228    }
229})();
230
231(function() {
232  'use strict';
233
234  // ─────────────────────────────────────────────────
235  // TEIL 1: Product + Offer Schema (Produktseiten)
236  // ─────────────────────────────────────────────────
237
238  function injectProductSchema() {
239    // Pruefen ob wir auf einer Produktseite sind
240    // Shopware 6.7 setzt body class "is-ctl-product" auf Produktseiten
241    var isProductPage = (
242      document.body.classList.contains('is-ctl-product') ||
243      document.querySelector('.product-detail-buy') ||
244      document.querySelector('.product-detail-name') ||
245      (window.controllerName === 'product')
246    );
247
248    if (!isProductPage) return;
249
250    // Pruefen ob Product JSON-LD bereits existiert (Duplikat vermeiden)
251    var existingScripts = document.querySelectorAll('script[type="application/ld+json"]');
252    for (var i = 0; i < existingScripts.length; i++) {
253      try {
254        var p = JSON.parse(existingScripts[i].textContent);
255        if (p['@type'] === 'Product') return;
256        if (p['@graph']) {
257          for (var k = 0; k < p['@graph'].length; k++) {
258            if (p['@graph'][k]['@type'] === 'Product') return;
259          }
260        }
261      } catch(e) {}
262    }
263
264    // --- Daten extrahieren ---
265
266    // Name: OG Title > h1.product-detail-name > document.title
267    var name = getMetaContent('og:title') ||
268               getTextContent('h1.product-detail-name') ||
269               getTextContent('h1[itemprop="name"]') ||
270               document.title.trim();
271
272    if (!name) return; // Ohne Name kein Schema
273
274    // Preis: OG product:price:amount > itemprop lowPrice > itemprop price > DOM
275    var priceStr = getMetaContent('product:price:amount') ||
276                   getAttr('[itemprop="lowPrice"]', 'content') ||
277                   getAttr('[itemprop="price"]', 'content');
278
279    // Fallback: Preis aus DOM-Text parsen
280    if (!priceStr) {
281      var priceEl = document.querySelector('.product-detail-price');
282      if (priceEl) {
283        var priceText = priceEl.textContent.trim();
284        // "10,25 €" oder "15,95 €" parsen
285        var priceMatch = priceText.match(/([\d]+[.,][\d]{2})/);
286        if (priceMatch) {
287          priceStr = priceMatch[1].replace(',', '.');
288        }
289      }
290    }
291
292    if (!priceStr) return; // Ohne Preis kein Schema
293
294    // Preis normalisieren
295    var price = parseFloat(String(priceStr).replace(',', '.'));
296    if (isNaN(price) || price <= 0) return;
297
298    // Waehrung: OG > itemprop > Default EUR
299    var currency = getMetaContent('product:price:currency') ||
300                   getAttr('[itemprop="priceCurrency"]', 'content') ||
301                   'EUR';
302
303    // Beschreibung: meta description > OG description
304    var description = getMetaContent('description') ||
305                      getMetaContent('og:description') ||
306                      '';
307    if (description.length > 500) {
308      description = description.substring(0, 497) + '...';
309    }
310
311    // Bild: OG image (zuverlaessigste Quelle auf Shopware)
312    var image = getMetaContent('og:image') || '';
313
314    // URL: canonical > current
315    var url = getAttr('link[rel="canonical"]', 'href') ||
316              window.location.href;
317    // Sicherstellen dass https
318    if (url.indexOf('http') !== 0) {
319      url = window.location.href;
320    }
321
322    // Marke/Hersteller: OG brand > Microdata brand > Manufacturer-Link title
323    var brand = getMetaContent('product:brand') ||
324                getAttr('[itemprop="brand"] [itemprop="name"]', 'content') ||
325                getAttr('[itemprop="brand"] meta[itemprop="name"]', 'content') ||
326                getAttr('.product-detail-manufacturer-link', 'title') ||
327                '';
328
329    // Hersteller-URL: Shopware Manufacturer-Link (z.B. /winzer/lenotti/)
330    var brandUrl = getAttr('.product-detail-manufacturer-link', 'href') || '';
331
332    // SKU / Artikelnummer: itemprop sku (span) > DOM
333    var sku = getTextContent('[itemprop="sku"]') ||
334              getTextContent('.product-detail-ordernumber') ||
335              '';
336    sku = sku.trim();
337
338    // GTIN/EAN: Shopware 6.7 nutzt itemprop="gtin13"
339    var gtin = getAttr('[itemprop="gtin13"]', 'content') ||
340               getAttr('[itemprop="gtin"]', 'content') ||
341               getAttr('[itemprop="gtin8"]', 'content') ||
342               '';
343
344    // Verfuegbarkeit: Shopware setzt <link itemprop="availability" href="...">
345    var availabilityUrl = getAttr('link[itemprop="availability"]', 'href') ||
346                          getAttr('[itemprop="availability"]', 'href') ||
347                          getAttr('[itemprop="availability"]', 'content') ||
348                          '';
349
350    // Fallback: Lieferstatus-Text analysieren
351    if (!availabilityUrl || availabilityUrl.indexOf('schema.org') === -1) {
352      var deliveryEl = document.querySelector('.delivery-information') ||
353                       document.querySelector('.product-delivery-information');
354      if (deliveryEl) {
355        var deliveryText = deliveryEl.textContent.toLowerCase();
356        if (deliveryText.indexOf('sofort') !== -1 || deliveryText.indexOf('verfügbar') !== -1 || deliveryText.indexOf('lager') !== -1) {
357          availabilityUrl = 'https://schema.org/InStock';
358        } else if (deliveryText.indexOf('nicht') !== -1 || deliveryText.indexOf('ausverkauft') !== -1) {
359          availabilityUrl = 'https://schema.org/OutOfStock';
360        } else if (deliveryText.indexOf('vorbestellung') !== -1) {
361          availabilityUrl = 'https://schema.org/PreOrder';
362        }
363      }
364      // Letzter Fallback
365      if (!availabilityUrl || availabilityUrl.indexOf('schema.org') === -1) {
366        availabilityUrl = 'https://schema.org/InStock';
367      }
368    }
369
370    // --- JSON-LD bauen ---
371
372    var productSchema = {
373      "@type": "Product",
374      "name": name,
375      "url": url
376    };
377
378    if (description) productSchema.description = description;
379    if (image) productSchema.image = image;
380    if (sku) productSchema.sku = sku;
381    if (gtin) productSchema.gtin13 = gtin;
382
383    if (brand) {
384      productSchema.brand = {
385        "@type": "Brand",
386        "name": brand
387      };
388      if (brandUrl) {
389        productSchema.brand.url = brandUrl;
390      }
391    }
392
393    // Offer mit shippingDetails und returnPolicy
394    // Hinweis: Shopware Microdata nutzt AggregateOffer (wegen Staffelpreise),
395    // wir nutzen bewusst "Offer" mit dem niedrigsten Preis (product:price:amount).
396    // Google bevorzugt JSON-LD Offer fuer Product Rich Results und kombiniert beides.
397    productSchema.offers = {
398      "@type": "Offer",
399      "price": price.toFixed(2),
400      "priceCurrency": currency,
401      "availability": availabilityUrl,
402      "url": url,
403      "priceValidUntil": (new Date().getFullYear()) + "-12-31",
404      "seller": {
405        "@type": "Organization",
406        "name": "importweine.de"
407      },
408      // Versandkosten (hardcoded — identisch fuer alle Produkte)
409      "shippingDetails": {
410        "@type": "OfferShippingDetails",
411        "shippingRate": {
412          "@type": "MonetaryAmount",
413          "value": "5.90",
414          "currency": "EUR"
415        },
416        "shippingDestination": {
417          "@type": "DefinedRegion",
418          "addressCountry": "DE"
419        },
420        "deliveryTime": {
421          "@type": "ShippingDeliveryTime",
422          "handlingTime": {
423            "@type": "QuantitativeValue",
424            "minValue": 1,
425            "maxValue": 2,
426            "unitCode": "DAY"
427          },
428          "transitTime": {
429            "@type": "QuantitativeValue",
430            "minValue": 2,
431            "maxValue": 5,
432            "unitCode": "DAY"
433          }
434        }
435      },
436      // Rueckgaberecht (hardcoded — identisch fuer alle Produkte)
437      "hasMerchantReturnPolicy": {
438        "@type": "MerchantReturnPolicy",
439        "applicableCountry": "DE",
440        "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
441        "merchantReturnDays": 14,
442        "returnMethod": "https://schema.org/ReturnByMail",
443        "returnFees": "https://schema.org/ReturnFeesCustomerResponsibility",
444        "refundType": "https://schema.org/FullRefund",
445        "merchantReturnLink": "https://importweine.de/widerrufsbelehrung"
446      }
447    };
448
449    // --- Mit bestehendem FAQ-Schema kombinieren ---
450
451    // Bestehendes FAQ-Schema suchen (eigene Schleifenvariable um var-Kollision zu vermeiden)
452    var faqSchema = null;
453    var faqScriptEl = null;
454
455    for (var fi = 0; fi < existingScripts.length; fi++) {
456      try {
457        var parsed = JSON.parse(existingScripts[fi].textContent);
458        if (parsed['@type'] === 'FAQPage') {
459          faqSchema = parsed;
460          faqScriptEl = existingScripts[fi];
461          break;
462        }
463        if (parsed['@graph']) {
464          for (var fj = 0; fj < parsed['@graph'].length; fj++) {
465            if (parsed['@graph'][fj]['@type'] === 'FAQPage') {
466              faqSchema = parsed['@graph'][fj];
467              faqScriptEl = existingScripts[fi];
468              break;
469            }
470          }
471          if (faqSchema) break;
472        }
473      } catch(e) {
474        // Ungueltiges JSON ignorieren
475      }
476    }
477
478    var finalSchema;
479
480    if (faqSchema) {
481      // Kombiniere Product + FAQ in @graph
482      delete faqSchema['@context'];
483
484      finalSchema = {
485        "@context": "https://schema.org",
486        "@graph": [productSchema, faqSchema]
487      };
488
489      // Altes FAQ-Script entfernen (wird durch kombiniertes ersetzt)
490      if (faqScriptEl) {
491        faqScriptEl.parentNode.removeChild(faqScriptEl);
492      }
493    } else {
494      // Nur Product Schema
495      productSchema["@context"] = "https://schema.org";
496      finalSchema = productSchema;
497    }
498
499    // --- Injizieren ---
500    var script = document.createElement('script');
501    script.type = 'application/ld+json';
502    script.textContent = JSON.stringify(finalSchema);
503    document.head.appendChild(script);
504  }
505
506
507  // ─────────────────────────────────────────────────
508  // TEIL 2: Organization Schema (nur Homepage)
509  // ─────────────────────────────────────────────────
510
511  function injectOrganizationSchema() {
512    // Nur auf der Homepage ausfuehren
513    var path = window.location.pathname;
514    if (path !== '/' && path !== '' && path !== '/index' && path !== '/index.html') return;
515
516    // Pruefe ob Organization Schema bereits existiert (Duplikat vermeiden)
517    var existingSchemas = document.querySelectorAll('script[type="application/ld+json"]');
518    for (var i = 0; i < existingSchemas.length; i++) {
519      try {
520        var parsed = JSON.parse(existingSchemas[i].textContent);
521        if (parsed['@type'] === 'Organization' || parsed['@type'] === 'OnlineStore') return;
522        if (parsed['@graph']) {
523          for (var j = 0; j < parsed['@graph'].length; j++) {
524            if (parsed['@graph'][j]['@type'] === 'Organization' || parsed['@graph'][j]['@type'] === 'OnlineStore') return;
525          }
526        }
527      } catch(e) {}
528    }
529
530    var orgSchema = {
531      "@context": "https://schema.org",
532      "@type": "OnlineStore",
533      "name": "importweine.de",
534      "alternateName": "Import Weine",
535      "url": "https://www.importweine.de",
536      "logo": "https://www.importweine.de/media/64/2c/47/1610194017/importweine_logo.png",
537      "description": "Ihr Online-Fachhandel fuer italienische Weine, franzoesische Weine und erlesene Spirituosen. Ueber 1.400 Produkte direkt vom Importeur.",
538      "telephone": "+49-6887-30060",
539      "email": "[email protected]",
540      "address": {
541        "@type": "PostalAddress",
542        "addressCountry": "DE"
543      },
544      "sameAs": [
545        "https://www.instagram.com/importweine/",
546        "https://www.facebook.com/importweine/",
547        "https://www.trustedshops.de/bewertung/info_X073F6468DA95D062BA271F7C7E84797E.html"
548      ],
549      "aggregateRating": {
550        "@type": "AggregateRating",
551        "ratingValue": "4.90",
552        "reviewCount": "599",
553        "bestRating": "5",
554        "worstRating": "1"
555      },
556      "hasMerchantReturnPolicy": {
557        "@type": "MerchantReturnPolicy",
558        "@id": "https://importweine.de/#return_policy",
559        "applicableCountry": "DE",
560        "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
561        "merchantReturnDays": 14,
562        "returnMethod": "https://schema.org/ReturnByMail",
563        "returnFees": "https://schema.org/ReturnFeesCustomerResponsibility",
564        "refundType": "https://schema.org/FullRefund",
565        "merchantReturnLink": "https://importweine.de/widerrufsbelehrung"
566      }
567    };
568
569    var script = document.createElement('script');
570    script.type = 'application/ld+json';
571    script.textContent = JSON.stringify(orgSchema);
572    document.head.appendChild(script);
573  }
574
575
576  // ─────────────────────────────────────────────────
577  // Hilfsfunktionen
578  // ─────────────────────────────────────────────────
579
580  /** Meta-Tag content lesen (name oder property) */
581  function getMetaContent(nameOrProperty) {
582    var el = document.querySelector('meta[property="' + nameOrProperty + '"]') ||
583             document.querySelector('meta[name="' + nameOrProperty + '"]');
584    return el ? (el.getAttribute('content') || '').trim() : '';
585  }
586
587  /** Beliebiges Attribut eines Elements lesen */
588  function getAttr(selector, attr) {
589    var el = document.querySelector(selector);
590    return el ? (el.getAttribute(attr) || '').trim() : '';
591  }
592
593  /** Text-Inhalt eines Elements lesen */
594  function getTextContent(selector) {
595    var el = document.querySelector(selector);
596    return el ? el.textContent.trim() : '';
597  }
598
599
600  // ─────────────────────────────────────────────────
601  // Ausfuehrung
602  // ─────────────────────────────────────────────────
603
604  if (document.readyState === 'loading') {
605    document.addEventListener('DOMContentLoaded', function() {
606      injectProductSchema();
607      injectOrganizationSchema();
608    });
609  } else {
610    injectProductSchema();
611    injectOrganizationSchema();
612  }
613
614})();
615

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.