From 2896e2d0065a41eedb86ec71e32f28af08484eec Mon Sep 17 00:00:00 2001
From: FrankZamora
Date: Wed, 10 Dec 2025 10:42:53 -0600
Subject: [PATCH] feat(php): implement advanced in-content ads with
multi-element targeting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add incontent_advanced group with 19 configurable fields in schema
- Support 5 density modes: paragraphs_only, conservative, balanced,
aggressive, custom
- Enable ad placement after H2, H3, paragraphs, images, lists,
blockquotes, and tables
- Add probability-based selection (25-100%) per element type
- Implement priority-based and position-based ad selection strategies
- Add detailed mode descriptions in admin UI for better UX
- Rename 'legacy' terminology to 'paragraphs_only' for clarity
- Support deterministic randomization using post_id + date seed
馃 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
.../AdsensePlacementFieldMapper.php | 21 +
.../Ui/AdsensePlacementFormBuilder.php | 285 ++++++++++++
.../Ui/Assets/Js/admin-dashboard.js | 398 +++++++++++++++++
.../Services/ContentAdInjector.php | 410 +++++++++++++++++-
Schemas/adsense-placement.json | 168 ++++++-
5 files changed, 1263 insertions(+), 19 deletions(-)
diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
index dbd67be0..6170f86c 100644
--- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
+++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
@@ -118,6 +118,27 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
+
+ // INCONTENT ADVANCED (In-Content Ads Avanzado)
+ 'adsense-placementIncontentMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_mode'],
+ 'adsense-placementIncontentAfterH2Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_enabled'],
+ 'adsense-placementIncontentAfterH2Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_probability'],
+ 'adsense-placementIncontentAfterH3Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_enabled'],
+ 'adsense-placementIncontentAfterH3Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_probability'],
+ 'adsense-placementIncontentAfterParagraphsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_enabled'],
+ 'adsense-placementIncontentAfterParagraphsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_probability'],
+ 'adsense-placementIncontentAfterImagesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_enabled'],
+ 'adsense-placementIncontentAfterImagesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_probability'],
+ 'adsense-placementIncontentAfterListsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_enabled'],
+ 'adsense-placementIncontentAfterListsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_probability'],
+ 'adsense-placementIncontentAfterBlockquotesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_enabled'],
+ 'adsense-placementIncontentAfterBlockquotesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_probability'],
+ 'adsense-placementIncontentAfterTablesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_enabled'],
+ 'adsense-placementIncontentAfterTablesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_probability'],
+ 'adsense-placementIncontentMaxTotalAds' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_max_total_ads'],
+ 'adsense-placementIncontentMinSpacing' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_min_spacing'],
+ 'adsense-placementIncontentFormat' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_format'],
+ 'adsense-placementIncontentPriorityMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_priority_mode'],
];
}
}
diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
index 3a40fd24..f5c5e1c4 100644
--- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
+++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
@@ -47,6 +47,7 @@ final class AdsensePlacementFormBuilder
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildDiagramSection();
$html .= $this->buildPostLocationsGroup($componentId);
+ $html .= $this->buildInContentAdvancedGroup($componentId);
$html .= $this->buildInContentAdsGroup($componentId);
$html .= $this->buildExclusionsGroup($componentId);
$html .= ' ';
@@ -342,6 +343,290 @@ final class AdsensePlacementFormBuilder
return $html;
}
+
+ /**
+ * Seccion avanzada para In-Content Ads con multiples tipos de ubicacion
+ * Incluye: modo de densidad, ubicaciones por elemento, limites y espaciado
+ */
+ private function buildInContentAdvancedGroup(string $cid): string
+ {
+ // Obtener valores actuales del grupo incontent_advanced
+ $mode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_mode', 'paragraphs_only');
+ $mode = is_string($mode) ? $mode : 'paragraphs_only';
+
+ // Ubicaciones
+ $h2Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_enabled', true);
+ $h2Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_probability', '100');
+ $h3Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_enabled', true);
+ $h3Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_probability', '50');
+ $paragraphsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_enabled', true);
+ $paragraphsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_probability', '75');
+ $imagesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_enabled', true);
+ $imagesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_probability', '75');
+ $listsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_enabled', false);
+ $listsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_probability', '50');
+ $blockquotesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_enabled', false);
+ $blockquotesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_probability', '50');
+ $tablesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_enabled', false);
+ $tablesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_probability', '50');
+
+ // Limites
+ $maxAds = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_max_total_ads', '8');
+ $minSpacing = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_min_spacing', '3');
+ $format = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_format', 'in-article');
+ $priorityMode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_priority_mode', 'position');
+
+ // Cast to string where needed
+ $h2Prob = is_string($h2Prob) ? $h2Prob : '100';
+ $h3Prob = is_string($h3Prob) ? $h3Prob : '50';
+ $paragraphsProb = is_string($paragraphsProb) ? $paragraphsProb : '75';
+ $imagesProb = is_string($imagesProb) ? $imagesProb : '75';
+ $listsProb = is_string($listsProb) ? $listsProb : '50';
+ $blockquotesProb = is_string($blockquotesProb) ? $blockquotesProb : '50';
+ $tablesProb = is_string($tablesProb) ? $tablesProb : '50';
+ $maxAds = is_string($maxAds) ? $maxAds : '8';
+ $minSpacing = is_string($minSpacing) ? $minSpacing : '3';
+ $format = is_string($format) ? $format : 'in-article';
+ $priorityMode = is_string($priorityMode) ? $priorityMode : 'position';
+
+ $isParagraphsOnly = $mode === 'paragraphs_only';
+ $disabledAttr = $isParagraphsOnly ? 'disabled' : '';
+
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' In-Content Ads Avanzado';
+ $html .= ' Nuevo ';
+ $html .= ' ';
+
+ // Indicador de densidad
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Densidad estimada: Calculando... ';
+ $html .= ' ~? ads ';
+ $html .= '
';
+
+ // Banner informativo para modo Solo parrafos
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Solo parrafos: Los anuncios se insertan unicamente despues de parrafos, ';
+ $html .= ' usando la configuracion de la seccion "Post Content". Cambia a otro modo para elegir ubicaciones adicionales.';
+ $html .= '
';
+
+ // Selector de modo con descripciones
+ $html .= '
';
+ $html .= '
';
+ $html .= ' Estrategia de insercion';
+ $html .= ' ';
+ $html .= '
Define donde y con que frecuencia se insertaran anuncios dentro del contenido.
';
+ $html .= '
';
+ $modeOptions = [
+ 'paragraphs_only' => 'Solo parrafos (clasico)',
+ 'conservative' => 'Conservador - H2 y parrafos',
+ 'balanced' => 'Balanceado - Multiples elementos',
+ 'aggressive' => 'Intensivo - Todos los elementos',
+ 'custom' => 'Personalizado'
+ ];
+ foreach ($modeOptions as $value => $label) {
+ $selected = selected($mode, $value, false);
+ $html .= '' . esc_html($label) . ' ';
+ }
+ $html .= ' ';
+
+ // Descripciones de cada modo
+ $html .= '
';
+
+ // Solo parrafos
+ $html .= '
';
+ $html .= '
Solo parrafos';
+ $html .= '
Inserta anuncios unicamente despues de parrafos. Usa la configuracion de la seccion "Post Content" (numero de anuncios, parrafos entre ads, etc).
';
+ $html .= '
Ideal si: Tu contenido tiene pocos encabezados o prefieres la configuracion tradicional.';
+ $html .= '
';
+
+ // Conservador
+ $html .= '
';
+ $html .= '
Conservador';
+ $html .= '
Maximo 5 anuncios con espaciado amplio (5 elementos). Solo inserta despues de titulos H2 y parrafos.
';
+ $html .= '
Ideal si: Priorizas la experiencia del usuario sobre los ingresos. Articulos cortos o medianos.';
+ $html .= '
';
+
+ // Balanceado
+ $html .= '
';
+ $html .= '
Balanceado';
+ $html .= '
Hasta 8 anuncios con espaciado moderado (3 elementos). Usa H2, H3, parrafos e imagenes.
';
+ $html .= '
Ideal si: Buscas equilibrio entre ingresos y experiencia. Articulos medianos a largos.';
+ $html .= '
';
+
+ // Intensivo
+ $html .= '
';
+ $html .= '
Intensivo';
+ $html .= '
Hasta 15 anuncios con espaciado minimo (2 elementos). Usa todos los tipos de elementos disponibles.
';
+ $html .= '
Ideal si: Priorizas maximizar ingresos. Solo para articulos muy largos (+3000 palabras).';
+ $html .= '
';
+
+ // Personalizado
+ $html .= '
';
+ $html .= '
Personalizado';
+ $html .= '
Tu configuras manualmente cada ubicacion, probabilidad y limites.
';
+ $html .= '
Ideal si: Quieres control total sobre donde aparecen los anuncios.';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ // Subseccion: Ubicaciones por elemento
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Ubicaciones por Elemento';
+ $html .= ' ';
+ $html .= ' ';
+
+ // Grid de ubicaciones
+ $locations = [
+ ['id' => 'H2', 'label' => 'Despues de H2 (titulos)', 'enabled' => $h2Enabled, 'prob' => $h2Prob, 'icon' => 'bi-type-h2'],
+ ['id' => 'H3', 'label' => 'Despues de H3 (subtitulos)', 'enabled' => $h3Enabled, 'prob' => $h3Prob, 'icon' => 'bi-type-h3'],
+ ['id' => 'Paragraphs', 'label' => 'Despues de parrafos', 'enabled' => $paragraphsEnabled, 'prob' => $paragraphsProb, 'icon' => 'bi-text-paragraph'],
+ ['id' => 'Images', 'label' => 'Despues de imagenes', 'enabled' => $imagesEnabled, 'prob' => $imagesProb, 'icon' => 'bi-image'],
+ ['id' => 'Lists', 'label' => 'Despues de listas', 'enabled' => $listsEnabled, 'prob' => $listsProb, 'icon' => 'bi-list-ul'],
+ ['id' => 'Blockquotes', 'label' => 'Despues de citas', 'enabled' => $blockquotesEnabled, 'prob' => $blockquotesProb, 'icon' => 'bi-quote'],
+ ['id' => 'Tables', 'label' => 'Despues de tablas', 'enabled' => $tablesEnabled, 'prob' => $tablesProb, 'icon' => 'bi-table'],
+ ];
+
+ $probOptions = [
+ '100' => '100%',
+ '75' => '75%',
+ '50' => '50%',
+ '25' => '25%'
+ ];
+
+ foreach ($locations as $loc) {
+ $enabledId = $cid . 'IncontentAfter' . $loc['id'] . 'Enabled';
+ $probId = $cid . 'IncontentAfter' . $loc['id'] . 'Probability';
+ $checked = checked($loc['enabled'], true, false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html($loc['label']);
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ foreach ($probOptions as $pValue => $pLabel) {
+ $pSelected = selected($loc['prob'], $pValue, false);
+ $html .= '' . esc_html($pLabel) . ' ';
+ }
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+ }
+
+ $html .= '
';
+ $html .= ' ';
+
+ // Subseccion: Limites y espaciado
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Limites y Espaciado';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Max total ads
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Maximo total de ads';
+ $html .= ' ';
+ $html .= ' ';
+ for ($i = 1; $i <= 15; $i++) {
+ $iStr = (string)$i;
+ $adSelected = selected($maxAds, $iStr, false);
+ $label = $i === 1 ? '1 anuncio' : $i . ' anuncios';
+ $html .= '' . esc_html($label) . ' ';
+ }
+ $html .= ' ';
+ $html .= '
';
+
+ // Min spacing
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado minimo (elementos)';
+ $html .= ' ';
+ $html .= ' ';
+ $spacingOptions = [
+ '2' => '2 elementos',
+ '3' => '3 elementos',
+ '4' => '4 elementos',
+ '5' => '5 elementos',
+ '6' => '6 elementos'
+ ];
+ foreach ($spacingOptions as $sValue => $sLabel) {
+ $sSelected = selected($minSpacing, $sValue, false);
+ $html .= '' . esc_html($sLabel) . ' ';
+ }
+ $html .= ' ';
+ $html .= '
';
+
+ // Formato de ads
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Formato de ads';
+ $html .= ' ';
+ $html .= ' ';
+ $formatOptions = [
+ 'in-article' => 'In-Article (fluid)',
+ 'auto' => 'Auto (responsive)'
+ ];
+ foreach ($formatOptions as $fValue => $fLabel) {
+ $fSelected = selected($format, $fValue, false);
+ $html .= '' . esc_html($fLabel) . ' ';
+ }
+ $html .= ' ';
+ $html .= '
';
+
+ // Priority mode
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Estrategia de seleccion';
+ $html .= ' ';
+ $html .= ' ';
+ $priorityOptions = [
+ 'position' => 'Por posicion (distribucion uniforme)',
+ 'priority' => 'Por prioridad (maximizar H2/H3)'
+ ];
+ foreach ($priorityOptions as $pmValue => $pmLabel) {
+ $pmSelected = selected($priorityMode, $pmValue, false);
+ $html .= '' . esc_html($pmLabel) . ' ';
+ }
+ $html .= ' ';
+ $html .= ' Como resolver conflictos cuando dos ubicaciones estan muy cerca ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+
+ // Warning para densidad alta
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Atencion: Densidad alta (>10 ads) puede afectar UX y violar politicas de AdSense.';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
/**
* Seccion especial para in-content ads con configuracion de 1-8 random
*/
diff --git a/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
index 8c175645..14c13701 100644
--- a/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
+++ b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
@@ -518,4 +518,402 @@
});
}
+ // =========================================================================
+ // IN-CONTENT ADS AVANZADO - JavaScript
+ // =========================================================================
+
+ document.addEventListener('DOMContentLoaded', function() {
+ initializeInContentAdvanced();
+ });
+
+ /**
+ * Inicializa la funcionalidad de In-Content Ads Avanzado
+ */
+ function initializeInContentAdvanced() {
+ // Buscar el selector de modo (puede tener prefijo din谩mico)
+ const modeSelect = document.querySelector('[id$="IncontentMode"]');
+ if (!modeSelect) {
+ return; // No estamos en la p谩gina de AdSense
+ }
+
+ // Obtener prefijo del componente desde el ID
+ const componentPrefix = modeSelect.id.replace('IncontentMode', '');
+
+ // Definir presets de modos
+ const modePresets = {
+ 'paragraphs_only': null, // Solo inserta despues de parrafos (config basica)
+ 'conservative': {
+ maxAds: '5',
+ minSpacing: '5',
+ h2: { enabled: true, prob: '75' },
+ h3: { enabled: false, prob: '50' },
+ paragraphs: { enabled: true, prob: '50' },
+ images: { enabled: false, prob: '50' },
+ lists: { enabled: false, prob: '50' },
+ blockquotes: { enabled: false, prob: '50' },
+ tables: { enabled: false, prob: '50' }
+ },
+ 'balanced': {
+ maxAds: '8',
+ minSpacing: '3',
+ h2: { enabled: true, prob: '100' },
+ h3: { enabled: true, prob: '50' },
+ paragraphs: { enabled: true, prob: '75' },
+ images: { enabled: true, prob: '75' },
+ lists: { enabled: false, prob: '50' },
+ blockquotes: { enabled: false, prob: '50' },
+ tables: { enabled: false, prob: '50' }
+ },
+ 'aggressive': {
+ maxAds: '15',
+ minSpacing: '2',
+ h2: { enabled: true, prob: '100' },
+ h3: { enabled: true, prob: '100' },
+ paragraphs: { enabled: true, prob: '100' },
+ images: { enabled: true, prob: '100' },
+ lists: { enabled: true, prob: '75' },
+ blockquotes: { enabled: true, prob: '75' },
+ tables: { enabled: true, prob: '75' }
+ },
+ 'custom': null // Configuraci贸n manual
+ };
+
+ // Elementos del DOM
+ const elements = {
+ mode: modeSelect,
+ paragraphsOnlyBanner: document.getElementById('roiParagraphsOnlyBanner'),
+ densityIndicator: document.getElementById('roiIncontentDensityIndicator'),
+ densityLevel: document.getElementById('roiDensityLevel'),
+ densityBadge: document.getElementById('roiDensityBadge'),
+ highDensityWarning: document.getElementById('roiHighDensityWarning'),
+ locationsDetails: document.getElementById('roiLocationsDetails'),
+ limitsDetails: document.getElementById('roiLimitsDetails'),
+ maxAds: document.querySelector('[id$="IncontentMaxTotalAds"]'),
+ minSpacing: document.querySelector('[id$="IncontentMinSpacing"]'),
+ // Descripciones de modos
+ modeDescriptions: {
+ paragraphs_only: document.getElementById('roiModeDescParagraphsOnly'),
+ conservative: document.getElementById('roiModeDescConservative'),
+ balanced: document.getElementById('roiModeDescBalanced'),
+ aggressive: document.getElementById('roiModeDescAggressive'),
+ custom: document.getElementById('roiModeDescCustom')
+ },
+ locations: [
+ { key: 'H2', el: document.querySelector('[id$="IncontentAfterH2Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH2Probability"]') },
+ { key: 'H3', el: document.querySelector('[id$="IncontentAfterH3Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH3Probability"]') },
+ { key: 'Paragraphs', el: document.querySelector('[id$="IncontentAfterParagraphsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterParagraphsProbability"]') },
+ { key: 'Images', el: document.querySelector('[id$="IncontentAfterImagesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterImagesProbability"]') },
+ { key: 'Lists', el: document.querySelector('[id$="IncontentAfterListsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterListsProbability"]') },
+ { key: 'Blockquotes', el: document.querySelector('[id$="IncontentAfterBlockquotesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterBlockquotesProbability"]') },
+ { key: 'Tables', el: document.querySelector('[id$="IncontentAfterTablesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterTablesProbability"]') }
+ ]
+ };
+
+ // Estado para detectar cambios manuales
+ let isApplyingPreset = false;
+
+ /**
+ * Actualiza el indicador de densidad
+ */
+ function updateDensityIndicator() {
+ const mode = elements.mode.value;
+
+ if (mode === 'paragraphs_only') {
+ elements.densityLevel.textContent = 'Solo parrafos';
+ elements.densityBadge.textContent = 'clasico';
+ elements.densityBadge.className = 'badge bg-secondary ms-1';
+ elements.densityIndicator.className = 'alert alert-light border small mb-3';
+ elements.highDensityWarning.classList.add('d-none');
+ return;
+ }
+
+ // Calcular densidad estimada
+ const maxAds = parseInt(elements.maxAds.value) || 8;
+ let totalWeight = 0;
+ let enabledCount = 0;
+
+ elements.locations.forEach(loc => {
+ if (loc.el && loc.el.checked) {
+ const prob = parseInt(loc.prob.value) || 100;
+ totalWeight += prob;
+ enabledCount++;
+ }
+ });
+
+ const avgProb = enabledCount > 0 ? totalWeight / enabledCount : 0;
+ const estimatedAds = Math.round((maxAds * avgProb) / 100);
+
+ // Determinar nivel
+ let level, badgeClass, alertClass;
+ if (estimatedAds <= 3) {
+ level = 'Baja';
+ badgeClass = 'bg-success';
+ alertClass = 'alert-success';
+ } else if (estimatedAds <= 6) {
+ level = 'Media';
+ badgeClass = 'bg-info';
+ alertClass = 'alert-info';
+ } else if (estimatedAds <= 10) {
+ level = 'Alta';
+ badgeClass = 'bg-warning';
+ alertClass = 'alert-warning';
+ } else {
+ level = 'Muy Alta';
+ badgeClass = 'bg-danger';
+ alertClass = 'alert-danger';
+ }
+
+ elements.densityLevel.textContent = level;
+ elements.densityBadge.textContent = '~' + estimatedAds + ' ads';
+ elements.densityBadge.className = 'badge ' + badgeClass + ' ms-1';
+ elements.densityIndicator.className = 'alert ' + alertClass + ' small mb-3';
+
+ // Mostrar/ocultar warning de densidad alta
+ if (estimatedAds > 10) {
+ elements.highDensityWarning.classList.remove('d-none');
+ } else {
+ elements.highDensityWarning.classList.add('d-none');
+ }
+ }
+
+ /**
+ * Aplica un preset de modo
+ */
+ function applyPreset(presetName) {
+ const preset = modePresets[presetName];
+ if (!preset) return;
+
+ isApplyingPreset = true;
+
+ // Aplicar max ads y spacing
+ if (elements.maxAds) elements.maxAds.value = preset.maxAds;
+ if (elements.minSpacing) elements.minSpacing.value = preset.minSpacing;
+
+ // Aplicar ubicaciones
+ const locationKeys = ['h2', 'h3', 'paragraphs', 'images', 'lists', 'blockquotes', 'tables'];
+ locationKeys.forEach((key, index) => {
+ const loc = elements.locations[index];
+ const presetLoc = preset[key];
+ if (loc.el && presetLoc) {
+ loc.el.checked = presetLoc.enabled;
+ if (loc.prob) loc.prob.value = presetLoc.prob;
+ }
+ });
+
+ isApplyingPreset = false;
+ updateDensityIndicator();
+ }
+
+ /**
+ * Habilita/deshabilita campos seg煤n modo
+ */
+ function toggleFieldsState() {
+ const currentMode = elements.mode.value;
+ const isParagraphsOnly = currentMode === 'paragraphs_only';
+
+ // Toggle details sections
+ if (elements.locationsDetails) {
+ if (isParagraphsOnly) {
+ elements.locationsDetails.removeAttribute('open');
+ } else {
+ elements.locationsDetails.setAttribute('open', '');
+ }
+ }
+ if (elements.limitsDetails) {
+ if (isParagraphsOnly) {
+ elements.limitsDetails.removeAttribute('open');
+ } else {
+ elements.limitsDetails.setAttribute('open', '');
+ }
+ }
+
+ // Toggle campos
+ if (elements.maxAds) elements.maxAds.disabled = isParagraphsOnly;
+ if (elements.minSpacing) elements.minSpacing.disabled = isParagraphsOnly;
+
+ elements.locations.forEach(loc => {
+ if (loc.el) loc.el.disabled = isParagraphsOnly;
+ if (loc.prob) loc.prob.disabled = isParagraphsOnly;
+ });
+
+ // Toggle banner informativo
+ if (elements.paragraphsOnlyBanner) {
+ if (isParagraphsOnly) {
+ elements.paragraphsOnlyBanner.classList.remove('d-none');
+ } else {
+ elements.paragraphsOnlyBanner.classList.add('d-none');
+ }
+ }
+
+ // Toggle descripciones de modo (mostrar solo la activa)
+ if (elements.modeDescriptions) {
+ Object.keys(elements.modeDescriptions).forEach(mode => {
+ const descEl = elements.modeDescriptions[mode];
+ if (descEl) {
+ if (mode === currentMode) {
+ descEl.classList.remove('d-none');
+ } else {
+ descEl.classList.add('d-none');
+ }
+ }
+ });
+ }
+
+ // Actualizar indicador
+ updateDensityIndicator();
+ }
+
+ /**
+ * Maneja cambio de modo
+ */
+ function handleModeChange(e) {
+ const newMode = e.target.value;
+ const currentMode = e.target.dataset.previousValue || 'paragraphs_only';
+
+ // Si cambia de custom a preset, mostrar confirmaci贸n
+ if (currentMode === 'custom' && newMode !== 'custom' && modePresets[newMode]) {
+ showConfirmModal(
+ 'Cambiar modo',
+ 'Al cambiar a un modo preconfigurado se perder谩n tus ajustes personalizados. 驴Continuar?',
+ function() {
+ applyPreset(newMode);
+ toggleFieldsState();
+ e.target.dataset.previousValue = newMode;
+ },
+ function() {
+ // Cancelar: restaurar valor anterior
+ e.target.value = currentMode;
+ }
+ );
+ return;
+ }
+
+ // Aplicar preset si corresponde
+ if (modePresets[newMode]) {
+ applyPreset(newMode);
+ }
+
+ toggleFieldsState();
+ e.target.dataset.previousValue = newMode;
+ }
+
+ /**
+ * Maneja cambios en campos (auto-switch a custom)
+ */
+ function handleFieldChange() {
+ if (isApplyingPreset) return;
+
+ const currentMode = elements.mode.value;
+ if (currentMode !== 'custom' && currentMode !== 'paragraphs_only') {
+ elements.mode.value = 'custom';
+ elements.mode.dataset.previousValue = 'custom';
+ showNotice('info', 'Modo cambiado a "Personalizado" por tus ajustes manuales.');
+ updateDensityIndicator();
+ } else {
+ updateDensityIndicator();
+ }
+ }
+
+ // Inicializar estado
+ elements.mode.dataset.previousValue = elements.mode.value;
+ toggleFieldsState();
+ updateDensityIndicator();
+
+ // Event listeners
+ elements.mode.addEventListener('change', handleModeChange);
+
+ if (elements.maxAds) {
+ elements.maxAds.addEventListener('change', handleFieldChange);
+ }
+ if (elements.minSpacing) {
+ elements.minSpacing.addEventListener('change', handleFieldChange);
+ }
+
+ elements.locations.forEach(loc => {
+ if (loc.el) {
+ loc.el.addEventListener('change', handleFieldChange);
+ }
+ if (loc.prob) {
+ loc.prob.addEventListener('change', handleFieldChange);
+ }
+ });
+ }
+
+ /**
+ * Muestra un modal de confirmaci贸n con callback de cancelaci贸n
+ */
+ function showConfirmModal(title, message, onConfirm, onCancel) {
+ // Crear modal si no existe
+ let modal = document.getElementById('roiConfirmModal');
+ if (!modal) {
+ const modalHTML = `
+
+
+
+
+
+ Mensaje de confirmaci贸n
+
+
+
+
+
+ `;
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
+ modal = document.getElementById('roiConfirmModal');
+ }
+
+ // Actualizar contenido
+ document.getElementById('roiConfirmModalTitle').textContent = title;
+ document.getElementById('roiConfirmModalBody').textContent = message;
+
+ // Configurar callback de confirmaci贸n
+ const confirmButton = document.getElementById('roiConfirmModalConfirm');
+ const newConfirmButton = confirmButton.cloneNode(true);
+ confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
+
+ newConfirmButton.addEventListener('click', function() {
+ const bsModal = bootstrap.Modal.getInstance(modal);
+ bsModal.hide();
+ if (typeof onConfirm === 'function') {
+ onConfirm();
+ }
+ });
+
+ // Configurar callback de cancelaci贸n
+ if (typeof onCancel === 'function') {
+ modal.addEventListener('hidden.bs.modal', function handler() {
+ modal.removeEventListener('hidden.bs.modal', handler);
+ // Solo llamar onCancel si no fue por confirmaci贸n
+ if (!modal.dataset.confirmed) {
+ onCancel();
+ }
+ delete modal.dataset.confirmed;
+ });
+
+ newConfirmButton.addEventListener('click', function() {
+ modal.dataset.confirmed = 'true';
+ });
+ }
+
+ // Mostrar modal
+ const bsModal = new bootstrap.Modal(modal);
+ bsModal.show();
+ }
+
})();
diff --git a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php
index 109f21a9..a1746997 100644
--- a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php
+++ b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php
@@ -9,13 +9,30 @@ use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
* Inyecta anuncios dentro del contenido del post
* via filtro the_content
*
- * Soporta:
- * - Modo aleatorio (random) con posiciones variables
- * - Configuracion de 1-8 ads maximo
- * - Espacio minimo entre anuncios
+ * Soporta dos modos:
+ * - Solo parrafos: Logica clasica solo con parrafos (usa config de behavior)
+ * - Avanzado: Multiples tipos de elementos (H2, H3, p, img, lists, blockquotes, tables)
+ *
+ * El modo se determina por incontent_mode:
+ * - "paragraphs_only": usa config de behavior (insercion solo en parrafos)
+ * - Otros: usa config de incontent_advanced
*/
final class ContentAdInjector
{
+ /**
+ * Prioridades de elementos para seleccion
+ * Mayor = mas importante
+ */
+ private const ELEMENT_PRIORITIES = [
+ 'h2' => 10,
+ 'p' => 8,
+ 'h3' => 7,
+ 'image' => 6,
+ 'list' => 5,
+ 'blockquote' => 4,
+ 'table' => 3,
+ ];
+
public function __construct(
private array $settings,
private AdsensePlacementRenderer $renderer
@@ -26,17 +43,32 @@ final class ContentAdInjector
*/
public function inject(string $content): string
{
- if (!($this->settings['behavior']['post_content_enabled'] ?? false)) {
- return $content;
- }
-
- // Verificar longitud minima
+ // PASO 0: Validar longitud minima (aplica a todos los modos)
$minLength = (int)($this->settings['forms']['min_content_length'] ?? 500);
if (strlen(strip_tags($content)) < $minLength) {
return $content;
}
- // Obtener configuracion
+ // Determinar modo de operacion
+ $mode = $this->settings['incontent_advanced']['incontent_mode'] ?? 'paragraphs_only';
+
+ if ($mode === 'paragraphs_only') {
+ return $this->injectParagraphsOnly($content);
+ }
+
+ return $this->injectAdvanced($content);
+ }
+
+ /**
+ * Modo solo parrafos: logica clasica que inserta anuncios unicamente despues de parrafos
+ */
+ private function injectParagraphsOnly(string $content): string
+ {
+ if (!($this->settings['behavior']['post_content_enabled'] ?? false)) {
+ return $content;
+ }
+
+ // Obtener configuracion de behavior (modo solo parrafos)
$minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1);
$maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3);
$afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3);
@@ -58,7 +90,7 @@ final class ContentAdInjector
}
// Calcular posiciones de insercion
- $adPositions = $this->calculateAdPositions(
+ $adPositions = $this->calculateParagraphsOnlyPositions(
$totalParagraphs,
$afterParagraphs,
$minBetween,
@@ -72,9 +104,354 @@ final class ContentAdInjector
}
// Reconstruir contenido con anuncios insertados
- return $this->buildContentWithAds($paragraphs, $adPositions);
+ return $this->buildParagraphsOnlyContent($paragraphs, $adPositions);
}
+ /**
+ * Modo avanzado: multiples tipos de elementos
+ */
+ private function injectAdvanced(string $content): string
+ {
+ $config = $this->settings['incontent_advanced'] ?? [];
+
+ // Obtener configuracion
+ $maxAds = (int)($config['incontent_max_total_ads'] ?? 8);
+ $minSpacing = (int)($config['incontent_min_spacing'] ?? 3);
+ $priorityMode = $config['incontent_priority_mode'] ?? 'position';
+ $format = $config['incontent_format'] ?? 'in-article';
+
+ // PASO 1: Escanear contenido para encontrar todas las ubicaciones
+ $locations = $this->scanContent($content);
+
+ if (empty($locations)) {
+ return $content;
+ }
+
+ // PASO 2: Filtrar por configuracion (enabled)
+ $locations = $this->filterByEnabled($locations, $config);
+
+ if (empty($locations)) {
+ return $content;
+ }
+
+ // PASO 3: Aplicar probabilidad deterministica
+ $postId = get_the_ID() ?: 0;
+ $locations = $this->applyProbability($locations, $config, $postId);
+
+ if (empty($locations)) {
+ return $content;
+ }
+
+ // PASO 4-5: Filtrar por espaciado y limite (segun priority_mode)
+ if ($priorityMode === 'priority') {
+ $locations = $this->filterByPriorityFirst($locations, $minSpacing, $maxAds);
+ } else {
+ $locations = $this->filterByPositionFirst($locations, $minSpacing, $maxAds);
+ }
+
+ if (empty($locations)) {
+ return $content;
+ }
+
+ // Ordenar por posicion para insercion correcta
+ usort($locations, fn($a, $b) => $a['position'] <=> $b['position']);
+
+ // PASO 6: Insertar anuncios
+ return $this->insertAds($content, $locations, $format);
+ }
+
+ /**
+ * PASO 1: Escanea el contenido para encontrar ubicaciones elegibles
+ *
+ * @return array{position: int, type: string, tag: string, element_index: int}[]
+ */
+ private function scanContent(string $content): array
+ {
+ $locations = [];
+ $elementIndex = 0;
+
+ // Regex para encontrar tags de cierre de elementos de bloque
+ $pattern = '/(<\/(?:p|h2|h3|figure|ul|ol|table|blockquote)>)/i';
+
+ // Encontrar todas las coincidencias con sus posiciones
+ if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
+ foreach ($matches[0] as $match) {
+ $tag = strtolower($match[0]);
+ $position = $match[1] + strlen($match[0]); // Posicion despues del tag
+
+ $type = $this->getTypeFromTag($tag);
+ if ($type) {
+ $locations[] = [
+ 'position' => $position,
+ 'type' => $type,
+ 'tag' => $tag,
+ 'element_index' => $elementIndex++,
+ ];
+ }
+ }
+ }
+
+ // Detectar imagenes standalone (no dentro de figure)
+ $locations = array_merge($locations, $this->scanStandaloneImages($content, $elementIndex));
+
+ // Validar listas (minimo 3 items)
+ $locations = $this->validateLists($content, $locations);
+
+ // Ordenar por posicion
+ usort($locations, fn($a, $b) => $a['position'] <=> $b['position']);
+
+ // Reasignar indices de elemento
+ foreach ($locations as $i => &$loc) {
+ $loc['element_index'] = $i;
+ }
+
+ return $locations;
+ }
+
+ /**
+ * Convierte tag de cierre a tipo de elemento
+ */
+ private function getTypeFromTag(string $tag): ?string
+ {
+ return match ($tag) {
+ '
' => 'p',
+ '' => 'h2',
+ '' => 'h3',
+ '' => 'image',
+ '', '' => 'list',
+ '' => 'table',
+ '' => 'blockquote',
+ default => null,
+ };
+ }
+
+ /**
+ * Detecta imagenes que no estan dentro de figure
+ */
+ private function scanStandaloneImages(string $content, int $startIndex): array
+ {
+ $locations = [];
+
+ // Encontrar todas las imagenes con sus posiciones
+ if (!preg_match_all('/ ]*>/i', $content, $matches, PREG_OFFSET_CAPTURE)) {
+ return $locations;
+ }
+
+ foreach ($matches[0] as $match) {
+ $imgTag = $match[0];
+ $imgPosition = $match[1];
+
+ // Verificar si hay un abierto antes de esta imagen
+ $contentBefore = substr($content, 0, $imgPosition);
+ $lastFigureOpen = strrpos($contentBefore, '');
+
+ // Si hay figure abierto sin cerrar, esta imagen esta dentro de figure
+ if ($lastFigureOpen !== false && ($lastFigureClose === false || $lastFigureClose < $lastFigureOpen)) {
+ continue; // Ignorar, se contara con
+ }
+
+ // Imagen standalone - calcular posicion despues del tag
+ $endPosition = $imgPosition + strlen($imgTag);
+
+ // Si la imagen esta seguida de o similar, usar esa posicion
+ $contentAfter = substr($content, $endPosition, 20);
+ if (preg_match('/^\s*<\/p>/i', $contentAfter, $closeMatch)) {
+ // La imagen esta dentro de un parrafo, no es standalone
+ continue;
+ }
+
+ $locations[] = [
+ 'position' => $endPosition,
+ 'type' => 'image',
+ 'tag' => $imgTag,
+ 'element_index' => $startIndex++,
+ ];
+ }
+
+ return $locations;
+ }
+
+ /**
+ * Valida que las listas tengan minimo 3 items
+ */
+ private function validateLists(string $content, array $locations): array
+ {
+ return array_filter($locations, function ($loc) use ($content) {
+ if ($loc['type'] !== 'list') {
+ return true;
+ }
+
+ // Encontrar el contenido de la lista
+ $endPos = $loc['position'];
+ $tag = $loc['tag'];
+ $openTag = str_replace('/', '', $tag); // ->
+
+ // Buscar hacia atras el tag de apertura
+ $contentBefore = substr($content, 0, $endPos);
+ $lastOpen = strrpos($contentBefore, '<' . substr($openTag, 1)); // = 3;
+ });
+ }
+
+ /**
+ * PASO 2: Filtra ubicaciones por campos enabled
+ */
+ private function filterByEnabled(array $locations, array $config): array
+ {
+ $enabledTypes = [];
+
+ $typeMapping = [
+ 'h2' => 'incontent_after_h2_enabled',
+ 'h3' => 'incontent_after_h3_enabled',
+ 'p' => 'incontent_after_paragraphs_enabled',
+ 'image' => 'incontent_after_images_enabled',
+ 'list' => 'incontent_after_lists_enabled',
+ 'blockquote' => 'incontent_after_blockquotes_enabled',
+ 'table' => 'incontent_after_tables_enabled',
+ ];
+
+ foreach ($typeMapping as $type => $field) {
+ if ($config[$field] ?? false) {
+ $enabledTypes[] = $type;
+ }
+ }
+
+ return array_filter($locations, fn($loc) => in_array($loc['type'], $enabledTypes, true));
+ }
+
+ /**
+ * PASO 3: Aplica probabilidad deterministica usando seed del dia
+ */
+ private function applyProbability(array $locations, array $config, int $postId): array
+ {
+ // Calcular seed deterministico
+ $seed = crc32($postId . date('Y-m-d'));
+ mt_srand($seed);
+
+ $probMapping = [
+ 'h2' => 'incontent_after_h2_probability',
+ 'h3' => 'incontent_after_h3_probability',
+ 'p' => 'incontent_after_paragraphs_probability',
+ 'image' => 'incontent_after_images_probability',
+ 'list' => 'incontent_after_lists_probability',
+ 'blockquote' => 'incontent_after_blockquotes_probability',
+ 'table' => 'incontent_after_tables_probability',
+ ];
+
+ return array_filter($locations, function ($loc) use ($config, $probMapping) {
+ $field = $probMapping[$loc['type']] ?? null;
+ if (!$field) {
+ return true;
+ }
+
+ $probability = (int)($config[$field] ?? 100);
+ $roll = mt_rand(1, 100);
+
+ return $roll <= $probability;
+ });
+ }
+
+ /**
+ * PASO 4-5 (modo position): Filtrar por espaciado primero, luego por prioridad
+ */
+ private function filterByPositionFirst(array $locations, int $minSpacing, int $maxAds): array
+ {
+ // PASO 4: Ordenar por posicion y filtrar por espaciado
+ usort($locations, fn($a, $b) => $a['element_index'] <=> $b['element_index']);
+
+ $filtered = [];
+ $lastIndex = -999;
+
+ foreach ($locations as $loc) {
+ if ($loc['element_index'] - $lastIndex >= $minSpacing) {
+ $filtered[] = $loc;
+ $lastIndex = $loc['element_index'];
+ }
+ }
+
+ // PASO 5: Si excede max, seleccionar por prioridad
+ if (count($filtered) > $maxAds) {
+ usort($filtered, fn($a, $b) =>
+ (self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
+ );
+ $filtered = array_slice($filtered, 0, $maxAds);
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * PASO 4-5 (modo priority): Ordenar por prioridad primero, luego aplicar espaciado
+ */
+ private function filterByPriorityFirst(array $locations, int $minSpacing, int $maxAds): array
+ {
+ // PASO 4: Ordenar por prioridad
+ usort($locations, fn($a, $b) =>
+ (self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
+ );
+
+ // PASO 5: Filtrar por espaciado en orden de prioridad
+ $selected = [];
+ $usedIndices = [];
+
+ foreach ($locations as $loc) {
+ if (count($selected) >= $maxAds) {
+ break;
+ }
+
+ // Verificar espaciado con ubicaciones ya seleccionadas
+ $violatesSpacing = false;
+ foreach ($usedIndices as $usedIndex) {
+ if (abs($loc['element_index'] - $usedIndex) < $minSpacing) {
+ $violatesSpacing = true;
+ break;
+ }
+ }
+
+ if (!$violatesSpacing) {
+ $selected[] = $loc;
+ $usedIndices[] = $loc['element_index'];
+ }
+ }
+
+ return $selected;
+ }
+
+ /**
+ * PASO 6: Inserta los anuncios en las posiciones calculadas
+ */
+ private function insertAds(string $content, array $locations, string $format): string
+ {
+ // Insertar de atras hacia adelante para no afectar posiciones
+ $locations = array_reverse($locations);
+
+ $adCount = count($locations);
+ $currentAd = $adCount;
+
+ foreach ($locations as $loc) {
+ $adHtml = $this->renderer->renderSlot($this->settings, 'post-content-adv-' . $currentAd);
+ $content = substr_replace($content, $adHtml, $loc['position'], 0);
+ $currentAd--;
+ }
+
+ return $content;
+ }
+
+ // =========================================================================
+ // METODOS LEGACY (sin cambios para backward compatibility)
+ // =========================================================================
+
/**
* Divide el contenido en parrafos preservando el HTML
*/
@@ -103,11 +480,11 @@ final class ContentAdInjector
}
/**
- * Calcula las posiciones donde insertar anuncios
+ * Calcula las posiciones donde insertar anuncios (modo solo parrafos)
*
* @return int[] Indices de parrafos despues de los cuales insertar ads
*/
- private function calculateAdPositions(
+ private function calculateParagraphsOnlyPositions(
int $totalParagraphs,
int $afterFirst,
int $minBetween,
@@ -117,7 +494,6 @@ final class ContentAdInjector
): array {
// Calcular posiciones disponibles respetando el espacio minimo
$availablePositions = [];
- $lastPosition = $afterFirst; // Primera posicion fija
// La primera posicion siempre es despues del parrafo indicado
if ($afterFirst < $totalParagraphs) {
@@ -178,9 +554,9 @@ final class ContentAdInjector
}
/**
- * Reconstruye el contenido insertando anuncios en las posiciones indicadas
+ * Reconstruye el contenido insertando anuncios en las posiciones indicadas (modo solo parrafos)
*/
- private function buildContentWithAds(array $paragraphs, array $adPositions): string
+ private function buildParagraphsOnlyContent(array $paragraphs, array $adPositions): string
{
$newContent = '';
$adsInserted = 0;
diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json
index 93716768..3df9af38 100644
--- a/Schemas/adsense-placement.json
+++ b/Schemas/adsense-placement.json
@@ -1,7 +1,7 @@
{
"component_name": "adsense-placement",
- "version": "1.3.0",
- "description": "Control de AdSense y Google Analytics - Con Anchor y Vignette Ads",
+ "version": "1.4.0",
+ "description": "Control de AdSense y Google Analytics - Con In-Content Ads Avanzado",
"groups": {
"visibility": {
"label": "Activacion",
@@ -113,6 +113,170 @@
}
}
},
+ "incontent_advanced": {
+ "label": "In-Content Ads Avanzado",
+ "priority": 69,
+ "fields": {
+ "incontent_mode": {
+ "type": "select",
+ "label": "Estrategia de insercion",
+ "default": "paragraphs_only",
+ "editable": true,
+ "options": {
+ "paragraphs_only": "Solo parrafos (clasico)",
+ "conservative": "Conservador - H2 y parrafos",
+ "balanced": "Balanceado - Multiples elementos",
+ "aggressive": "Intensivo - Todos los elementos",
+ "custom": "Personalizado"
+ },
+ "description": "Define donde se insertaran los anuncios dentro del contenido del post."
+ },
+ "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"
+ }
+ }
+ },
"behavior": {
"label": "Ubicaciones en Posts",
"priority": 70,