Files
roi-theme/openspec/changes/add-advanced-incontent-ads/design.md
FrankZamora c2fff49961 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>
2025-12-09 19:58:50 -06:00

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 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

$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_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