diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php index b3c677db..c4abf4ec 100644 --- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php +++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php @@ -37,8 +37,11 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface 'adsense-placementPostTopEnabled' => ['group' => 'behavior', 'attribute' => 'post_top_enabled'], 'adsense-placementPostTopFormat' => ['group' => 'behavior', 'attribute' => 'post_top_format'], 'adsense-placementPostContentEnabled' => ['group' => 'behavior', 'attribute' => 'post_content_enabled'], - 'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'], + 'adsense-placementPostContentRandomMode' => ['group' => 'behavior', 'attribute' => 'post_content_random_mode'], + 'adsense-placementPostContentMinAds' => ['group' => 'behavior', 'attribute' => 'post_content_min_ads'], 'adsense-placementPostContentMaxAds' => ['group' => 'behavior', 'attribute' => 'post_content_max_ads'], + 'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'], + 'adsense-placementPostContentMinParagraphsBetween' => ['group' => 'behavior', 'attribute' => 'post_content_min_paragraphs_between'], 'adsense-placementPostContentFormat' => ['group' => 'behavior', 'attribute' => 'post_content_format'], 'adsense-placementPostBottomEnabled' => ['group' => 'behavior', 'attribute' => 'post_bottom_enabled'], 'adsense-placementPostBottomFormat' => ['group' => 'behavior', 'attribute' => 'post_bottom_format'], diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php index a7786657..ba21ba02 100644 --- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php +++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php @@ -7,6 +7,11 @@ use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; /** * FormBuilder para AdSense Placement y Google Analytics + * + * Panel reorganizado con: + * - Diagrama visual de ubicaciones + * - Secciones colapsables + * - In-content ads configurables (1-8 random) */ final class AdsensePlacementFormBuilder { @@ -27,7 +32,7 @@ final class AdsensePlacementFormBuilder $html .= ' AdSense y Analytics'; $html .= ' '; $html .= '
'; - $html .= ' Configura Google AdSense y Google Analytics'; + $html .= ' Configura Google AdSense y Analytics con ubicaciones visuales'; $html .= '
'; $html .= ' '; $html .= ' '; @@ -36,18 +41,19 @@ final class AdsensePlacementFormBuilder // LAYOUT 2 COLUMNAS $html .= 'Slots por tipo de anuncio:
'; + + // Slots con descripciones claras + $html .= 'Anuncios fijos en los espacios laterales del viewport. Solo visibles en pantallas >= 1600px.
'; + $html .= 'Anuncios fijos en los margenes del viewport. Solo en pantallas muy anchas.
'; // Master switch $railEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_ads_enabled', false); @@ -262,66 +479,28 @@ final class AdsensePlacementFormBuilder return $html; } - private function buildArchiveLocationsGroup(string $cid): string - { - $html = 'Ubicaciones Globales
'; - - // Global locations - $headerBelowEnabled = $this->renderer->getFieldValue($cid, 'layout', 'header_below_enabled', false); - $html .= $this->buildSwitch($cid . 'HeaderBelowEnabled', 'Debajo del header (global)', $headerBelowEnabled); - - $footerAboveEnabled = $this->renderer->getFieldValue($cid, 'layout', 'footer_above_enabled', false); - $html .= $this->buildSwitch($cid . 'FooterAboveEnabled', 'Arriba del footer (global)', $footerAboveEnabled); - - // Global format - $html .= $this->buildSelect($cid . 'GlobalFormat', 'Formato para globales', - $this->renderer->getFieldValue($cid, 'layout', 'global_format', 'auto'), - ['auto' => 'Auto', 'display-large' => 'Display Large (970x250)'] - ); - - $html .= 'Rendimiento:
'; + $delayEnabled = $this->renderer->getFieldValue($cid, 'forms', 'delay_enabled', true); - $html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga de anuncios', $delayEnabled, 'bi-hourglass-split'); + $html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga (mejor PageSpeed)', $delayEnabled, 'bi-hourglass-split'); $delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000'); $html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout); diff --git a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php index b1f2ec3c..109f21a9 100644 --- a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php +++ b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php @@ -8,6 +8,11 @@ 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 */ final class ContentAdInjector { @@ -31,40 +36,166 @@ final class ContentAdInjector return $content; } + // Obtener configuracion + $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); - $maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 2); + $minBetween = (int)($this->settings['behavior']['post_content_min_paragraphs_between'] ?? 4); + $randomMode = ($this->settings['behavior']['post_content_random_mode'] ?? true) === true; + + // Validar min <= max + if ($minAds > $maxAds) { + $minAds = $maxAds; + } // Dividir contenido en parrafos - $paragraphs = explode('', $content); + $paragraphs = $this->splitIntoParagraphs($content); $totalParagraphs = count($paragraphs); + // Necesitamos al menos afterParagraphs + 1 parrafos if ($totalParagraphs < $afterParagraphs + 1) { return $content; } - $adsInserted = 0; + // Calcular posiciones de insercion + $adPositions = $this->calculateAdPositions( + $totalParagraphs, + $afterParagraphs, + $minBetween, + $minAds, + $maxAds, + $randomMode + ); + + if (empty($adPositions)) { + return $content; + } + + // Reconstruir contenido con anuncios insertados + return $this->buildContentWithAds($paragraphs, $adPositions); + } + + /** + * Divide el contenido en parrafos preservando el HTML + */ + private function splitIntoParagraphs(string $content): array + { + // Dividir por , pero mantener el tag + $parts = preg_split('/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + + $paragraphs = []; + $current = ''; + + foreach ($parts as $part) { + $current .= $part; + if (strtolower($part) === '') { + $paragraphs[] = $current; + $current = ''; + } + } + + // Si hay contenido restante (sin cerrar), agregarlo + if (!empty(trim($current))) { + $paragraphs[] = $current; + } + + return $paragraphs; + } + + /** + * Calcula las posiciones donde insertar anuncios + * + * @return int[] Indices de parrafos despues de los cuales insertar ads + */ + private function calculateAdPositions( + int $totalParagraphs, + int $afterFirst, + int $minBetween, + int $minAds, + int $maxAds, + bool $randomMode + ): 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) { + $availablePositions[] = $afterFirst; + } + + // Calcular posiciones adicionales respetando minBetween + $nextPosition = $afterFirst + $minBetween; + while ($nextPosition < $totalParagraphs - 1) { // -1 para no insertar al final + $availablePositions[] = $nextPosition; + $nextPosition += $minBetween; + } + + // Determinar cuantos ads insertar + $maxPossible = count($availablePositions); + if ($maxPossible === 0) { + return []; + } + + // Limitar por maxAds y lo que el contenido permite + $actualMax = min($maxAds, $maxPossible); + $actualMin = min($minAds, $actualMax); + + // Determinar cantidad final de ads + if ($randomMode) { + // En modo random, elegir cantidad aleatoria entre min y max + $numAds = rand($actualMin, $actualMax); + } else { + // En modo fijo, usar el maximo posible + $numAds = $actualMax; + } + + if ($numAds === 0) { + return []; + } + + // Seleccionar posiciones + if ($randomMode && $numAds < $maxPossible) { + // Modo random: elegir posiciones aleatorias + // Siempre incluir la primera posicion + $selectedPositions = [$availablePositions[0]]; + + if ($numAds > 1) { + // Elegir aleatoriamente del resto + $remainingPositions = array_slice($availablePositions, 1); + shuffle($remainingPositions); + $additionalPositions = array_slice($remainingPositions, 0, $numAds - 1); + $selectedPositions = array_merge($selectedPositions, $additionalPositions); + } + + // Ordenar para insertar en orden correcto + sort($selectedPositions); + return $selectedPositions; + } else { + // Modo fijo o todas las posiciones necesarias + return array_slice($availablePositions, 0, $numAds); + } + } + + /** + * Reconstruye el contenido insertando anuncios en las posiciones indicadas + */ + private function buildContentWithAds(array $paragraphs, array $adPositions): string + { $newContent = ''; + $adsInserted = 0; foreach ($paragraphs as $index => $paragraph) { $newContent .= $paragraph; - if ($index < $totalParagraphs - 1) { - $newContent .= ''; - } - + // Verificar si debemos insertar un ad despues de este parrafo + // El indice es 0-based, las posiciones son 1-based (parrafo #3 = index 2) $paragraphNumber = $index + 1; - // Primer anuncio despues del parrafo indicado - if ($paragraphNumber === $afterParagraphs && $adsInserted < $maxAds) { - $newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1)); - $adsInserted++; - } - - // Segundo anuncio a mitad del contenido restante - $midPoint = $afterParagraphs + (int)(($totalParagraphs - $afterParagraphs) / 2); - if ($paragraphNumber === $midPoint && $adsInserted < $maxAds && $maxAds > 1) { - $newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1)); + if (in_array($paragraphNumber, $adPositions, true)) { $adsInserted++; + $adHtml = $this->renderer->renderSlot($this->settings, 'post-content-' . $adsInserted); + $newContent .= $adHtml; } } diff --git a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php index fb6c2c57..ea61cff7 100644 --- a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php +++ b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php @@ -105,6 +105,15 @@ final class AdsensePlacementRenderer { $locationKey = str_replace('-', '_', $location); + // Manejar ubicaciones de in-content (post_content_1, post_content_2, etc.) + if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) { + // In-content ads heredan la configuracion de post_content + return [ + 'enabled' => $settings['behavior']['post_content_enabled'] ?? false, + 'format' => $settings['behavior']['post_content_format'] ?? 'in-article', + ]; + } + // Mapeo de ubicaciones a grupos y campos $locationMap = [ 'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'], diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json index da1189d2..d06987d8 100644 --- a/Schemas/adsense-placement.json +++ b/Schemas/adsense-placement.json @@ -1,10 +1,10 @@ { "component_name": "adsense-placement", - "version": "1.1.0", - "description": "Control de AdSense y Google Analytics", + "version": "1.2.0", + "description": "Control de AdSense y Google Analytics - Panel reorganizado", "groups": { "visibility": { - "label": "Visibilidad AdSense", + "label": "Activacion", "priority": 10, "fields": { "is_enabled": { @@ -130,18 +130,43 @@ "editable": true, "description": "Inserta anuncios automaticamente entre parrafos" }, - "post_content_after_paragraphs": { - "type": "text", - "label": "Despues del parrafo #", - "default": "3", + "post_content_random_mode": { + "type": "boolean", + "label": "Modo aleatorio", + "default": true, "editable": true, - "description": "Numero de parrafo despues del cual insertar" + "description": "Inserta ads en posiciones aleatorias (mejor UX)" + }, + "post_content_min_ads": { + "type": "select", + "label": "Minimo de ads", + "default": "1", + "editable": true, + "options": ["1", "2", "3", "4"], + "description": "Cantidad minima de anuncios a mostrar" }, "post_content_max_ads": { + "type": "select", + "label": "Maximo de ads", + "default": "3", + "editable": true, + "options": ["1", "2", "3", "4", "5", "6", "7", "8"], + "description": "Cantidad maxima de anuncios a mostrar" + }, + "post_content_after_paragraphs": { "type": "text", - "label": "Maximo ads en contenido", - "default": "2", - "editable": true + "label": "Primer ad despues del parrafo #", + "default": "3", + "editable": true, + "description": "Numero de parrafo despues del cual insertar el primer ad" + }, + "post_content_min_paragraphs_between": { + "type": "select", + "label": "Parrafos minimos entre ads", + "default": "4", + "editable": true, + "options": ["2", "3", "4", "5", "6"], + "description": "Espacio minimo entre anuncios consecutivos" }, "post_content_format": { "type": "select", @@ -270,7 +295,7 @@ } }, "forms": { - "label": "Exclusiones", + "label": "Exclusiones y Rendimiento", "priority": 90, "fields": { "exclude_categories": {