391 lines
15 KiB
Vue
391 lines
15 KiB
Vue
<template>
|
||
<div v-if="isEnabled" :class="['header-ad-banner', { sticky: finalSticky } ]">
|
||
<div ref="adContainer" class="ad-container" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||
|
||
const adContainer = ref(null);
|
||
|
||
// Provider selection via env: 'adsense' or 'propeller'
|
||
const provider = (import.meta.env.VITE_AD_PROVIDER || 'propeller').toLowerCase();
|
||
|
||
// AdSense config (kept for compatibility)
|
||
const adClient = import.meta.env.VITE_ADSENSE_CLIENT || '';
|
||
const adSlot = import.meta.env.VITE_ADSENSE_HEADER_SLOT || '';
|
||
|
||
// Propeller config: you can set the script URL and the slot id via env
|
||
// Defaults set to the in-page push tag you provided (can be overridden via env)
|
||
const propScriptUrl = import.meta.env.VITE_PROP_SCRIPT_URL || 'https://nap5k.com/tag.min.js';
|
||
const propSlotId = import.meta.env.VITE_PROP_SLOT || '11023637';
|
||
|
||
// Show only if the selected provider has config
|
||
const isEnabled = computed(() => {
|
||
if (provider === 'adsense') return Boolean(adClient && adSlot);
|
||
if (provider === 'propeller') return Boolean(propScriptUrl && propSlotId);
|
||
return false;
|
||
});
|
||
|
||
// Small sticky header option (thin sticky header)
|
||
const sticky = (import.meta.env.VITE_HEADER_STICKY || 'true') === 'true';
|
||
|
||
// Debug bypass: allow forcing ad load with ?ads_debug=1
|
||
const debugForceLoad = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('ads_debug') === '1';
|
||
|
||
// Auto load flag (env) - set to 'true' to load ads on mount without waiting for interaction
|
||
const autoLoad = (import.meta.env.VITE_PROP_AUTOLOAD || 'true') === 'true';
|
||
|
||
// A/B test config
|
||
const abRatio = Number(import.meta.env.VITE_AB_RATIO || '50'); // percent for variant B
|
||
|
||
function getStoredVariant() {
|
||
try {
|
||
const v = localStorage.getItem('ads_ab_variant');
|
||
if (v === 'A' || v === 'B') return v;
|
||
} catch (e) {}
|
||
return null;
|
||
}
|
||
|
||
function assignVariant() {
|
||
const existing = getStoredVariant();
|
||
if (existing) return existing;
|
||
const r = Math.random() * 100;
|
||
const variant = r < abRatio ? 'B' : 'A';
|
||
try {
|
||
localStorage.setItem('ads_ab_variant', variant);
|
||
} catch (e) {}
|
||
window.dispatchEvent(new CustomEvent('ads:ab-assigned', { detail: { variant } }));
|
||
return variant;
|
||
}
|
||
|
||
const variant = getStoredVariant() || assignVariant();
|
||
// finalSticky follows the variant: A = static, B = sticky
|
||
const finalSticky = variant === 'B' ? true : variant === 'A' ? false : sticky;
|
||
|
||
function hasConsent() {
|
||
try {
|
||
return localStorage.getItem('ads_consent') === 'true';
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function listenForConsentEvent() {
|
||
function onConsent() {
|
||
loadAdIfNeeded();
|
||
window.removeEventListener('ads:consent-granted', onConsent);
|
||
}
|
||
window.addEventListener('ads:consent-granted', onConsent);
|
||
}
|
||
|
||
function ensureScriptLoaded(src, attrName) {
|
||
if (document.querySelector(`script[data-ad-loader="${attrName}"]`)) return Promise.resolve();
|
||
return new Promise((resolve, reject) => {
|
||
const s = document.createElement('script');
|
||
s.async = true;
|
||
s.src = src;
|
||
s.dataset.adLoader = attrName;
|
||
s.onload = () => resolve();
|
||
s.onerror = (e) => reject(e);
|
||
document.head.appendChild(s);
|
||
});
|
||
}
|
||
|
||
async function renderPropeller() {
|
||
if (!adContainer.value) return;
|
||
try {
|
||
// Insert Propeller in-page push tag exactly as provided by PropellerAds.
|
||
// Avoid duplicate insertion if same script+zone already present.
|
||
if (document.querySelector(`script[src="${propScriptUrl}"][data-zone="${propSlotId}"]`)) {
|
||
return;
|
||
}
|
||
|
||
// create placeholder first and insert into ad container
|
||
const placeholder = document.createElement('div');
|
||
placeholder.className = 'propeller-ad-placeholder';
|
||
adContainer.value.appendChild(placeholder);
|
||
|
||
const s = document.createElement('script');
|
||
s.dataset.zone = propSlotId;
|
||
s.src = propScriptUrl;
|
||
s.async = true;
|
||
s.onload = () => {
|
||
console.log('Propeller script loaded:', propScriptUrl, 'zone:', propSlotId);
|
||
};
|
||
s.onerror = (e) => {
|
||
console.warn('Propeller script failed to load:', propScriptUrl, e);
|
||
try { window.dispatchEvent(new CustomEvent('ads:load-failed', { detail: { provider: 'propeller', zone: propSlotId, error: String(e) } })); } catch {}
|
||
};
|
||
|
||
// Append the script directly into the placeholder so the provider injects into it
|
||
placeholder.appendChild(s);
|
||
|
||
// Observe the document for injected ad nodes and relocate them into our placeholder
|
||
const observeAndRelocate = () => {
|
||
const parent = document.body;
|
||
try {
|
||
const observer = new MutationObserver(mutations => {
|
||
for (const m of mutations) {
|
||
for (const node of Array.from(m.addedNodes)) {
|
||
try {
|
||
// Handle text nodes that may contain raw script/HTML
|
||
if (node.nodeType === Node.TEXT_NODE) {
|
||
const txt = (node.nodeValue || '').trim();
|
||
if (txt.includes('<script') || txt.length > 500) {
|
||
// remove large/raw script text nodes to avoid visible JS
|
||
node.parentNode && node.parentNode.removeChild(node);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (!(node instanceof HTMLElement)) continue;
|
||
|
||
// If element contains script tags, extract and execute them safely
|
||
const innerScripts = Array.from(node.querySelectorAll('script'));
|
||
if (innerScripts.length) {
|
||
for (const sc of innerScripts) {
|
||
try {
|
||
if (sc.src) {
|
||
const s2 = document.createElement('script');
|
||
s2.src = sc.src;
|
||
s2.async = true;
|
||
document.head.appendChild(s2);
|
||
} else if (sc.textContent) {
|
||
const s2 = document.createElement('script');
|
||
s2.textContent = sc.textContent;
|
||
document.head.appendChild(s2);
|
||
}
|
||
} catch (e) {
|
||
console.warn('execute inner script failed', e);
|
||
}
|
||
// remove the inline script node from original element to avoid visible code
|
||
sc.parentNode && sc.parentNode.removeChild(sc);
|
||
}
|
||
}
|
||
|
||
// If the added node is a script element itself, execute safely via head insertion
|
||
if (node.tagName === 'SCRIPT') {
|
||
const src = node.getAttribute('src');
|
||
if (src) {
|
||
const s2 = document.createElement('script');
|
||
s2.src = src;
|
||
s2.async = true;
|
||
s2.onload = () => console.log('relocated script loaded', src);
|
||
s2.onerror = (e) => console.warn('relocated script failed', src, e);
|
||
document.head.appendChild(s2);
|
||
node.parentNode && node.parentNode.removeChild(node);
|
||
continue;
|
||
} else if (node.textContent) {
|
||
const s2 = document.createElement('script');
|
||
s2.textContent = node.textContent;
|
||
document.head.appendChild(s2);
|
||
node.parentNode && node.parentNode.removeChild(node);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Detect iframe ad nodes or nodes with nap5k indicators
|
||
const iframe = node.tagName === 'IFRAME' ? node : node.querySelector && node.querySelector('iframe');
|
||
const src = iframe ? (iframe.getAttribute('src') || '') : '';
|
||
const isAdNode = src.includes('nap5k') || (node.dataset && node.dataset.zone === propSlotId) || /nap5k|propel|propeller|inpage|push/i.test((node.className || '') + ' ' + (node.id || ''));
|
||
if (isAdNode) {
|
||
try {
|
||
// move iframe/ad node into placeholder
|
||
placeholder.innerHTML = '';
|
||
if (iframe && iframe.parentNode) {
|
||
placeholder.appendChild(iframe);
|
||
} else {
|
||
placeholder.appendChild(node);
|
||
}
|
||
const moved = placeholder.firstElementChild;
|
||
if (moved) {
|
||
// Normalize size
|
||
moved.style.maxWidth = '100%';
|
||
moved.style.display = 'block';
|
||
// If provider inserted a fixed/absolute positioned element (overlays),
|
||
// force it to behave as inline block inside our header placeholder.
|
||
try {
|
||
const cs = getComputedStyle(moved);
|
||
if (cs.position === 'fixed' || cs.position === 'absolute') {
|
||
moved.style.setProperty('position', 'relative', 'important');
|
||
moved.style.setProperty('top', 'auto', 'important');
|
||
moved.style.setProperty('right', 'auto', 'important');
|
||
moved.style.setProperty('left', 'auto', 'important');
|
||
moved.style.setProperty('bottom', 'auto', 'important');
|
||
moved.style.setProperty('z-index', '1', 'important');
|
||
moved.style.setProperty('width', '100%', 'important');
|
||
moved.style.setProperty('max-width', '970px', 'important');
|
||
}
|
||
if (moved.tagName === 'IFRAME') {
|
||
moved.style.width = '100%';
|
||
moved.style.height = moved.style.height || '60px';
|
||
}
|
||
} catch (e) {
|
||
console.warn('style normalize failed', e);
|
||
}
|
||
}
|
||
observer.disconnect();
|
||
try { window.dispatchEvent(new CustomEvent('ads:relocated',{detail:{provider:'propeller',zone:propSlotId}})); } catch {}
|
||
return;
|
||
} catch (e) {
|
||
console.warn('Failed to relocate ad node', e);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('mutation handler error', e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
observer.observe(parent, { childList: true, subtree: true });
|
||
|
||
// Check existing iframes right away
|
||
const existing = Array.from(document.querySelectorAll('iframe')).find(f => (f.src || '').includes('nap5k'));
|
||
if (existing) {
|
||
placeholder.innerHTML = '';
|
||
placeholder.appendChild(existing);
|
||
observer.disconnect();
|
||
}
|
||
|
||
// Failsafe: stop observing after 6s
|
||
setTimeout(() => observer.disconnect(), 6000);
|
||
} catch (e) {
|
||
console.warn('observeAndRelocate error', e);
|
||
}
|
||
};
|
||
|
||
// Start observing shortly after script insertion to allow provider to run
|
||
setTimeout(observeAndRelocate, 100);
|
||
|
||
// Fallback: wenn nach Script‑Load innerhalb 3s kein Ad im Placeholder erscheint,
|
||
// lade ein iframe‑Backup. Das erhöht die Chance, ein sichtbares Banner im Header zu zeigen.
|
||
setTimeout(() => {
|
||
try {
|
||
const ph = adContainer.value.querySelector('.propeller-ad-placeholder');
|
||
const hasChild = ph && ph.children && ph.children.length > 0;
|
||
const height = ph ? parseInt(getComputedStyle(ph).height || '0', 10) : 0;
|
||
if (!hasChild && (!height || height === 0)) {
|
||
console.log('Ad placeholder empty — inserting iframe fallback');
|
||
const iframe = document.createElement('iframe');
|
||
iframe.width = '100%';
|
||
iframe.height = '90';
|
||
iframe.frameBorder = '0';
|
||
iframe.scrolling = 'no';
|
||
iframe.style.border = '0';
|
||
iframe.title = 'Advertisement';
|
||
// Fallback URL: attempt to call tag URL with slot param — may be accepted by provider
|
||
iframe.src = `${propScriptUrl}?slot=${encodeURIComponent(propSlotId)}&responsive=1`;
|
||
ph.innerHTML = '';
|
||
ph.appendChild(iframe);
|
||
try { window.dispatchEvent(new CustomEvent('ads:fallback-inserted',{detail:{provider:'propeller',zone:propSlotId}})); } catch {}
|
||
}
|
||
} catch (e) {
|
||
console.warn('Fallback insertion failed', e);
|
||
}
|
||
}, 3000);
|
||
} catch (err) {
|
||
console.warn('Propeller ad load failed', err);
|
||
}
|
||
}
|
||
|
||
async function renderAdSense() {
|
||
if (!adContainer.value) return;
|
||
try {
|
||
await ensureScriptLoaded(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adClient}`, 'adsense');
|
||
const ins = document.createElement('ins');
|
||
ins.className = 'adsbygoogle';
|
||
ins.style.display = 'block';
|
||
ins.style.width = '100%';
|
||
ins.style.height = '50px';
|
||
ins.setAttribute('data-ad-client', adClient);
|
||
ins.setAttribute('data-ad-slot', adSlot);
|
||
adContainer.value.innerHTML = '';
|
||
adContainer.value.appendChild(ins);
|
||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||
} catch (err) {
|
||
console.warn('AdSense load failed', err);
|
||
}
|
||
}
|
||
|
||
let adLoaded = false;
|
||
async function loadAdIfNeeded() {
|
||
if (adLoaded || !isEnabled.value) return;
|
||
adLoaded = true;
|
||
try {
|
||
if (adContainer.value) adContainer.value.dataset.abVariant = variant;
|
||
window.dispatchEvent(new CustomEvent('ads:load-start', { detail: { provider, variant } }));
|
||
} catch (e) {}
|
||
if (provider === 'propeller') await renderPropeller();
|
||
else if (provider === 'adsense') await renderAdSense();
|
||
try { window.dispatchEvent(new CustomEvent('ads:loaded', { detail: { provider, variant } })); } catch (e) {}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if (!isEnabled.value) return;
|
||
|
||
// If consent present, autoLoad, or debugForceLoad => load immediately
|
||
if (hasConsent() || autoLoad || debugForceLoad) {
|
||
await nextTick();
|
||
await loadAdIfNeeded();
|
||
return;
|
||
}
|
||
|
||
// Otherwise, wait for either a user interaction or consent event
|
||
const onFirstInteraction = async () => {
|
||
window.removeEventListener('pointerdown', onFirstInteraction);
|
||
window.removeEventListener('scroll', onFirstInteraction);
|
||
await loadAdIfNeeded();
|
||
};
|
||
|
||
window.addEventListener('pointerdown', onFirstInteraction, { once: true });
|
||
window.addEventListener('scroll', onFirstInteraction, { once: true });
|
||
listenForConsentEvent();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.header-ad-banner {
|
||
width: 100%;
|
||
max-height: 90px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 6px 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.header-ad-banner.sticky {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 600;
|
||
background: rgba(255,255,255,0.98);
|
||
backdrop-filter: blur(4px);
|
||
border-bottom: 1px solid rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.ad-container {
|
||
width: 100%;
|
||
max-width: 980px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ad-container iframe,
|
||
.ad-container ins {
|
||
width: 100%;
|
||
max-width: 970px;
|
||
height: 60px;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.ad-container iframe,
|
||
.ad-container ins {
|
||
height: 50px;
|
||
max-width: 320px;
|
||
}
|
||
}
|
||
</style>
|