docs(config): add advanced incontent ads specification

- 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>
This commit is contained in:
FrankZamora
2025-12-09 19:58:50 -06:00
parent 85f3387fd2
commit c2fff49961
4 changed files with 1326 additions and 0 deletions

View File

@@ -0,0 +1,471 @@
# 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**

View File

@@ -0,0 +1,36 @@
# Change: Ampliar opciones de In-Content Ads para maximizar ingresos
## Why
Los ingresos de AdSense han disminuido aproximadamente un 50% (de ~130 MXN/dia a ~65 MXN/dia). Se ha observado que se muestran significativamente menos anuncios que antes. El sistema actual de in-content ads solo inserta anuncios despues de parrafos, pero el contenido tiene muchos mas puntos de insercion potenciales (despues de encabezados H2/H3, despues de imagenes, despues de listas, etc.) que no se estan aprovechando.
## What Changes
### Nuevas ubicaciones de insercion
- **ADDED** Insercion despues de encabezados H2 (configurable)
- **ADDED** Insercion despues de encabezados H3 (configurable)
- **ADDED** Insercion despues de imagenes/figuras
- **ADDED** Insercion despues de blockquotes
- **ADDED** Insercion despues de listas (ul/ol)
- **ADDED** Insercion despues de tablas
### Configuracion avanzada
- **ADDED** Cantidad maxima de ads aumentada de 8 a 15
- **ADDED** Control individual por tipo de ubicacion (activar/desactivar cada tipo)
- **ADDED** Prioridad de ubicaciones (orden de preferencia)
- **ADDED** Modo agresivo vs conservador
- **ADDED** Espaciado minimo entre cualquier tipo de ad
### UI Admin mejorada
- **MODIFIED** Seccion In-Content Ads reorganizada con subsecciones
- **ADDED** Preview visual de posibles ubicaciones
- **ADDED** Indicadores de densidad de anuncios
## Impact
- **Affected specs**: openspec/specs/adsense-placement (a crear)
- **Affected code**:
- `Schemas/adsense-placement.json` - Nuevos campos
- `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php` - Nueva UI
- `Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php` - Nueva logica de insercion
- **Expected outcome**: Incremento significativo en impresiones de anuncios, potencialmente duplicando o triplicando los ingresos actuales al aprovechar todas las oportunidades de insercion

View File

@@ -0,0 +1,690 @@
# Especificacion: AdSense Placement - In-Content Ads Avanzados
## Definiciones Tecnicas
### Definicion: Elemento de Bloque Contable
Un "elemento" para efectos de espaciado y conteo se define como el tag de cierre de los siguientes elementos de bloque principal:
| Tag | Cuenta como elemento | Notas |
|-----|---------------------|-------|
| `</p>` | SI | Parrafo |
| `</h2>` | SI | Encabezado nivel 2 |
| `</h3>` | SI | Encabezado nivel 3 |
| `</figure>` | SI | Contenedor de imagen con caption |
| `</ul>` | SI | Lista desordenada (el contenedor, no los `<li>`) |
| `</ol>` | SI | Lista ordenada (el contenedor, no los `<li>`) |
| `</table>` | SI | Tabla (el contenedor, no `<tr>` ni `<td>`) |
| `</blockquote>` | SI | Cita en bloque |
| `<img>` | SOLO si no esta dentro de `<figure>` | Imagen standalone |
| `</li>` | NO | Items de lista no cuentan individualmente |
| `</tr>`, `</td>` | NO | Elementos internos de tabla no cuentan |
| `</div>` | NO | Divs genericos no cuentan |
### Definicion: Prioridades de Ubicacion (Corregidas)
| Tipo de Ubicacion | Prioridad | Justificacion |
|-------------------|-----------|---------------|
| Despues de H2 | 10 | Ruptura tematica mayor, alta visibilidad |
| Despues de parrafos | 8 | Ubicacion tradicional, probada |
| Despues de H3 | 7 | Ruptura tematica menor |
| Despues de imagenes/figure | 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 |
---
## ADDED Requirements
### Requirement: Algoritmo de Insercion de Anuncios
El sistema DEBE seguir un algoritmo determinista para insertar anuncios in-content.
#### Scenario: Algoritmo completo de insercion
- **GIVEN** contenido HTML con multiples elementos de bloque
- **WHEN** se procesa el contenido para insertar ads
- **THEN** el sistema DEBE ejecutar los siguientes pasos en orden:
```
PASO 0: PRECONDICION - VALIDAR LONGITUD MINIMA
- Obtener min_content_length del grupo forms (default: 500)
- Si strlen(strip_tags($content)) < min_content_length:
- Retornar contenido sin modificar
- NO ejecutar pasos siguientes
- Esta validacion aplica a TODOS los modos (legacy y nuevo)
PASO 1: ESCANEO
- Parsear contenido usando regex: preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', ...)
- Para cada tag de cierre, registrar: {posicion, tipo, indice_elemento}
- Detectar <img> standalone usando logica de dos pasos:
1. Encontrar todos los <img> con: preg_match_all('/<img[^>]*>/i', $content, $imgs, PREG_OFFSET_CAPTURE)
2. Para cada <img> encontrado:
- Buscar si existe <figure> abierto antes sin cerrar
- Si NO hay <figure> abierto: registrar como ubicacion elegible tipo "image"
- Si SI hay <figure> abierto: ignorar (se contara con </figure>)
PASO 2: FILTRADO POR CONFIGURACION
- Eliminar ubicaciones cuyo tipo tenga enabled=false
- Ejemplo: si incontent_after_h3_enabled=false, eliminar todas las ubicaciones </h3>
PASO 3: APLICAR PROBABILIDAD DETERMINISTICA
- Calcular seed: crc32(post_id . date('Y-m-d'))
- Inicializar: mt_srand(seed)
- Para cada ubicacion restante:
- Si mt_rand(1, 100) > probabilidad_del_tipo: eliminar ubicacion
- Esto garantiza consistencia durante el mismo dia para cache
PASO 4-5: FILTRADO Y SELECCION (segun incontent_priority_mode)
SI incontent_priority_mode == "position" (default):
PASO 4: FILTRAR POR ESPACIADO (orden DOM)
- Ordenar ubicaciones por posicion en DOM
- Iterar secuencialmente:
- Si distancia a ubicacion anterior < min_spacing: eliminar ubicacion
- La distancia se mide en cantidad de elementos de bloque entre ambas
PASO 5: APLICAR LIMITE
- Si cantidad de ubicaciones > max_total_ads:
- Ordenar por prioridad descendente (H2=10 primero)
- Tomar las primeras max_total_ads ubicaciones
- Reordenar por posicion en DOM
SI incontent_priority_mode == "priority":
PASO 4: ORDENAR POR PRIORIDAD
- Ordenar ubicaciones por prioridad DESC (H2=10 primero, luego p=8, etc.)
PASO 5: FILTRAR POR ESPACIADO (orden de prioridad)
- Iterar en orden de prioridad (no DOM):
- Si la ubicacion viola min_spacing con alguna ya seleccionada: eliminar
- Si ya tenemos max_total_ads: parar
- Reordenar resultado final por posicion DOM
PASO 6: INSERCION
- Para cada ubicacion final, insertar HTML del ad despues del tag de cierre
- El ad usa el formato configurado (in-article, auto, etc.)
```
#### Scenario: Seed deterministico para cache
- **GIVEN** un post con ID 12345 visitado multiples veces el mismo dia
- **WHEN** se calculan las posiciones de ads
- **THEN** las posiciones DEBEN ser identicas en todas las visitas del mismo dia
- **AND** al dia siguiente las posiciones pueden cambiar (nuevo seed)
#### Scenario: Validacion de longitud minima de contenido
- **GIVEN** `min_content_length` = 500 (campo del grupo `forms`)
- **AND** el contenido tiene 300 caracteres (sin tags HTML)
- **WHEN** se invoca el algoritmo de insercion
- **THEN** NO se ejecuta ningun paso del algoritmo
- **AND** se retorna el contenido original sin modificar
- **AND** esta validacion aplica tanto a modo "legacy" como a modos nuevos
#### Scenario: Resolucion de conflictos de posicion
- **GIVEN** un H2 seguido inmediatamente por un parrafo
- **WHEN** ambos tipos estan habilitados
- **AND** el espaciado minimo es 1
- **THEN** solo se inserta ad en la ubicacion de mayor prioridad (H2)
- **AND** el parrafo se cuenta para el espaciado pero no recibe ad
---
### Requirement: Ubicaciones de insercion por tipo de elemento
El sistema DEBE permitir insertar anuncios despues de diferentes tipos de elementos HTML estructurales, cada uno con configuracion independiente.
#### Scenario: Insercion despues de encabezados H2
- **WHEN** el campo `incontent_after_h2_enabled` es true
- **AND** el contenido tiene elementos `</h2>`
- **THEN** el sistema DEBE registrar cada `</h2>` como ubicacion elegible
- **AND** la probabilidad de insercion es controlada por `incontent_after_h2_probability`
#### Scenario: Insercion despues de parrafos
- **WHEN** el campo `incontent_after_paragraphs_enabled` es true
- **AND** el contenido tiene elementos `</p>`
- **THEN** el sistema DEBE registrar cada `</p>` como ubicacion elegible
- **AND** la probabilidad de insercion es controlada por `incontent_after_paragraphs_probability`
- **AND** esta es la ubicacion "tradicional" del sistema legacy
#### Scenario: Insercion despues de encabezados H3
- **WHEN** el campo `incontent_after_h3_enabled` es true
- **AND** el contenido tiene elementos `</h3>`
- **THEN** el sistema DEBE registrar cada `</h3>` como ubicacion elegible
- **AND** la probabilidad de insercion es controlada por `incontent_after_h3_probability`
#### Scenario: Insercion despues de imagenes
- **WHEN** el campo `incontent_after_images_enabled` es true
- **AND** el contenido tiene elementos `</figure>` o `<img>` standalone
- **THEN** el sistema DEBE registrar como ubicacion elegible
- **AND** si `<img>` esta dentro de `<figure>`, solo cuenta `</figure>` (no duplicar)
- **AND** la probabilidad es controlada por `incontent_after_images_probability`
#### Scenario: Insercion despues de listas
- **WHEN** el campo `incontent_after_lists_enabled` es true
- **AND** el contenido tiene elementos `</ul>` o `</ol>`
- **THEN** el sistema DEBE registrar cada cierre de lista como ubicacion elegible
- **AND** NO se insertara ad si la lista tiene menos de 3 `<li>` directos
- **AND** la probabilidad es controlada por `incontent_after_lists_probability`
#### Scenario: Conteo de items en listas anidadas
- **GIVEN** el siguiente contenido:
```html
<ul>
<li>Item 1</li>
<li>Item 2
<ul>
<li>Sub-item A</li>
<li>Sub-item B</li>
</ul>
</li>
<li>Item 3</li>
</ul>
```
- **WHEN** se evalua si la lista externa es elegible para ad
- **THEN** solo se cuentan los `<li>` directos (hijos inmediatos): 3
- **AND** los `<li>` de la lista anidada NO se cuentan para la lista padre
- **AND** la lista anidada se evalua por separado (tiene 2 items, no elegible)
- **NOTA IMPLEMENTACION**: Usar `substr_count($list_content, '<li')` para contar items. Limitacion conocida: listas anidadas contaran todos los `<li>`. Esto es aceptable para v1 ya que listas anidadas son infrecuentes en el contenido del sitio.
#### Scenario: Todos los tipos deshabilitados
- **GIVEN** todos los campos `*_enabled` son false (H2, H3, paragraphs, images, lists, blockquotes, tables)
- **WHEN** se ejecuta el algoritmo de insercion
- **THEN** PASO 2 elimina todas las ubicaciones candidatas
- **AND** se retorna el contenido sin modificar
- **AND** no se insertan anuncios
#### Scenario: Insercion despues de blockquotes
- **WHEN** el campo `incontent_after_blockquotes_enabled` es true
- **AND** el contenido tiene elementos `</blockquote>`
- **THEN** el sistema DEBE registrar cada `</blockquote>` como ubicacion elegible
- **AND** la probabilidad es controlada por `incontent_after_blockquotes_probability`
#### Scenario: Insercion despues de tablas
- **WHEN** el campo `incontent_after_tables_enabled` es true
- **AND** el contenido tiene elementos `</table>`
- **THEN** el sistema DEBE registrar cada `</table>` como ubicacion elegible
- **AND** la probabilidad es controlada por `incontent_after_tables_probability`
---
### Requirement: Modos de densidad de anuncios
El sistema DEBE ofrecer modos predefinidos que controlan la cantidad y espaciado de anuncios.
#### Scenario: Modo legacy (backward compatibility)
- **WHEN** `incontent_mode` es "legacy"
- **THEN** el sistema NO usa el grupo `incontent_advanced` para el algoritmo
- **AND** usa los campos del grupo `behavior` (post_content_*)
- **AND** ejecuta la logica de insercion anterior (solo despues de parrafos)
- **AND** los campos de `incontent_advanced` se muestran deshabilitados en el UI
- **AND** se muestra banner: "Usando configuracion legacy. Migra al nuevo sistema para mas opciones."
#### Scenario: Modo conservador
- **WHEN** `incontent_mode` es "conservative"
- **THEN** el maximo de ads in-content es 5
- **AND** el espaciado minimo entre ads es 5 elementos
- **AND** solo se activan ubicaciones despues de H2 y parrafos por defecto
#### Scenario: Modo balanceado
- **WHEN** `incontent_mode` es "balanced"
- **THEN** el maximo de ads in-content es 8
- **AND** el espaciado minimo entre ads es 3 elementos
- **AND** se activan H2, H3, imagenes y parrafos por defecto
#### Scenario: Modo agresivo
- **WHEN** `incontent_mode` es "aggressive"
- **THEN** el maximo de ads in-content es 15
- **AND** el espaciado minimo entre ads es 2 elementos
- **AND** se activan todas las ubicaciones por defecto
#### Scenario: Modo personalizado
- **WHEN** `incontent_mode` es "custom"
- **THEN** el usuario puede configurar cada campo individualmente
- **AND** los valores de max/spacing no se sobreescriben al cambiar de modo
#### Scenario: Modificacion de campos en modo preset (auto-switch a custom)
- **GIVEN** `incontent_mode` es "balanced" (o cualquier preset excepto "custom" y "legacy")
- **WHEN** el usuario modifica manualmente cualquiera de estos campos:
- `incontent_max_total_ads`
- `incontent_min_spacing`
- Cualquier campo `*_enabled` o `*_probability`
- **THEN** el sistema DEBE cambiar automaticamente `incontent_mode` a "custom"
- **AND** mostrar mensaje informativo: "Modo cambiado a Personalizado"
- **AND** los valores modificados se preservan
#### Scenario: Cambio de modo preset sobreescribe valores
- **GIVEN** `incontent_mode` es "custom" con valores personalizados
- **WHEN** el usuario cambia `incontent_mode` a "balanced"
- **THEN** los valores de max_total_ads, min_spacing y flags enabled se sobreescriben con los del preset
- **AND** se muestra confirmacion antes de aplicar el cambio
---
### Requirement: Espaciado minimo entre anuncios
El sistema DEBE mantener un espaciado minimo entre cualquier par de anuncios in-content.
#### Scenario: Calculo de espaciado
- **GIVEN** dos ubicaciones candidatas para ads
- **WHEN** se evalua el espaciado entre ellas
- **THEN** el espaciado se calcula contando elementos de bloque entre ambas posiciones
- **AND** solo se cuentan los elementos definidos en "Elemento de Bloque Contable"
#### Scenario: Ejemplo de calculo de espaciado
- **GIVEN** el siguiente contenido:
```html
<h2>Titulo</h2> <!-- Ubicacion A (posible ad) -->
<p>Parrafo 1</p> <!-- Elemento 1 -->
<p>Parrafo 2</p> <!-- Elemento 2 -->
<figure><img></figure> <!-- Elemento 3 -->
<p>Parrafo 3</p> <!-- Ubicacion B (posible ad) - Elemento 4 -->
```
- **WHEN** min_spacing es 3
- **THEN** la distancia entre A y B es 4 elementos
- **AND** ambas ubicaciones pueden tener ads (4 >= 3)
#### Scenario: Espaciado insuficiente
- **GIVEN** min_spacing es 5
- **AND** solo hay 3 elementos entre dos ubicaciones candidatas
- **THEN** la segunda ubicacion se elimina de las candidatas
- **AND** se conserva la de mayor prioridad
---
### Requirement: Estrategia de seleccion configurable
El sistema DEBE permitir elegir como resolver conflictos cuando dos ubicaciones estan muy cerca.
#### Scenario: Modo position (default)
- **WHEN** `incontent_priority_mode` es "position"
- **AND** hay un H2 (prioridad 10) en posicion 3 y un parrafo (prioridad 8) en posicion 1
- **AND** min_spacing es 3
- **THEN** el parrafo en posicion 1 se selecciona primero (por orden DOM)
- **AND** el H2 en posicion 3 se elimina por violar espaciado (distancia 2 < 3)
- **AND** el resultado favorece distribucion uniforme
#### Scenario: Modo priority
- **WHEN** `incontent_priority_mode` es "priority"
- **AND** hay un H2 (prioridad 10) en posicion 3 y un parrafo (prioridad 8) en posicion 1
- **AND** min_spacing es 3
- **THEN** el H2 se selecciona primero (por mayor prioridad)
- **AND** el parrafo se elimina por violar espaciado con el H2
- **AND** el resultado maximiza ubicaciones de alto valor
---
### Requirement: Probabilidad deterministica por ubicacion
El sistema DEBE soportar probabilidad configurable que sea consistente durante el dia.
#### Scenario: Implementacion de probabilidad deterministica
- **GIVEN** un post_id y una fecha
- **WHEN** se calcula si insertar ad en una ubicacion
- **THEN** el seed es `crc32(post_id . 'YYYY-MM-DD')`
- **AND** se usa `mt_srand(seed)` antes de evaluar probabilidades
- **AND** cada ubicacion consume un `mt_rand(1, 100)` en orden de aparicion
#### Scenario: Valores de probabilidad disponibles
- **WHEN** el usuario configura probabilidad para cualquier tipo
- **THEN** los valores disponibles son: 25, 50, 75, 100
- **AND** el valor se interpreta como porcentaje
---
## MODIFIED Requirements
### Requirement: Estrategia de campos legacy
Los campos existentes del grupo `behavior` relacionados con in-content ads DEBEN coexistir con el nuevo sistema mediante deprecacion suave.
#### Scenario: Campos legacy a deprecar
- **GIVEN** los siguientes campos existentes:
- `post_content_enabled` (behavior)
- `post_content_max_ads` (behavior)
- `post_content_after_paragraphs` (behavior)
- `post_content_min_paragraphs_between` (behavior)
- `post_content_random_mode` (behavior)
- `post_content_format` (behavior)
- **WHEN** el sistema tiene ambos grupos de campos
- **THEN** el grupo `incontent_advanced` tiene precedencia si `incontent_mode` != "legacy"
- **AND** si `incontent_mode` == "legacy", se usan los campos del grupo `behavior`
#### Scenario: Migracion automatica en UI
- **WHEN** el usuario visita el panel de AdSense por primera vez despues de la actualizacion
- **AND** tiene configuracion legacy activa (`post_content_enabled` = true)
- **THEN** se muestra un banner informativo sobre el nuevo sistema
- **AND** se ofrece boton "Migrar a nuevo sistema" que copia valores equivalentes
#### Scenario: Mapeo de campos legacy a nuevos
| Campo Legacy | Campo Nuevo | Logica de Mapeo |
|--------------|-------------|-----------------|
| post_content_max_ads | incontent_max_total_ads | Copia directa (ampliar opciones) |
| post_content_min_paragraphs_between | incontent_min_spacing | Copia directa |
| post_content_random_mode | N/A | Si true, todas las probabilidades = 75% |
| post_content_after_paragraphs | N/A | Se usa para primer ad, resto aleatorio |
---
### Requirement: UI de In-Content Ads reorganizada
La interfaz de administracion DEBE organizarse en subsecciones claras usando elementos HTML semanticos.
#### Scenario: Estructura HTML de la seccion
- **WHEN** se renderiza el card de In-Content Ads Avanzado
- **THEN** DEBE seguir esta estructura:
```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">
<i class="bi bi-speedometer2 me-1"></i>
Densidad estimada: <strong>Media</strong>
<span class="badge bg-warning">~6 ads</span>
</div>
<!-- Selector de modo -->
<div class="mb-4">
<label class="form-label fw-semibold">Modo de densidad</label>
<select class="form-select" id="incontentMode">
<option value="legacy">Legacy (usar config anterior)</option>
<option value="conservative">Conservador (max 5, espaciado 5)</option>
<option value="balanced" selected>Balanceado (max 8, espaciado 3)</option>
<option value="aggressive">Agresivo (max 15, espaciado 2)</option>
<option value="custom">Personalizado</option>
</select>
</div>
<!-- Subseccion: Ubicaciones por elemento -->
<details class="mb-3 border rounded" open>
<summary class="p-3 bg-light fw-bold cursor-pointer">
<i class="bi bi-geo-alt me-1"></i>
Ubicaciones por Elemento
</summary>
<div class="p-3">
<!-- Toggle + probabilidad para cada tipo -->
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="afterH2Enabled" checked>
<label class="form-check-label">Despues de H2</label>
</div>
</div>
<div class="col-md-6">
<select class="form-select form-select-sm" id="afterH2Prob">
<option value="100" selected>100%</option>
<option value="75">75%</option>
<option value="50">50%</option>
<option value="25">25%</option>
</select>
</div>
<!-- Repetir para H3, images, lists, blockquotes, tables -->
</div>
</div>
</details>
<!-- Subseccion: Limites y espaciado -->
<details class="mb-3 border rounded">
<summary class="p-3 bg-light fw-bold cursor-pointer">
<i class="bi bi-sliders me-1"></i>
Limites y Espaciado
</summary>
<div class="p-3">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Maximo total de ads</label>
<select class="form-select" id="maxTotalAds">
<!-- Opciones 1-15 -->
</select>
</div>
<div class="col-md-6">
<label class="form-label">Espaciado minimo (elementos)</label>
<select class="form-select" id="minSpacing">
<option value="2">2 elementos</option>
<option value="3" selected>3 elementos</option>
<option value="4">4 elementos</option>
<option value="5">5 elementos</option>
<option value="6">6 elementos</option>
</select>
</div>
<div class="col-md-12 mt-3">
<label class="form-label">Estrategia de seleccion</label>
<select class="form-select" id="priorityMode">
<option value="position" selected>Por posicion (distribucion uniforme)</option>
<option value="priority">Por prioridad (maximizar H2/H3)</option>
</select>
<small class="text-muted">Como resolver conflictos cuando dos ubicaciones estan muy cerca</small>
</div>
</div>
</div>
</details>
<!-- Warning para densidad alta -->
<div id="highDensityWarning" class="alert alert-warning small d-none">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Atencion:</strong> Densidad alta puede afectar UX y violar politicas de AdSense.
</div>
</div>
</div>
```
#### Scenario: Indicador de densidad dinamico
- **WHEN** el usuario modifica cualquier campo de in-content ads
- **THEN** el indicador de densidad se actualiza en tiempo real via JavaScript
- **AND** muestra estimacion basada en: max_ads * promedio_probabilidades / 100
- **AND** colores: verde (<5), amarillo (5-10), rojo (>10)
---
## Schema JSON - Campos Completos
### Grupo: incontent_advanced (priority 69)
```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",
"balanced": "Balanceado",
"aggressive": "Agresivo",
"custom": "Personalizado"
},
"description": "Presets que ajustan limites y ubicaciones automaticamente. Default 'legacy' para backward compatibility."
},
"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 (ubicacion tradicional)"
},
"incontent_after_paragraphs_probability": {
"type": "select",
"label": "Probabilidad parrafos",
"default": "75",
"editable": true,
"options": ["25", "50", "75", "100"],
"description": "Porcentaje de probabilidad de insercion despues de parrafos"
},
"incontent_after_images_enabled": {
"type": "boolean",
"label": "Despues de imagenes",
"default": true,
"editable": true,
"description": "Insertar anuncios 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 anuncios 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 anuncios 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 anuncios 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 todas las 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"
}
}
}
}
```
---
## Implementacion Tecnica
### Opcion de Parsing Recomendada: Regex Multiple
```php
// Regex para detectar todos los elementos de bloque contables (H4 no soportado)
$pattern = '/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i';
$parts = preg_split($pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE);
// Para detectar <img> standalone (no dentro de figure)
// Procesar en segundo paso, verificando contexto
```
**Justificacion:**
- Mas rapido que DOMDocument
- No modifica el HTML original
- Suficiente para el caso de uso (no necesitamos validar anidamiento complejo)
- El contexto de `<img>` dentro de `<figure>` se resuelve verificando si hay `<figure>` abierto sin cerrar
### Diagrama de Dependencias de Tasks
```
1.1 Schema: grupo incontent_advanced ─┐
1.2 Schema: campos individuales ──────┼──> 1.3 Sync BD ──┬──> 2.x FormBuilder
│ │
│ └──> 3.x Renderer
│ │
│ v
│ 4.x Validacion
│ │
└──────────────────────> 5.x Docs
```

View File

@@ -0,0 +1,129 @@
# Tasks: Implementacion de In-Content Ads Avanzados
## Diagrama de Dependencias
```
1.1 ──┬──> 1.3 ──┬──> 2.x FormBuilder ──> 4.x
1.2 ──┘ │
└──> 3.x Renderer ──────> 4.x
v
5.x Docs
```
---
## 1. Schema JSON - Nuevos campos
**Prerequisitos:** Ninguno
- [ ] 1.1 Agregar grupo `incontent_advanced` con priority 69 al schema JSON
- [ ] 1.2 Agregar todos los campos definidos en spec:
- [ ] incontent_mode (select: legacy/conservative/balanced/aggressive/custom)
- [ ] incontent_after_h2_enabled + probability
- [ ] incontent_after_h3_enabled + probability
- [ ] incontent_after_paragraphs_enabled + probability (ubicacion tradicional)
- [ ] incontent_after_images_enabled + probability
- [ ] incontent_after_lists_enabled + probability
- [ ] incontent_after_blockquotes_enabled + probability
- [ ] incontent_after_tables_enabled + probability
- [ ] incontent_max_total_ads (1-15)
- [ ] incontent_min_spacing (2-6)
- [ ] incontent_format
- [ ] incontent_priority_mode (position/priority)
- [ ] 1.3 Sincronizar schema con BD via WP-CLI: `wp roi-theme sync-component adsense-placement`
---
## 2. FormBuilder Admin - Nueva UI
**Prerequisitos:** 1.3 completado
- [ ] 2.1 Crear metodo `buildInContentAdvancedGroup()` en AdsensePlacementFormBuilder
- [ ] 2.2 Implementar indicador de densidad (HTML + logica de color)
- [ ] 2.3 Implementar selector de modo con presets
- [ ] 2.4 Crear subseccion colapsable "Ubicaciones por Elemento" usando `<details>`
- [ ] 2.4.1 Toggle + probabilidad para H2
- [ ] 2.4.2 Toggle + probabilidad para H3
- [ ] 2.4.3 Toggle + probabilidad para parrafos (ubicacion tradicional)
- [ ] 2.4.4 Toggle + probabilidad para imagenes
- [ ] 2.4.5 Toggle + probabilidad para listas
- [ ] 2.4.6 Toggle + probabilidad para blockquotes
- [ ] 2.4.7 Toggle + probabilidad para tablas
- [ ] 2.5 Crear subseccion colapsable "Limites y Espaciado"
- [ ] 2.5.1 Select max_total_ads (1-15)
- [ ] 2.5.2 Select min_spacing (2-6)
- [ ] 2.6 Agregar warning visual para densidad alta (>10 ads)
- [ ] 2.7 Agregar banner de migracion para usuarios con config legacy activa
- [ ] 2.8 Mantener seccion legacy existente (modo "legacy" la usa)
- [ ] 2.9 Agregar mapeos en AdsensePlacementFieldMapper para todos los campos nuevos:
- [ ] incontent_mode
- [ ] incontent_after_h2_enabled + probability
- [ ] incontent_after_h3_enabled + probability
- [ ] incontent_after_paragraphs_enabled + probability
- [ ] incontent_after_images_enabled + probability
- [ ] incontent_after_lists_enabled + probability
- [ ] incontent_after_blockquotes_enabled + probability
- [ ] incontent_after_tables_enabled + probability
- [ ] incontent_max_total_ads
- [ ] incontent_min_spacing
- [ ] incontent_format
- [ ] incontent_priority_mode
- [ ] 2.10 Implementar logica JavaScript:
- [ ] 2.10.1 Auto-switch a "custom" al modificar campos (con toast informativo)
- [ ] 2.10.2 Modal de confirmacion al cambiar de "custom" a preset
- [ ] 2.10.3 Actualizar indicador de densidad en tiempo real
---
## 3. Renderer - Logica de insercion mejorada
**Prerequisitos:** 1.3 completado
- [ ] 3.1 Crear/modificar ContentAdInjector service con nuevo algoritmo
- [ ] 3.1.1 Implementar PASO 0: Validar min_content_length
- [ ] 3.1.2 Implementar PASO 1: Escaneo con regex multiple
```php
preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', ...)
```
- [ ] 3.1.3 Implementar deteccion de `<img>` standalone (no dentro de figure)
- [ ] 3.1.4 Implementar validacion de listas (usar substr_count, minimo 3 items)
- [ ] 3.2 Implementar PASO 2: Filtrado por configuracion (enabled flags)
- [ ] 3.3 Implementar PASO 3: Probabilidad deterministica
- [ ] 3.3.1 Calcular seed: `crc32(post_id . date('Y-m-d'))`
- [ ] 3.3.2 Usar `mt_srand(seed)` + `mt_rand(1, 100)`
- [ ] 3.4 Implementar PASO 4-5: Filtrado y seleccion (segun incontent_priority_mode)
- [ ] 3.4.1 Definir constantes de prioridad (H2=10, p=8, H3=7, img=6, lists=5, bq=4, table=3)
- [ ] 3.4.2 Implementar modo "position": espaciado primero, luego prioridad
- [ ] 3.4.3 Implementar modo "priority": prioridad primero, luego espaciado
- [ ] 3.4.4 Aplicar limite max_total_ads
- [ ] 3.4.5 Reordenar resultado final por posicion DOM
- [ ] 3.5 Implementar PASO 6: Insercion de ads
- [ ] 3.6 Implementar logica de precedencia legacy vs nuevo sistema
- [ ] 3.6.1 Si incontent_mode == "legacy", usar campos del grupo behavior
- [ ] 3.6.2 Si incontent_mode != "legacy", usar incontent_advanced
---
## 4. Validacion y Testing
**Prerequisitos:** 2.x y 3.x completados
- [ ] 4.1 Ejecutar `validate-architecture.php adsense-placement`
- [ ] 4.2 Probar en post con contenido variado (H2, H3, images, lists, tables)
- [ ] 4.3 Verificar que seed deterministico funciona (mismas posiciones mismo dia)
- [ ] 4.4 Verificar que espaciado minimo se respeta
- [ ] 4.5 Verificar que limite max_total_ads se respeta
- [ ] 4.6 Verificar que modo legacy sigue funcionando
- [ ] 4.7 Verificar que delay de carga sigue funcionando
- [ ] 4.8 Probar indicador de densidad en admin
- [ ] 4.9 Verificar ambos modos de priority_mode (position vs priority)
---
## 5. Documentacion
**Prerequisitos:** 4.x completado
- [ ] 5.1 Crear spec.md final en `openspec/specs/adsense-placement/`
- [ ] 5.2 Archivar este change: `openspec archive add-advanced-incontent-ads`