- proposal.md: define problem and expected changes - design.md: 9 technical decisions with rationale - spec.md: complete GIVEN/WHEN/THEN scenarios - tasks.md: implementation tasks with dependencies Features specified: - 7 ad insertion locations (H2, H3, p, img, lists, blockquotes, tables) - 5 density modes (legacy, conservative, balanced, aggressive, custom) - 2 selection strategies (position vs priority) - Deterministic probability with daily seed - Backward compatibility with legacy fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
472 lines
16 KiB
Markdown
472 lines
16 KiB
Markdown
# Design: Sistema Avanzado de In-Content Ads
|
|
|
|
## Context
|
|
|
|
El sitio analisisdepreciosunitarios.com ha experimentado una reduccion del 50% en ingresos de AdSense. El analisis indica que el sistema actual solo inserta anuncios despues de parrafos, desperdiciando oportunidades de insercion despues de otros elementos estructurales del contenido (encabezados, imagenes, listas, etc.).
|
|
|
|
### Stakeholders
|
|
- Propietario del sitio (monetizacion)
|
|
- Usuarios (experiencia de lectura)
|
|
- Google AdSense (politicas de densidad)
|
|
|
|
### Constraints
|
|
- Politicas de AdSense: No mas de 3 anuncios visibles simultaneamente en viewport
|
|
- UX: Mantener legibilidad del contenido
|
|
- Performance: No afectar tiempos de carga (lazy load existente)
|
|
|
|
## Goals / Non-Goals
|
|
|
|
### Goals
|
|
- Incrementar ubicaciones potenciales de anuncios de ~8 a ~15-20
|
|
- Proporcionar control granular por tipo de elemento
|
|
- Mantener cumplimiento con politicas de AdSense
|
|
- Mejorar ingresos sin sacrificar UX drasticamente
|
|
|
|
### Non-Goals
|
|
- No implementar insercion dentro de parrafos (mid-paragraph)
|
|
- No implementar anuncios de video
|
|
- No cambiar el sistema de delay/lazy load existente
|
|
|
|
---
|
|
|
|
## Decisions
|
|
|
|
### Decision 1: Tipos de ubicacion soportados
|
|
|
|
**Seleccionados:**
|
|
- Despues de parrafos (existente, mejorado)
|
|
- Despues de encabezados H2
|
|
- Despues de encabezados H3
|
|
- Despues de imagenes/figuras
|
|
- Despues de blockquotes
|
|
- Despues de listas (ul/ol completadas)
|
|
- Despues de tablas
|
|
|
|
**Rationale:** Estos elementos representan pausas naturales en la lectura donde un anuncio es menos intrusivo.
|
|
|
|
### Decision 2: Sistema de prioridades (CORREGIDO)
|
|
|
|
```
|
|
Prioridad (valores fijos, no configurables):
|
|
| Tipo | Prioridad | Justificacion |
|
|
|-------------------|-----------|----------------------------------|
|
|
| Despues de H2 | 10 | Ruptura tematica mayor |
|
|
| Despues de parrafos | 8 | Ubicacion tradicional, probada |
|
|
| Despues de H3 | 7 | Ruptura tematica menor |
|
|
| Despues de imagenes | 6 | Pausa visual natural |
|
|
| Despues de listas | 5 | Fin de enumeracion |
|
|
| Despues de blockquotes | 4 | Fin de cita |
|
|
| Despues de tablas | 3 | Fin de datos tabulares |
|
|
```
|
|
|
|
**Nota:** El orden numerico refleja la prioridad real. H2 > parrafos > H3 > imagenes.
|
|
|
|
### Decision 3: Modos de densidad
|
|
|
|
| Modo | Max Ads | Espaciado Min | Ubicaciones Activas por Defecto |
|
|
|------|---------|---------------|--------------------------------|
|
|
| Legacy | (usa config anterior) | (usa config anterior) | Solo parrafos |
|
|
| Conservador | 5 | 5 elementos | H2, parrafos |
|
|
| Balanceado | 8 | 3 elementos | H2, H3, parrafos, imagenes |
|
|
| Agresivo | 15 | 2 elementos | Todas |
|
|
| Personalizado | Configurable | Configurable | Configurable |
|
|
|
|
### Decision 4: Estrategia de campos legacy
|
|
|
|
**Problema:** Existen campos en el grupo `behavior` que se solapan con los nuevos:
|
|
|
|
| Campo Legacy | Campo Nuevo | Estrategia |
|
|
|--------------|-------------|------------|
|
|
| post_content_enabled | N/A | Se mantiene para modo legacy |
|
|
| post_content_max_ads (1-8) | incontent_max_total_ads (1-15) | Deprecacion suave |
|
|
| post_content_min_paragraphs_between | incontent_min_spacing | Deprecacion suave |
|
|
| post_content_random_mode | Probabilidades por tipo | Mapeo: true → 75% |
|
|
| post_content_after_paragraphs | N/A | Solo aplica en modo legacy |
|
|
|
|
**Solucion elegida:** Deprecacion suave con modo "legacy"
|
|
- Si `incontent_mode == "legacy"`: usar campos del grupo `behavior`
|
|
- Si `incontent_mode != "legacy"`: usar campos de `incontent_advanced`
|
|
- Mostrar banner de migracion en admin
|
|
|
|
### Decision 5: Enfoque de parsing HTML
|
|
|
|
**Opcion A: DOMDocument**
|
|
```php
|
|
$dom = new DOMDocument();
|
|
$dom->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'));
|
|
```
|
|
- Pros: Parsing robusto, manejo correcto de anidamiento
|
|
- Contras: Puede modificar HTML, mas lento
|
|
|
|
**Opcion B: Regex multiple (SELECCIONADA)**
|
|
```php
|
|
preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE)
|
|
```
|
|
- Pros: Rapido, no modifica HTML
|
|
- Contras: No detecta contexto de anidamiento
|
|
|
|
**Justificacion:** Regex es suficiente para el caso de uso. El contexto de `<img>` dentro de `<figure>` se resuelve con validacion adicional.
|
|
|
|
### Decision 6: Definicion de "Elemento de Bloque Contable"
|
|
|
|
Para efectos de espaciado y conteo, un "elemento" se define como:
|
|
|
|
| Tag | Cuenta | Notas |
|
|
|-----|--------|-------|
|
|
| `</p>` | SI | Parrafo |
|
|
| `</h2>`, `</h3>` | SI | Encabezados (H4 no soportado) |
|
|
| `</figure>` | SI | Contenedor de imagen |
|
|
| `</ul>`, `</ol>` | SI | Listas (contenedor, no items) |
|
|
| `</table>` | SI | Tabla (contenedor) |
|
|
| `</blockquote>` | SI | Cita en bloque |
|
|
| `<img>` standalone | SI | Solo si NO esta dentro de `<figure>` |
|
|
| `</li>`, `</tr>`, `</td>` | NO | Elementos internos |
|
|
| `</div>` | NO | Divs genericos |
|
|
|
|
### Decision 7: Algoritmo de insercion
|
|
|
|
El algoritmo sigue 6 pasos secuenciales:
|
|
|
|
```
|
|
PASO 1: ESCANEO
|
|
→ Detectar todos los tags de cierre de bloques contables
|
|
→ Registrar: {posicion, tipo, indice}
|
|
|
|
PASO 2: FILTRADO POR CONFIGURACION
|
|
→ Eliminar ubicaciones con enabled=false
|
|
|
|
PASO 3: PROBABILIDAD DETERMINISTICA
|
|
→ Seed: crc32(post_id . date('Y-m-d'))
|
|
→ mt_srand(seed) + mt_rand(1, 100)
|
|
→ Eliminar si rand > probabilidad
|
|
|
|
PASO 4: FILTRADO POR ESPACIADO
|
|
→ Iterar en orden DOM
|
|
→ Eliminar si distancia < min_spacing
|
|
|
|
PASO 5: LIMITE Y PRIORIDAD
|
|
→ Si count > max_total_ads:
|
|
→ Ordenar por prioridad DESC
|
|
→ Tomar primeros N
|
|
→ Reordenar por posicion DOM
|
|
|
|
PASO 6: INSERCION
|
|
→ Insertar HTML de ad despues de cada tag
|
|
```
|
|
|
|
### Decision 8: Probabilidad deterministica
|
|
|
|
**Problema:** `rand()` genera posiciones diferentes en cada request, afectando cache.
|
|
|
|
**Solucion:**
|
|
```php
|
|
$seed = crc32($post_id . date('Y-m-d'));
|
|
mt_srand($seed);
|
|
// mt_rand() ahora es determinístico por día
|
|
```
|
|
|
|
**Beneficios:**
|
|
- Mismo post = mismas posiciones durante el dia
|
|
- Cache de pagina funciona correctamente
|
|
- Al dia siguiente, posiciones cambian (variedad)
|
|
|
|
### Decision 9: Estrategia de seleccion configurable
|
|
|
|
**Problema:** El orden entre espaciado y prioridad afecta qué ubicaciones sobreviven cuando hay conflictos.
|
|
|
|
**Solucion:** Campo `incontent_priority_mode` con dos opciones:
|
|
|
|
| Modo | Orden de pasos | Resultado |
|
|
|------|----------------|-----------|
|
|
| `position` | Espaciado → Prioridad | Distribucion uniforme, respeta orden DOM |
|
|
| `priority` | Prioridad → Espaciado | Maximiza valor, H2/H3 siempre ganan |
|
|
|
|
**Algoritmo segun modo:**
|
|
|
|
```
|
|
SI incontent_priority_mode == "position":
|
|
PASO 4: Filtrar por espaciado (orden DOM)
|
|
PASO 5: Ordenar por prioridad, tomar max_total_ads
|
|
|
|
SI incontent_priority_mode == "priority":
|
|
PASO 4: Ordenar por prioridad DESC
|
|
PASO 5: Iterar en orden de prioridad, eliminar si viola espaciado
|
|
Tomar max_total_ads
|
|
```
|
|
|
|
**Default:** `position` (comportamiento mas predecible y uniforme)
|
|
|
|
---
|
|
|
|
## Estructura de UI (FormBuilder)
|
|
|
|
```html
|
|
<div class="card shadow-sm mb-3" style="border-left: 4px solid #0d6efd;">
|
|
<div class="card-body">
|
|
<h5 class="fw-bold mb-3">
|
|
<i class="bi bi-body-text me-2"></i>
|
|
In-Content Ads Avanzado
|
|
<span class="badge bg-success ms-2">Nuevo</span>
|
|
</h5>
|
|
|
|
<!-- Indicador de densidad -->
|
|
<div id="densityIndicator" class="alert alert-info small mb-3">
|
|
Densidad estimada: <strong>Media</strong> <span class="badge bg-warning">~6 ads</span>
|
|
</div>
|
|
|
|
<!-- Selector de modo -->
|
|
<select class="form-select mb-4" name="incontent_mode">
|
|
<option value="legacy">Legacy (config anterior)</option>
|
|
<option value="conservative">Conservador</option>
|
|
<option value="balanced" selected>Balanceado</option>
|
|
<option value="aggressive">Agresivo</option>
|
|
<option value="custom">Personalizado</option>
|
|
</select>
|
|
|
|
<!-- Subseccion: Ubicaciones -->
|
|
<details class="mb-3 border rounded" open>
|
|
<summary class="p-3 bg-light fw-bold">Ubicaciones por Elemento</summary>
|
|
<div class="p-3">
|
|
<!-- Toggle + probabilidad por tipo -->
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Subseccion: Limites -->
|
|
<details class="mb-3 border rounded">
|
|
<summary class="p-3 bg-light fw-bold">Limites y Espaciado</summary>
|
|
<div class="p-3">
|
|
<!-- max_total_ads, min_spacing -->
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Warning densidad alta -->
|
|
<div id="highDensityWarning" class="alert alert-warning small d-none">
|
|
Densidad alta puede afectar UX y violar politicas de AdSense.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Schema JSON Completo
|
|
|
|
**Nota sobre formato de options:**
|
|
- **Array** `["25", "50", "75", "100"]`: Cuando value y label son identicos
|
|
- **Objeto** `{"2": "2 elementos"}`: Cuando label difiere del value
|
|
|
|
```json
|
|
{
|
|
"incontent_advanced": {
|
|
"label": "In-Content Ads Avanzado",
|
|
"priority": 69,
|
|
"fields": {
|
|
"incontent_mode": {
|
|
"type": "select",
|
|
"label": "Modo de densidad",
|
|
"default": "legacy",
|
|
"editable": true,
|
|
"options": {
|
|
"legacy": "Legacy (config anterior)",
|
|
"conservative": "Conservador (max 5, espaciado 5)",
|
|
"balanced": "Balanceado (max 8, espaciado 3)",
|
|
"aggressive": "Agresivo (max 15, espaciado 2)",
|
|
"custom": "Personalizado"
|
|
},
|
|
"description": "Presets que ajustan limites y ubicaciones. Legacy usa campos del grupo Ubicaciones en Posts."
|
|
},
|
|
"incontent_after_h2_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de H2",
|
|
"default": true,
|
|
"editable": true,
|
|
"description": "Insertar anuncios despues de encabezados H2"
|
|
},
|
|
"incontent_after_h2_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad H2",
|
|
"default": "100",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"],
|
|
"description": "Porcentaje de probabilidad de insercion"
|
|
},
|
|
"incontent_after_h3_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de H3",
|
|
"default": true,
|
|
"editable": true,
|
|
"description": "Insertar anuncios despues de encabezados H3"
|
|
},
|
|
"incontent_after_h3_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad H3",
|
|
"default": "50",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_after_paragraphs_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de parrafos",
|
|
"default": true,
|
|
"editable": true,
|
|
"description": "Insertar anuncios despues de parrafos"
|
|
},
|
|
"incontent_after_paragraphs_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad parrafos",
|
|
"default": "75",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_after_images_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de imagenes",
|
|
"default": true,
|
|
"editable": true,
|
|
"description": "Insertar despues de figure o img standalone"
|
|
},
|
|
"incontent_after_images_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad imagenes",
|
|
"default": "75",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_after_lists_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de listas",
|
|
"default": false,
|
|
"editable": true,
|
|
"description": "Insertar despues de ul/ol (minimo 3 items)"
|
|
},
|
|
"incontent_after_lists_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad listas",
|
|
"default": "50",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_after_blockquotes_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de blockquotes",
|
|
"default": false,
|
|
"editable": true,
|
|
"description": "Insertar despues de citas en bloque"
|
|
},
|
|
"incontent_after_blockquotes_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad blockquotes",
|
|
"default": "50",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_after_tables_enabled": {
|
|
"type": "boolean",
|
|
"label": "Despues de tablas",
|
|
"default": false,
|
|
"editable": true,
|
|
"description": "Insertar despues de tablas"
|
|
},
|
|
"incontent_after_tables_probability": {
|
|
"type": "select",
|
|
"label": "Probabilidad tablas",
|
|
"default": "50",
|
|
"editable": true,
|
|
"options": ["25", "50", "75", "100"]
|
|
},
|
|
"incontent_max_total_ads": {
|
|
"type": "select",
|
|
"label": "Maximo total de ads",
|
|
"default": "8",
|
|
"editable": true,
|
|
"options": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"],
|
|
"description": "Cantidad maxima de anuncios in-content por post"
|
|
},
|
|
"incontent_min_spacing": {
|
|
"type": "select",
|
|
"label": "Espaciado minimo",
|
|
"default": "3",
|
|
"editable": true,
|
|
"options": {
|
|
"2": "2 elementos",
|
|
"3": "3 elementos",
|
|
"4": "4 elementos",
|
|
"5": "5 elementos",
|
|
"6": "6 elementos"
|
|
},
|
|
"description": "Minimo de elementos de bloque entre anuncios"
|
|
},
|
|
"incontent_format": {
|
|
"type": "select",
|
|
"label": "Formato de ads",
|
|
"default": "in-article",
|
|
"editable": true,
|
|
"options": {
|
|
"in-article": "In-Article (fluid)",
|
|
"auto": "Auto (responsive)"
|
|
},
|
|
"description": "Formato de anuncio para ubicaciones in-content"
|
|
},
|
|
"incontent_priority_mode": {
|
|
"type": "select",
|
|
"label": "Estrategia de seleccion",
|
|
"default": "position",
|
|
"editable": true,
|
|
"options": {
|
|
"position": "Por posicion (distribucion uniforme)",
|
|
"priority": "Por prioridad (maximizar H2/H3)"
|
|
},
|
|
"description": "Como resolver conflictos cuando dos ubicaciones estan muy cerca. 'Por posicion' respeta el orden del contenido. 'Por prioridad' favorece ubicaciones de mayor valor (H2 sobre parrafos)."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Risks / Trade-offs
|
|
|
|
### Risk 1: Violacion de politicas de AdSense
|
|
- **Probabilidad**: Media
|
|
- **Impacto**: Alto (suspension de cuenta)
|
|
- **Mitigacion**: Indicador de densidad en admin, warning para >10 ads
|
|
|
|
### Risk 2: Degradacion de UX
|
|
- **Probabilidad**: Media-Alta
|
|
- **Impacto**: Medio (usuarios abandonan)
|
|
- **Mitigacion**: Modo conservador como default inicial, preview de densidad
|
|
|
|
### Risk 3: Conflicto con contenido corto
|
|
- **Probabilidad**: Media
|
|
- **Impacto**: Bajo
|
|
- **Mitigacion**: Campo existente `min_content_length` ya maneja esto
|
|
|
|
### Risk 4: Complejidad de migracion
|
|
- **Probabilidad**: Baja
|
|
- **Impacto**: Medio
|
|
- **Mitigacion**: Default "legacy" preserva comportamiento actual
|
|
|
|
---
|
|
|
|
## Migration Plan
|
|
|
|
1. **Fase 1 - Schema**: Agregar grupo `incontent_advanced` con default "legacy"
|
|
2. **Fase 2 - Sync**: `wp roi-theme sync-component adsense-placement`
|
|
3. **Fase 3 - FormBuilder**: Nueva UI con banner de migracion
|
|
4. **Fase 4 - Renderer**: Implementar ContentAdInjector con algoritmo de 6 pasos
|
|
5. **Fase 5 - Testing**: Validar en posts con contenido variado
|
|
6. **Fase 6 - Deploy**: Default "legacy", usuarios migran manualmente
|
|
|
|
### Rollback
|
|
- Cambiar `incontent_mode` a "legacy" restaura comportamiento anterior
|
|
- No hay cambios destructivos en BD
|
|
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
1. ~~¿Se deberia agregar un preview en vivo de donde apareceran los ads?~~ **Diferido a v2**
|
|
2. ~~¿Implementar A/B testing entre modos?~~ **Diferido a v2**
|
|
3. ~~¿Agregar reportes de rendimiento por tipo de ubicacion?~~ **Diferido a v2**
|