- 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>
16 KiB
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 grupobehavior - Si
incontent_mode != "legacy": usar campos deincontent_advanced - Mostrar banner de migracion en admin
Decision 5: Enfoque de parsing HTML
Opcion A: DOMDocument
$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)
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:
$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)
<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
{
"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_lengthya maneja esto
Risk 4: Complejidad de migracion
- Probabilidad: Baja
- Impacto: Medio
- Mitigacion: Default "legacy" preserva comportamiento actual
Migration Plan
- Fase 1 - Schema: Agregar grupo
incontent_advancedcon default "legacy" - Fase 2 - Sync:
wp roi-theme sync-component adsense-placement - Fase 3 - FormBuilder: Nueva UI con banner de migracion
- Fase 4 - Renderer: Implementar ContentAdInjector con algoritmo de 6 pasos
- Fase 5 - Testing: Validar en posts con contenido variado
- Fase 6 - Deploy: Default "legacy", usuarios migran manualmente
Rollback
- Cambiar
incontent_modea "legacy" restaura comportamiento anterior - No hay cambios destructivos en BD
Open Questions
¿Se deberia agregar un preview en vivo de donde apareceran los ads?Diferido a v2¿Implementar A/B testing entre modos?Diferido a v2¿Agregar reportes de rendimiento por tipo de ubicacion?Diferido a v2