This commit is contained in:
136
ADSENSE.md
136
ADSENSE.md
@@ -1,130 +1,22 @@
|
|||||||
# AdSense in YpChat
|
# Werbung in YpChat
|
||||||
|
|
||||||
## Ziel
|
Die bisherige AdSense-/Propeller-Dokumentation ist veraltet.
|
||||||
|
|
||||||
Im Header kann ein Google-AdSense-Banner eingeblendet werden. Die Einbindung ist bereits vorbereitet, aber nur aktiv, wenn die passenden Vite-Variablen gesetzt sind.
|
Aktuell ist im Projekt eine direkte Adsterra-Headerbanner-Loesung aktiv:
|
||||||
|
|
||||||
## Bereits im Code vorbereitet
|
- Mobile: `320x50`
|
||||||
|
- Key: `fb9b5e7f817d40d72943dae0c54eb769`
|
||||||
|
- Desktop: `468x60`
|
||||||
|
- Key: `2b658317c1e28b4b4f234d26c8fca28d`
|
||||||
|
|
||||||
- Header-Komponente: [HeaderAdBanner.vue](/mnt/share/torsten/Programs/YpChat/client/src/components/HeaderAdBanner.vue)
|
Implementierung:
|
||||||
- Einbindung in die Kopfzeilen:
|
|
||||||
- [ChatView.vue](/mnt/share/torsten/Programs/YpChat/client/src/views/ChatView.vue)
|
|
||||||
- [PartnersView.vue](/mnt/share/torsten/Programs/YpChat/client/src/views/PartnersView.vue)
|
|
||||||
- [FeedbackView.vue](/mnt/share/torsten/Programs/YpChat/client/src/views/FeedbackView.vue)
|
|
||||||
|
|
||||||
Aktiv wird der Banner nur mit:
|
- [client/src/components/HeaderAdBanner.vue](/mnt/share/torsten/Programs/SingleChat/client/src/components/HeaderAdBanner.vue)
|
||||||
|
|
||||||
- `VITE_ADSENSE_CLIENT`
|
Funktionsweise:
|
||||||
- `VITE_ADSENSE_HEADER_SLOT`
|
|
||||||
|
|
||||||
## Was bei Google AdSense erledigt werden muss
|
- bis `720px` Viewport wird das `320x50`-Banner geladen
|
||||||
|
- ab `721px` Viewport wird das `468x60`-Banner geladen
|
||||||
|
- eingebunden ueber Adsterra `IFRAME SYNC`
|
||||||
|
|
||||||
### 1. AdSense-Konto und Website
|
Wird spaeter ein anderer Werbeanbieter oder ein weiteres Adsterra-Format verwendet, sollte diese Datei entsprechend aktualisiert werden.
|
||||||
|
|
||||||
In AdSense selbst:
|
|
||||||
|
|
||||||
1. Website `ypchat.net` hinzufügen
|
|
||||||
2. Eigentum/Einbindung abschließen
|
|
||||||
3. Warten, bis die Website von Google geprüft und freigegeben wurde
|
|
||||||
|
|
||||||
Ohne freigegebene Website werden in der Regel keine regulären Anzeigen ausgeliefert.
|
|
||||||
|
|
||||||
### 2. Anzeigenblock anlegen
|
|
||||||
|
|
||||||
Für den Header in AdSense einen normalen responsiven Display-Anzeigenblock anlegen.
|
|
||||||
|
|
||||||
Benötigt werden daraus:
|
|
||||||
|
|
||||||
- Publisher-ID
|
|
||||||
Beispiel: `ca-pub-1234567890123456`
|
|
||||||
- Slot-ID des Header-Anzeigenblocks
|
|
||||||
Beispiel: `1234567890`
|
|
||||||
|
|
||||||
### 3. `ads.txt` korrekt pflegen
|
|
||||||
|
|
||||||
AdSense erwartet in der Regel einen korrekten Eintrag in `/ads.txt`.
|
|
||||||
|
|
||||||
Für Google AdSense ist das Format typischerweise:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0
|
|
||||||
```
|
|
||||||
|
|
||||||
Wichtig:
|
|
||||||
|
|
||||||
- `pub-...` muss zu deinem echten AdSense-Konto passen
|
|
||||||
- die Datei muss öffentlich unter `https://ypchat.net/ads.txt` erreichbar sein
|
|
||||||
- Änderungen brauchen oft etwas Zeit, bis Google sie erkennt
|
|
||||||
|
|
||||||
Im Projekt liegt aktuell eine Datei unter [docroot/ads.txt](/mnt/share/torsten/Programs/YpChat/docroot/ads.txt). Diese muss auf deine echte Publisher-ID geprüft und ggf. angepasst werden.
|
|
||||||
|
|
||||||
## Was im Projekt erledigt werden muss
|
|
||||||
|
|
||||||
### 1. Vite-Variablen setzen
|
|
||||||
|
|
||||||
In der Projekt-`.env` die beiden Werte ergänzen:
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_ADSENSE_CLIENT=ca-pub-1234567890123456
|
|
||||||
VITE_ADSENSE_HEADER_SLOT=1234567890
|
|
||||||
```
|
|
||||||
|
|
||||||
Hinweis:
|
|
||||||
|
|
||||||
- `VITE_...` ist notwendig, damit die Werte im Client verfügbar sind
|
|
||||||
- ohne diese Werte bleibt der Banner automatisch unsichtbar
|
|
||||||
|
|
||||||
### 2. Frontend neu bauen
|
|
||||||
|
|
||||||
Nach Änderung der `.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd client
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach wie bisher den Build nach `docroot/dist` deployen.
|
|
||||||
|
|
||||||
### 3. Server/Deployment aktualisieren
|
|
||||||
|
|
||||||
Je nach Deploy-Prozess:
|
|
||||||
|
|
||||||
1. neuen Client-Build deployen
|
|
||||||
2. prüfen, dass `docroot/dist` aktuell ist
|
|
||||||
3. Service neu starten oder Deployment neu laden
|
|
||||||
|
|
||||||
## Prüfung nach dem Deploy
|
|
||||||
|
|
||||||
### Technisch
|
|
||||||
|
|
||||||
Prüfen:
|
|
||||||
|
|
||||||
- ist im HTML ein AdSense-Script geladen?
|
|
||||||
- erscheint im Header ein reservierter Anzeigenbereich?
|
|
||||||
- gibt es Fehler in der Browser-Konsole?
|
|
||||||
|
|
||||||
### Extern
|
|
||||||
|
|
||||||
Prüfen:
|
|
||||||
|
|
||||||
- `https://ypchat.net/ads.txt` ist erreichbar
|
|
||||||
- AdSense zeigt keinen `ads.txt`-Fehler mehr
|
|
||||||
- die Website ist in AdSense als bereit/freigegeben markiert
|
|
||||||
|
|
||||||
## Wichtige Hinweise
|
|
||||||
|
|
||||||
- Im lokalen Development erscheinen AdSense-Anzeigen oft nicht sinnvoll oder gar nicht.
|
|
||||||
- Nach dem ersten Einbau kann es dauern, bis Google echte Anzeigen ausliefert.
|
|
||||||
- Wenn Header-Anzeigen die UX zu stark stören, sollte der Banner auf Mobile ausgeblendet bleiben. Das ist im Code bereits berücksichtigt.
|
|
||||||
- Für Consent-/CMP-Themen kann je nach Land eine zusätzliche Einwilligungslösung nötig sein. Das ist aktuell nicht Teil dieser Implementierung.
|
|
||||||
|
|
||||||
## Kurz-Checkliste
|
|
||||||
|
|
||||||
1. In AdSense Website hinzufügen und freigeben lassen.
|
|
||||||
2. Header-Display-Ad-Unit anlegen.
|
|
||||||
3. Publisher-ID und Slot-ID notieren.
|
|
||||||
4. `docroot/ads.txt` auf korrekten Google-Eintrag prüfen.
|
|
||||||
5. `.env` mit `VITE_ADSENSE_CLIENT` und `VITE_ADSENSE_HEADER_SLOT` ergänzen.
|
|
||||||
6. Client neu bauen.
|
|
||||||
7. Deployen.
|
|
||||||
8. Live prüfen, ob `ads.txt` und Banner korrekt ausgeliefert werden.
|
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import android.Manifest
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color as AndroidColor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -66,6 +71,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -128,6 +134,8 @@ private val Primary500 = Color(0xFF3D8654)
|
|||||||
private val Primary100 = Color(0xFFE7F1EA)
|
private val Primary100 = Color(0xFFE7F1EA)
|
||||||
private val Danger = Color(0xFFA24040)
|
private val Danger = Color(0xFFA24040)
|
||||||
private const val PrivacyPolicyUrl = "https://www.ypchat.net/datenschutz"
|
private const val PrivacyPolicyUrl = "https://www.ypchat.net/datenschutz"
|
||||||
|
private const val AndroidAdMobileKey = "fb9b5e7f817d40d72943dae0c54eb769"
|
||||||
|
private const val AndroidAdDesktopKey = "2b658317c1e28b4b4f234d26c8fca28d"
|
||||||
|
|
||||||
private data class GenderOption(val value: String, val label: String)
|
private data class GenderOption(val value: String, val label: String)
|
||||||
private data class SmileyItem(val token: String, val hexCode: String, val tooltip: String)
|
private data class SmileyItem(val token: String, val hexCode: String, val tooltip: String)
|
||||||
@@ -194,6 +202,7 @@ private fun LoginScreen(state: ChatState, onLogin: (String, String, Int, String)
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
LandingIntroCard()
|
LandingIntroCard()
|
||||||
|
AndroidHeaderAdBanner()
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@@ -333,17 +342,20 @@ private fun ChatShell(state: ChatState, viewModel: ChatViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(modifier = Modifier.padding(padding).fillMaxSize()) {
|
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
|
||||||
if (state.currentConversation != null) {
|
AndroidHeaderAdBanner()
|
||||||
ChatScreen(state, viewModel)
|
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
} else {
|
if (state.currentConversation != null) {
|
||||||
when (selectedTab) {
|
ChatScreen(state, viewModel)
|
||||||
AppTab.Online -> UserListScreen(state.users, state.countries, onUserClick = viewModel::openConversation)
|
} else {
|
||||||
AppTab.Search -> SearchScreen(state, viewModel)
|
when (selectedTab) {
|
||||||
AppTab.Inbox -> InboxScreen(state, viewModel)
|
AppTab.Online -> UserListScreen(state.users, state.countries, onUserClick = viewModel::openConversation)
|
||||||
AppTab.History -> HistoryScreen(state, viewModel)
|
AppTab.Search -> SearchScreen(state, viewModel)
|
||||||
AppTab.Console -> ConsoleScreen(state, viewModel)
|
AppTab.Inbox -> InboxScreen(state, viewModel)
|
||||||
AppTab.More -> MoreScreen(state, viewModel, moreSection) { moreSection = it }
|
AppTab.History -> HistoryScreen(state, viewModel)
|
||||||
|
AppTab.Console -> ConsoleScreen(state, viewModel)
|
||||||
|
AppTab.More -> MoreScreen(state, viewModel, moreSection) { moreSection = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,6 +392,118 @@ private fun UserListScreen(users: List<UserDto>, countries: List<CountryOption>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class AndroidAdPlacement(
|
||||||
|
val key: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AndroidHeaderAdBanner() {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val placement = remember(configuration.screenWidthDp) {
|
||||||
|
if (configuration.screenWidthDp <= 600) {
|
||||||
|
AndroidAdPlacement(
|
||||||
|
key = AndroidAdMobileKey,
|
||||||
|
width = 320,
|
||||||
|
height = 50
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AndroidAdPlacement(
|
||||||
|
key = AndroidAdDesktopKey,
|
||||||
|
width = 468,
|
||||||
|
height = 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val html = remember(placement) { adsterraHtml(placement) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFFF4F8F5))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(placement.height.dp),
|
||||||
|
factory = { context ->
|
||||||
|
WebView(context).apply {
|
||||||
|
setBackgroundColor(AndroidColor.TRANSPARENT)
|
||||||
|
overScrollMode = WebView.OVER_SCROLL_NEVER
|
||||||
|
isVerticalScrollBarEnabled = false
|
||||||
|
isHorizontalScrollBarEnabled = false
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.domStorageEnabled = true
|
||||||
|
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
settings.loadsImagesAutomatically = true
|
||||||
|
webChromeClient = WebChromeClient()
|
||||||
|
webViewClient = object : WebViewClient() {}
|
||||||
|
loadDataWithBaseURL(
|
||||||
|
"https://www.highperformanceformat.com/",
|
||||||
|
html,
|
||||||
|
"text/html",
|
||||||
|
"utf-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { webView ->
|
||||||
|
val currentPlacementKey = webView.tag as? String
|
||||||
|
if (currentPlacementKey != placement.key) {
|
||||||
|
webView.tag = placement.key
|
||||||
|
webView.loadDataWithBaseURL(
|
||||||
|
"https://www.highperformanceformat.com/",
|
||||||
|
html,
|
||||||
|
"text/html",
|
||||||
|
"utf-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adsterraHtml(placement: AndroidAdPlacement): String = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
atOptions = {
|
||||||
|
'key' : '${placement.key}',
|
||||||
|
'format' : 'iframe',
|
||||||
|
'height' : ${placement.height},
|
||||||
|
'width' : ${placement.width},
|
||||||
|
'params' : {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="https://www.highperformanceformat.com/${placement.key}/invoke.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserRow(user: UserDto, countries: List<CountryOption>, onUserClick: (String) -> Unit) {
|
private fun UserRow(user: UserDto, countries: List<CountryOption>, onUserClick: (String) -> Unit) {
|
||||||
val countryLabel = remember(user.country, user.isoCountryCode, countries) { displayCountryName(user, countries) }
|
val countryLabel = remember(user.country, user.isoCountryCode, countries) { displayCountryName(user, countries) }
|
||||||
|
|||||||
@@ -1,78 +1,31 @@
|
|||||||
Aktuell habe ich das In‑Page Push Snippet, das du gepostet hast, als Default eingebaut:
|
# Header-Ad Integration
|
||||||
```html
|
|
||||||
<script>(function(s){s.dataset.zone='11023637',s.src='https://nap5k.com/tag.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script>
|
|
||||||
Das ist jetzt die Standard‑Konfiguration in `HeaderAdBanner.vue` wenn `VITE_AD_PROVIDER=propeller` ist. Du kannst die Werte via Env überschreiben:
|
|
||||||
- `VITE_PROP_SCRIPT_URL` (default `https://nap5k.com/tag.min.js`)
|
|
||||||
- `VITE_PROP_SLOT` (default `11023637`)
|
|
||||||
|
|
||||||
Wenn du später ein anderes Format/Zone anlegen willst, gib mir die neue Zone‑ID oder das komplette Snippet, dann passe ich es an.
|
Aktuell nutzt `YpChat` im Header eine direkte Adsterra-Integration ueber `HeaderAdBanner.vue`.
|
||||||
Live testen (empfohlen): kopiere die Datei in dein Produktions‑Dokument‑Root so, dass sie unter `https://yourdomain/scripts/sw.js` erreichbar ist. In diesem Repo habe ich die Datei zusätzlich ins Projekt‑Root (`/sw.js`) und in [docroot/sw.js](docroot/sw.js) gelegt.
|
|
||||||
|
|
||||||
Live testen (empfohlen): kopiere die Datei in dein Produktions‑Dokument‑Root so, dass sie unter `https://yourdomain/sw.js` erreichbar ist (Root Pfad). In diesem Repo habe ich die Datei zusätzlich ins Projekt‑Root (`/sw.js`) und in [docroot/sw.js](docroot/sw.js) gelegt.
|
Verwendete Placements:
|
||||||
Kurz: Anleitung zur Aktivierung des Header‑Ads (Propeller / AdSense)
|
|
||||||
|
|
||||||
1) Umgebungsvariablen
|
- Mobile: `320x50`
|
||||||
- Lege in deiner lokalen Umgebung (z. B. `.env.local`) folgende Variablen an:
|
- Key: `fb9b5e7f817d40d72943dae0c54eb769`
|
||||||
- `VITE_AD_PROVIDER=propeller` # oder `adsense`
|
- Desktop: `468x60`
|
||||||
- `VITE_PROP_SCRIPT_URL=https://example-propeller.example/ads.js` # Propeller: Script/Endpoint
|
- Key: `2b658317c1e28b4b4f234d26c8fca28d`
|
||||||
- `VITE_PROP_SLOT=YOUR_PROP_SLOT_ID`
|
|
||||||
- `VITE_ADSENSE_CLIENT=ca-pub-XXXXXXXXXXXX` # nur bei AdSense
|
|
||||||
- `VITE_ADSENSE_HEADER_SLOT=NNNNNNNNNN` # nur bei AdSense
|
|
||||||
- `VITE_HEADER_STICKY=true` # `true`=thin sticky header, `false`=static
|
|
||||||
|
|
||||||
2) Was ich bereits implementiert habe
|
Die Komponente waehlt automatisch anhand der Viewport-Breite:
|
||||||
- `client/src/components/HeaderAdBanner.vue` wurde erweitert: unterstützt `propeller` + `adsense`, lazy load nach Interaktion, prüft `localStorage('ads_consent')`, und kann sticky sein.
|
|
||||||
- `HeaderAdBanner` wurde in die Chat‑Ansicht eingebaut: [client/src/views/ChatView.vue](client/src/views/ChatView.vue)
|
|
||||||
|
|
||||||
3) Consent‑Handling (was du tun musst)
|
- bis `720px`: `320x50`
|
||||||
- Der Code wartet auf einen Consent-Wert in `localStorage` unter dem Key `ads_consent` (Wert `'true'`).
|
- ab `721px`: `468x60`
|
||||||
- Du solltest eine Consent‑UI (CMP / IAB TCF) implementieren, die nach Zustimmung `localStorage.setItem('ads_consent','true')` setzt und das Event `window.dispatchEvent(new Event('ads:consent-granted'))` feuert.
|
|
||||||
- Kurztest (ohne CMP): Öffne die Konsole und führe aus:
|
|
||||||
```js
|
|
||||||
localStorage.setItem('ads_consent','true');
|
|
||||||
window.dispatchEvent(new Event('ads:consent-granted'));
|
|
||||||
```
|
|
||||||
|
|
||||||
4) Propeller‑Integration
|
Einbauorte:
|
||||||
- Propeller liefert normalerweise ein Provider‑Snippet. Trage die korrekte `VITE_PROP_SCRIPT_URL` und `VITE_PROP_SLOT` ein.
|
|
||||||
- Falls Propeller ein Inline‑Div erwartet, liefere mir das Snippet/Anweisungen und ich passe `HeaderAdBanner.vue` an (derzeit wird ein iframe mit `?slot=` angehängt als generischer Fallback).
|
|
||||||
|
|
||||||
5) Test & Dev
|
- [client/src/components/HeaderAdBanner.vue](/mnt/share/torsten/Programs/SingleChat/client/src/components/HeaderAdBanner.vue)
|
||||||
- Dev starten:
|
- [client/src/views/ChatView.vue](/mnt/share/torsten/Programs/SingleChat/client/src/views/ChatView.vue)
|
||||||
```bash
|
- weitere SEO-/Info-Seiten mit `HeaderAdBanner`
|
||||||
cd client
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
- Besuche eine Chat‑Konversation (eingeloggt) und prüfe, ob das Header‑Ad nach Interaktion oder Consent geladen wird.
|
|
||||||
|
|
||||||
6) A/B‑Test‑Vorschlag (kurz)
|
Technik:
|
||||||
- Variante A: statischer Header (VITE_HEADER_STICKY=false)
|
|
||||||
- Variante B: thin sticky header (VITE_HEADER_STICKY=true)
|
|
||||||
- Metriken: RPM, CTR, Session Length, Bounce Rate. Testlauf: 2 Wochen oder mind. 10k Seitenaufrufe pro Variante.
|
|
||||||
|
|
||||||
Hinweis: Die Anwendung weist jedem neuen Besucher automatisch eine Variante zu (localStorage `ads_ab_variant` = `A` oder `B`).
|
- Adsterra `IFRAME SYNC`
|
||||||
|
- Script-Quelle:
|
||||||
|
- `https://www.highperformanceformat.com/<key>/invoke.js`
|
||||||
|
|
||||||
Kurzbefehle zum Debugging:
|
Aktuell gibt es keine zusaetzliche Consent-, Provider- oder Fallback-Logik mehr in dieser Komponente.
|
||||||
- Aktuelle Variante anzeigen:
|
|
||||||
```js
|
|
||||||
localStorage.getItem('ads_ab_variant')
|
|
||||||
```
|
|
||||||
- Variante zurücksetzen (erneute Zuteilung bei Neuaufruf):
|
|
||||||
```js
|
|
||||||
localStorage.removeItem('ads_ab_variant');
|
|
||||||
location.reload();
|
|
||||||
```
|
|
||||||
|
|
||||||
Events:
|
Wenn neue Banner-Formate kommen, muessen nur die Keys und Groessen in `HeaderAdBanner.vue` angepasst werden.
|
||||||
- `ads:ab-assigned` → Fired when a user is assigned to A or B. Detail: `{variant:'A'|'B'}`.
|
|
||||||
- `ads:load-start` and `ads:loaded` → Fired around ad load with `{provider, variant}`.
|
|
||||||
|
|
||||||
7) Monitoring & Events (optional)
|
|
||||||
- Um schnelle Messungen zu erhalten, feuere ein Custom Event beim Ad‑Load:
|
|
||||||
```js
|
|
||||||
window.dispatchEvent(new CustomEvent('ads:loaded',{detail:{provider: 'propeller'}}))
|
|
||||||
```
|
|
||||||
- Du kannst im Frontend listeners registrieren und diese Events an dein Analytics (Matomo/GA4) weiterleiten.
|
|
||||||
|
|
||||||
Wenn du mir jetzt die echte `VITE_PROP_SCRIPT_URL` und `VITE_PROP_SLOT` gibst, baue ich das Inline‑Snippet exakt so ein, wie Propeller es erwartet. Möchtest du, dass ich auch eine minimale Consent‑UI direkt in `client` ergänze (ein einfacher Banner mit Zustimmen/ablehnen)?
|
|
||||||
|
|||||||
@@ -1,372 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isEnabled" :class="['header-ad-banner', { sticky: finalSticky } ]">
|
<div class="header-ad-banner">
|
||||||
<div ref="adContainer" class="ad-container" />
|
<div
|
||||||
|
ref="adContainer"
|
||||||
|
class="ad-container"
|
||||||
|
:class="{ 'is-mobile': activePlacement.width === 320 }"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 720;
|
||||||
|
const placements = {
|
||||||
|
mobile: {
|
||||||
|
key: 'fb9b5e7f817d40d72943dae0c54eb769',
|
||||||
|
width: 320,
|
||||||
|
height: 50
|
||||||
|
},
|
||||||
|
desktop: {
|
||||||
|
key: '2b658317c1e28b4b4f234d26c8fca28d',
|
||||||
|
width: 468,
|
||||||
|
height: 60
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const adContainer = ref(null);
|
const adContainer = ref(null);
|
||||||
|
const activePlacement = ref(selectPlacement());
|
||||||
|
let resizeTimer = null;
|
||||||
|
|
||||||
// Provider selection via env: 'adsense' or 'propeller'
|
function selectPlacement() {
|
||||||
const provider = (import.meta.env.VITE_AD_PROVIDER || 'propeller').toLowerCase();
|
if (typeof window === 'undefined') {
|
||||||
|
return placements.desktop;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
return window.innerWidth <= MOBILE_BREAKPOINT ? placements.mobile : placements.desktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listenForConsentEvent() {
|
function clearAdContainer() {
|
||||||
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;
|
if (!adContainer.value) return;
|
||||||
try {
|
adContainer.value.innerHTML = '';
|
||||||
// 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}"]`)) {
|
function buildInvokeUrl(key) {
|
||||||
return;
|
return `https://www.highperformanceformat.com/${key}/invoke.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAd() {
|
||||||
|
if (!adContainer.value) return;
|
||||||
|
|
||||||
|
const placement = selectPlacement();
|
||||||
|
activePlacement.value = placement;
|
||||||
|
clearAdContainer();
|
||||||
|
|
||||||
|
window.atOptions = {
|
||||||
|
key: placement.key,
|
||||||
|
format: 'iframe',
|
||||||
|
height: placement.height,
|
||||||
|
width: placement.width,
|
||||||
|
params: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = buildInvokeUrl(placement.key);
|
||||||
|
script.async = true;
|
||||||
|
script.onload = () => {
|
||||||
|
console.log('Adsterra script loaded:', placement.key, `${placement.width}x${placement.height}`);
|
||||||
|
};
|
||||||
|
script.onerror = (error) => {
|
||||||
|
console.warn('Adsterra script failed to load:', placement.key, error);
|
||||||
|
};
|
||||||
|
adContainer.value.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
window.clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(() => {
|
||||||
|
const nextPlacement = selectPlacement();
|
||||||
|
if (nextPlacement.key !== activePlacement.value.key) {
|
||||||
|
renderAd();
|
||||||
}
|
}
|
||||||
|
}, 120);
|
||||||
// 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 {
|
|
||||||
// If the detected node is a SCRIPT, execute it in head and do not append it to placeholder
|
|
||||||
if (node.tagName === 'SCRIPT') {
|
|
||||||
const src = node.getAttribute('src');
|
|
||||||
if (src) {
|
|
||||||
const s2 = document.createElement('script');
|
|
||||||
s2.src = src;
|
|
||||||
s2.async = true;
|
|
||||||
document.head.appendChild(s2);
|
|
||||||
} else if (node.textContent) {
|
|
||||||
const s2 = document.createElement('script');
|
|
||||||
s2.textContent = node.textContent;
|
|
||||||
document.head.appendChild(s2);
|
|
||||||
}
|
|
||||||
node.parentNode && node.parentNode.removeChild(node);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// move iframe/ad node into placeholder (prefer inline iframe if present)
|
|
||||||
placeholder.innerHTML = '';
|
|
||||||
if (iframe && iframe.parentNode) {
|
|
||||||
placeholder.appendChild(iframe);
|
|
||||||
} else if (node.tagName !== 'SCRIPT') {
|
|
||||||
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 () => {
|
onMounted(async () => {
|
||||||
if (!isEnabled.value) return;
|
await nextTick();
|
||||||
|
renderAd();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
// If consent present, autoLoad, or debugForceLoad => load immediately
|
onBeforeUnmount(() => {
|
||||||
if (hasConsent() || autoLoad || debugForceLoad) {
|
window.removeEventListener('resize', handleResize);
|
||||||
await nextTick();
|
window.clearTimeout(resizeTimer);
|
||||||
await loadAdIfNeeded();
|
clearAdContainer();
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header-ad-banner {
|
.header-ad-banner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 90px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -374,35 +105,23 @@ onMounted(async () => {
|
|||||||
box-sizing: border-box;
|
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 {
|
.ad-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 980px;
|
min-height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ad-container iframe,
|
.ad-container.is-mobile {
|
||||||
.ad-container ins {
|
min-height: 50px;
|
||||||
width: 100%;
|
|
||||||
max-width: 970px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
.ad-container :deep(iframe) {
|
||||||
.ad-container iframe,
|
border: 0;
|
||||||
.ad-container ins {
|
display: block;
|
||||||
height: 50px;
|
margin: 0 auto;
|
||||||
max-width: 320px;
|
max-width: 100%;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -125,8 +125,14 @@ echo "✓ Dateien kopiert"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Synchronisiere .env Datei mit Vorlage..."
|
echo "Synchronisiere .env Datei mit Vorlage..."
|
||||||
SESSION_SECRET="$(openssl rand -hex 32)"
|
SESSION_SECRET="$(openssl rand -hex 32)"
|
||||||
|
if [ -f "$TARGET_DIR/.env" ]; then
|
||||||
|
ENV_BACKUP_PATH="$TARGET_DIR/.env.bak"
|
||||||
|
cp -a "$TARGET_DIR/.env" "$ENV_BACKUP_PATH"
|
||||||
|
echo "✓ Backup der bisherigen .env erstellt: $ENV_BACKUP_PATH"
|
||||||
|
fi
|
||||||
"$ENV_MERGE_SCRIPT" "$TARGET_DIR/.env.example" "$TARGET_DIR/.env" "$SESSION_SECRET"
|
"$ENV_MERGE_SCRIPT" "$TARGET_DIR/.env.example" "$TARGET_DIR/.env" "$SESSION_SECRET"
|
||||||
chown $USER:$GROUP "$TARGET_DIR/.env"
|
chown $USER:$GROUP "$TARGET_DIR/.env"
|
||||||
|
chmod 640 "$TARGET_DIR/.env"
|
||||||
echo "✓ .env Datei synchronisiert (bestehende Werte beibehalten)"
|
echo "✓ .env Datei synchronisiert (bestehende Werte beibehalten)"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -141,11 +141,17 @@ fi
|
|||||||
|
|
||||||
log "Synchronisiere .env mit Vorlage"
|
log "Synchronisiere .env mit Vorlage"
|
||||||
session_secret="$(openssl rand -hex 32)"
|
session_secret="$(openssl rand -hex 32)"
|
||||||
|
if [ -f "$APP_DIR/.env" ]; then
|
||||||
|
env_backup_path="$APP_DIR/.env.bak"
|
||||||
|
cp -a "$APP_DIR/.env" "$env_backup_path"
|
||||||
|
log "Backup der bisherigen .env erstellt: $env_backup_path"
|
||||||
|
fi
|
||||||
"$ENV_MERGE_SCRIPT" "$ENV_TEMPLATE" "$APP_DIR/.env" "$session_secret"
|
"$ENV_MERGE_SCRIPT" "$ENV_TEMPLATE" "$APP_DIR/.env" "$session_secret"
|
||||||
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
chown "$RUN_USER:$RUN_GROUP" "$APP_DIR/.env"
|
chown "$RUN_USER:$RUN_GROUP" "$APP_DIR/.env"
|
||||||
fi
|
fi
|
||||||
|
chmod 640 "$APP_DIR/.env"
|
||||||
|
|
||||||
log "Baue Client"
|
log "Baue Client"
|
||||||
run_as_deploy_user npm run build
|
run_as_deploy_user npm run build
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ fi
|
|||||||
tmp_output="$(mktemp)"
|
tmp_output="$(mktemp)"
|
||||||
trap 'rm -f "$tmp_output"' EXIT
|
trap 'rm -f "$tmp_output"' EXIT
|
||||||
|
|
||||||
|
target_mode="640"
|
||||||
|
if [ -f "$env_file" ]; then
|
||||||
|
target_mode="$(stat -c '%a' "$env_file" 2>/dev/null || printf '640')"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "$env_file" ]; then
|
if [ -f "$env_file" ]; then
|
||||||
awk '
|
awk '
|
||||||
function key_from(line, key) {
|
function key_from(line, key) {
|
||||||
@@ -74,4 +79,5 @@ if [ -n "$session_secret" ] && grep -q '^SESSION_SECRET=$' "$tmp_output"; then
|
|||||||
sed -i "s/^SESSION_SECRET=$/SESSION_SECRET=$session_secret/" "$tmp_output"
|
sed -i "s/^SESSION_SECRET=$/SESSION_SECRET=$session_secret/" "$tmp_output"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv "$tmp_output" "$env_file"
|
cat "$tmp_output" > "$env_file"
|
||||||
|
chmod "$target_mode" "$env_file"
|
||||||
|
|||||||
Reference in New Issue
Block a user