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.