From 62f5914b04fc9f05e9cd835b174303b44f0c265b Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 18 Jun 2026 08:33:42 +0200 Subject: [PATCH] =?UTF-8?q?=C3=84nderung=20der=20werbung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ADSENSE.md | 136 +----- .../java/de/ypchat/android/ui/YpChatRoot.kt | 146 +++++- client/ADS-INTEGRATION.md | 87 +--- client/src/components/HeaderAdBanner.vue | 453 ++++-------------- deploy-to-opt.sh | 6 + scripts/actualize-singlechat.sh | 6 + scripts/merge-env-template.sh | 8 +- 7 files changed, 274 insertions(+), 568 deletions(-) diff --git a/ADSENSE.md b/ADSENSE.md index fd318a5..0918d2a 100644 --- a/ADSENSE.md +++ b/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) -- 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) +Implementierung: -Aktiv wird der Banner nur mit: +- [client/src/components/HeaderAdBanner.vue](/mnt/share/torsten/Programs/SingleChat/client/src/components/HeaderAdBanner.vue) -- `VITE_ADSENSE_CLIENT` -- `VITE_ADSENSE_HEADER_SLOT` +Funktionsweise: -## 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 - -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. +Wird spaeter ein anderer Werbeanbieter oder ein weiteres Adsterra-Format verwendet, sollte diese Datei entsprechend aktualisiert werden. diff --git a/android/app/src/main/java/de/ypchat/android/ui/YpChatRoot.kt b/android/app/src/main/java/de/ypchat/android/ui/YpChatRoot.kt index b025f17..800e776 100644 --- a/android/app/src/main/java/de/ypchat/android/ui/YpChatRoot.kt +++ b/android/app/src/main/java/de/ypchat/android/ui/YpChatRoot.kt @@ -4,7 +4,12 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.Color as AndroidColor 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.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -66,6 +71,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource @@ -128,6 +134,8 @@ private val Primary500 = Color(0xFF3D8654) private val Primary100 = Color(0xFFE7F1EA) private val Danger = Color(0xFFA24040) 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 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)) { LandingIntroCard() + AndroidHeaderAdBanner() Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), @@ -333,17 +342,20 @@ private fun ChatShell(state: ChatState, viewModel: ChatViewModel) { } } ) { padding -> - Box(modifier = Modifier.padding(padding).fillMaxSize()) { - if (state.currentConversation != null) { - ChatScreen(state, viewModel) - } else { - when (selectedTab) { - AppTab.Online -> UserListScreen(state.users, state.countries, onUserClick = viewModel::openConversation) - AppTab.Search -> SearchScreen(state, viewModel) - AppTab.Inbox -> InboxScreen(state, viewModel) - AppTab.History -> HistoryScreen(state, viewModel) - AppTab.Console -> ConsoleScreen(state, viewModel) - AppTab.More -> MoreScreen(state, viewModel, moreSection) { moreSection = it } + Column(modifier = Modifier.padding(padding).fillMaxSize()) { + AndroidHeaderAdBanner() + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + if (state.currentConversation != null) { + ChatScreen(state, viewModel) + } else { + when (selectedTab) { + AppTab.Online -> UserListScreen(state.users, state.countries, onUserClick = viewModel::openConversation) + AppTab.Search -> SearchScreen(state, viewModel) + AppTab.Inbox -> InboxScreen(state, viewModel) + 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, countries: List, } } +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 = """ + + + + + + + + + + + + +""".trimIndent() + @Composable private fun UserRow(user: UserDto, countries: List, onUserClick: (String) -> Unit) { val countryLabel = remember(user.country, user.isoCountryCode, countries) { displayCountryName(user, countries) } diff --git a/client/ADS-INTEGRATION.md b/client/ADS-INTEGRATION.md index 5d4026e..49b3f87 100644 --- a/client/ADS-INTEGRATION.md +++ b/client/ADS-INTEGRATION.md @@ -1,78 +1,31 @@ -Aktuell habe ich das In‑Page Push Snippet, das du gepostet hast, als Default eingebaut: -```html - -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`) +# Header-Ad Integration -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. -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. +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/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. -Kurz: Anleitung zur Aktivierung des Header‑Ads (Propeller / AdSense) +Verwendete Placements: -1) Umgebungsvariablen -- Lege in deiner lokalen Umgebung (z. B. `.env.local`) folgende Variablen an: - - `VITE_AD_PROVIDER=propeller` # oder `adsense` - - `VITE_PROP_SCRIPT_URL=https://example-propeller.example/ads.js` # Propeller: Script/Endpoint - - `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 +- Mobile: `320x50` + - Key: `fb9b5e7f817d40d72943dae0c54eb769` +- Desktop: `468x60` + - Key: `2b658317c1e28b4b4f234d26c8fca28d` -2) Was ich bereits implementiert habe -- `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) +Die Komponente waehlt automatisch anhand der Viewport-Breite: -3) Consent‑Handling (was du tun musst) -- Der Code wartet auf einen Consent-Wert in `localStorage` unter dem Key `ads_consent` (Wert `'true'`). -- 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')); - ``` +- bis `720px`: `320x50` +- ab `721px`: `468x60` -4) Propeller‑Integration -- 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). +Einbauorte: -5) Test & Dev -- Dev starten: - ```bash - 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. +- [client/src/components/HeaderAdBanner.vue](/mnt/share/torsten/Programs/SingleChat/client/src/components/HeaderAdBanner.vue) +- [client/src/views/ChatView.vue](/mnt/share/torsten/Programs/SingleChat/client/src/views/ChatView.vue) +- weitere SEO-/Info-Seiten mit `HeaderAdBanner` -6) A/B‑Test‑Vorschlag (kurz) -- 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. +Technik: -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//invoke.js` -Kurzbefehle zum Debugging: -- Aktuelle Variante anzeigen: - ```js - localStorage.getItem('ads_ab_variant') - ``` -- Variante zurücksetzen (erneute Zuteilung bei Neuaufruf): - ```js - localStorage.removeItem('ads_ab_variant'); - location.reload(); - ``` +Aktuell gibt es keine zusaetzliche Consent-, Provider- oder Fallback-Logik mehr in dieser Komponente. -Events: -- `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)? +Wenn neue Banner-Formate kommen, muessen nur die Keys und Groessen in `HeaderAdBanner.vue` angepasst werden. diff --git a/client/src/components/HeaderAdBanner.vue b/client/src/components/HeaderAdBanner.vue index 97243a2..8a3d7ae 100644 --- a/client/src/components/HeaderAdBanner.vue +++ b/client/src/components/HeaderAdBanner.vue @@ -1,372 +1,103 @@ diff --git a/deploy-to-opt.sh b/deploy-to-opt.sh index 75cecfa..ecbbfa3 100755 --- a/deploy-to-opt.sh +++ b/deploy-to-opt.sh @@ -125,8 +125,14 @@ echo "✓ Dateien kopiert" echo "" echo "Synchronisiere .env Datei mit Vorlage..." 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" chown $USER:$GROUP "$TARGET_DIR/.env" +chmod 640 "$TARGET_DIR/.env" echo "✓ .env Datei synchronisiert (bestehende Werte beibehalten)" echo "" diff --git a/scripts/actualize-singlechat.sh b/scripts/actualize-singlechat.sh index a6c513f..1ea975b 100755 --- a/scripts/actualize-singlechat.sh +++ b/scripts/actualize-singlechat.sh @@ -141,11 +141,17 @@ fi log "Synchronisiere .env mit Vorlage" 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" if [ "$(id -u)" -eq 0 ]; then chown "$RUN_USER:$RUN_GROUP" "$APP_DIR/.env" fi +chmod 640 "$APP_DIR/.env" log "Baue Client" run_as_deploy_user npm run build diff --git a/scripts/merge-env-template.sh b/scripts/merge-env-template.sh index 5c6dd10..68379f2 100644 --- a/scripts/merge-env-template.sh +++ b/scripts/merge-env-template.sh @@ -19,6 +19,11 @@ fi tmp_output="$(mktemp)" 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 awk ' 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" fi -mv "$tmp_output" "$env_file" +cat "$tmp_output" > "$env_file" +chmod "$target_mode" "$env_file"