diff --git a/README.md b/README.md
index 0cd349c..cf5b4bb 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ npm run dev # http://127.0.0.1:3000
| `ADMIN_EMAILS` / `ADMIN_GMLEVEL` | Acceso al panel de administración |
| `CRON_SECRET` | Protege el endpoint de reconciliación de SumUp |
| `NEXT_PUBLIC_GA_ID` | Google Analytics 4 (p. ej. `G-XXXXXXXXXX`). **Vacío o sin poner = analítica desactivada**, no se carga nada. Además, aunque esté puesta, solo se carga si el visitante acepta la categoría «Analíticas» del banner de cookies. Al ser `NEXT_PUBLIC_*` se incrusta en el build: activar o desactivar exige `npm run build` + reinicio |
+| `MONETAG_SRC` / `MONETAG_ZONE` | Script de anuncios de Monetag (MultiTag). Salen del panel: Sites → canal → Get Tag; el `src` varía por zona. **Cualquiera de las dos vacía = anuncios desactivados**. Aunque estén puestas, solo se cargan si el visitante acepta la categoría «Marketing» del banner y pasa el filtro de `lib/ads.ts`. El push necesita además `public/sw.js` del panel. Se leen en el servidor: activar/desactivar **sin rebuild** |
| `ADSENSE_CLIENT_ID` | ID de editor de Google AdSense (`ca-pub-...`). Emite `` en el `
` de todas las páginas. **Vacío o sin poner = etiqueta desactivada**. No la ven ni los admins ni las cuentas de rango 2+ o que hayan pagado alguna vez (ver `components/AdSense.tsx`). Se lee en el servidor en cada petición: activar o desactivar es tocar el `.env` + reinicio, **sin rebuild** |
## 3. Producción
diff --git a/web-next/app/[locale]/layout.tsx b/web-next/app/[locale]/layout.tsx
index 15b02b5..28105bd 100644
--- a/web-next/app/[locale]/layout.tsx
+++ b/web-next/app/[locale]/layout.tsx
@@ -11,6 +11,7 @@ import { Footer } from '@/components/Footer'
import { CookieBanner } from '@/components/CookieConsent'
import { Analytics } from '@/components/Analytics'
import { AdSense } from '@/components/AdSense'
+import { Monetag } from '@/components/Monetag'
import '../globals.css'
export function generateStaticParams() {
@@ -100,6 +101,9 @@ export default async function LocaleLayout({
{/* Analítica: solo si hay NEXT_PUBLIC_GA_ID y el visitante ha aceptado
la categoría «Analíticas» del banner. Ver components/Analytics.tsx. */}
+ {/* Anuncios de Monetag: solo si hay MONETAG_SRC + MONETAG_ZONE, el visitante
+ no es admin ni ha pagado, y ha aceptado «Marketing». Ver components/Monetag.tsx. */}
+
{/* Tooltips de wowhead (WotLK) en todos los enlaces .../wowhead.com/... y
elementos con data-wowhead. Funciona con contenido añadido por React
(carrito, búsquedas) porque delega el hover en el documento. */}
diff --git a/web-next/components/AdSense.tsx b/web-next/components/AdSense.tsx
index ef61b23..7bddbaf 100644
--- a/web-next/components/AdSense.tsx
+++ b/web-next/components/AdSense.tsx
@@ -1,42 +1,25 @@
-import { getSession } from '@/lib/session'
-import { isAdmin } from '@/lib/admin'
-import { getAccountRankInfo } from '@/lib/account'
+import { shouldServeAds } from '@/lib/ads'
/**
- * Etiqueta de verificación de Google AdSense, con TRES interruptores:
+ * Etiqueta de verificación de Google AdSense, con DOS interruptores:
*
* 1. `ADSENSE_CLIENT_ID` en el .env. Vacío o sin poner = no se emite nada. Es el
* interruptor del administrador. No lleva prefijo NEXT_PUBLIC_ a propósito:
* esto se renderiza en el servidor (el layout ya es dinámico porque
- * lee la sesión), así que no hace falta exponerlo al bundle del cliente.
- * 2. Admin (allowlist ADMIN_EMAILS o gmlevel de AzerothCore): sin etiqueta, para
- * que la navegación del staff no cuente como tráfico con anuncios.
- * 3. Rango de la cuenta (ns-ranks, ver getAccountRank en lib/account.ts) y pagos:
- * solo ve anuncios el rango 1 (Newbie, el de por defecto) que ADEMÁS nunca ha
- * pagado. Son dos cortes distintos a propósito: cualquier pago confirmado por
- * Stripe o SumUp los quita por mínimo que sea, y el mínimo de SumUp (0,20 € =
- * 20 PD) no llega a los 100 PD del rango 2, así que mirar solo el rango dejaría
- * con anuncios a alguien que ya ha pagado.
- *
- * Ambas comprobaciones cortan en seco sin tocar la BD si no hay sesión, así que el
- * visitante anónimo (el caso normal) no paga ninguna consulta extra.
+ * lee la sesión), así que no hace falta exponerlo al bundle del cliente ni
+ * rebuildear para activarlo o desactivarlo.
+ * 2. El filtro común de publicidad (`shouldServeAds`, ver lib/ads.ts): fuera para
+ * admins y para quien sea rango 2+ o haya pagado alguna vez.
*
* Ojo: esto es solo la etiqueta de verificación de la cuenta, no carga adsbygoogle.js
- * ni escribe cookies, por eso no pasa por el banner de consentimiento. El día que se
- * añada el script de anuncios sí tendrá que ir detrás de la categoría «Marketing»
- * (useConsent('marketing'), ver components/Analytics.tsx).
+ * ni escribe cookies, por eso no pasa por el banner de consentimiento (el script de
+ * anuncios sí lo hace, ver components/Monetag.tsx).
*/
export async function AdSense() {
const client = process.env.ADSENSE_CLIENT_ID
if (!client) return null
- const session = await getSession()
- if (await isAdmin(session)) return null
-
- if (session.accountId) {
- const { level, donatedPD } = await getAccountRankInfo(session.accountId)
- if (level >= 2 || donatedPD > 0) return null
- }
+ if (!(await shouldServeAds())) return null
return
}
diff --git a/web-next/components/Monetag.tsx b/web-next/components/Monetag.tsx
new file mode 100644
index 0000000..0c78a44
--- /dev/null
+++ b/web-next/components/Monetag.tsx
@@ -0,0 +1,32 @@
+import { shouldServeAds } from '@/lib/ads'
+import { MonetagScript } from './MonetagScript'
+
+/**
+ * Script de anuncios de Monetag (MultiTag: popunder + push + in-page push + vignette).
+ *
+ * TRES interruptores:
+ *
+ * 1. `MONETAG_SRC` y `MONETAG_ZONE` en el .env. Si falta cualquiera de los dos no se
+ * carga nada: ese es el interruptor de encendido/apagado. Los dos valores salen
+ * del panel de Monetag (Sites → tu canal → Get Tag) y son propios de cada zona,
+ * por eso el `src` también es variable y no está fijado en el código.
+ * Sin prefijo NEXT_PUBLIC_: se leen en el servidor y se pasan como props, así que
+ * activar o desactivar es tocar el .env + reinicio, SIN rebuild.
+ * 2. El filtro común de publicidad (`shouldServeAds`, ver lib/ads.ts): fuera para
+ * admins y para quien sea rango 2+ o haya pagado alguna vez.
+ * 3. El consentimiento «Marketing» del visitante, en (cliente).
+ *
+ * OJO con el push de MultiTag: además de esto necesita un `sw.js` servido desde la
+ * RAÍZ del dominio (https://www.nightspire.gg/sw.js). Se descarga del panel de
+ * Monetag y se deja en `public/sw.js`, que Next sirve tal cual en la raíz. Sin ese
+ * fichero el resto de formatos funcionan, pero las notificaciones push no.
+ */
+export async function Monetag() {
+ const src = process.env.MONETAG_SRC
+ const zone = process.env.MONETAG_ZONE
+ if (!src || !zone) return null
+
+ if (!(await shouldServeAds())) return null
+
+ return
+}
diff --git a/web-next/components/MonetagScript.tsx b/web-next/components/MonetagScript.tsx
new file mode 100644
index 0000000..07f3390
--- /dev/null
+++ b/web-next/components/MonetagScript.tsx
@@ -0,0 +1,31 @@
+'use client'
+
+import Script from 'next/script'
+import { useConsent } from './CookieConsent'
+
+/**
+ * Parte de cliente del script de Monetag: solo existe para mirar el consentimiento,
+ * que vive en el navegador (localStorage + evento `ns:consentchange`).
+ *
+ * A diferencia de la etiqueta de AdSense, esto SÍ pasa por el banner: MultiTag trae
+ * popunder, push, in-page push y vignette, y todos escriben cookies o piden permisos
+ * al navegador. Cargarlo sin mirar la categoría «Marketing» convertiría el banner en
+ * mentira y, con visitantes en la UE, en un problema legal.
+ *
+ * `useConsent` reacciona en caliente: entra en cuanto se acepta y deja de renderizarse
+ * si se revoca, sin recargar. Arranca en `false`, así que hasta confirmar el
+ * consentimiento no se carga nada.
+ */
+export function MonetagScript({ src, zone }: { src: string; zone: string }) {
+ const consent = useConsent('marketing')
+ if (!consent) return null
+
+ return (
+
+ )
+}
diff --git a/web-next/lib/ads.ts b/web-next/lib/ads.ts
new file mode 100644
index 0000000..8594d37
--- /dev/null
+++ b/web-next/lib/ads.ts
@@ -0,0 +1,35 @@
+import { getSession } from './session'
+import { isAdmin } from './admin'
+import { getAccountRankInfo } from './account'
+
+/**
+ * ¿A este visitante se le sirven anuncios?
+ *
+ * Criterio único para TODA la publicidad (la etiqueta de AdSense y el script de
+ * Monetag), para que no se puedan desincronizar:
+ *
+ * - Admin (allowlist ADMIN_EMAILS o gmlevel de AzerothCore): nunca. La navegación
+ * del staff no debe contar como tráfico con anuncios.
+ * - Rango 2+ (100 PD, ver getAccountRank en lib/account.ts) o cualquier pago
+ * confirmado por Stripe/SumUp: nunca. Los anuncios son la contrapartida de no
+ * haber donado.
+ * - El resto (anónimos y rango 1 sin pagos): sí.
+ *
+ * Los dos cortes del segundo punto son distintos a propósito: el mínimo de SumUp
+ * (0,20 € = 20 PD) no llega a los 100 PD del rango 2, así que mirar solo el rango
+ * dejaría con anuncios a alguien que ya ha pagado.
+ *
+ * Corta en seco sin tocar la BD si no hay sesión, así que el visitante anónimo (el
+ * caso normal) no paga ninguna consulta extra.
+ */
+export async function shouldServeAds(): Promise {
+ const session = await getSession()
+ if (await isAdmin(session)) return false
+
+ if (session.accountId) {
+ const { level, donatedPD } = await getAccountRankInfo(session.accountId)
+ if (level >= 2 || donatedPD > 0) return false
+ }
+
+ return true
+}