diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php new file mode 100644 index 00000000..5de53bf2 --- /dev/null +++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php @@ -0,0 +1,70 @@ + ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'adsense-placementDisableAutoAds' => ['group' => 'visibility', 'attribute' => 'disable_auto_ads'], + 'adsense-placementShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'adsense-placementShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + + // CONTENT (Credentials) + 'adsense-placementPublisherId' => ['group' => 'content', 'attribute' => 'publisher_id'], + 'adsense-placementSlotDisplay' => ['group' => 'content', 'attribute' => 'slot_display'], + 'adsense-placementSlotAuto' => ['group' => 'content', 'attribute' => 'slot_auto'], + 'adsense-placementSlotAutorelaxed' => ['group' => 'content', 'attribute' => 'slot_autorelaxed'], + 'adsense-placementSlotInarticle' => ['group' => 'content', 'attribute' => 'slot_inarticle'], + 'adsense-placementSlotSkyscraper' => ['group' => 'content', 'attribute' => 'slot_skyscraper'], + + // BEHAVIOR (Post locations + formats) + '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-placementPostContentMaxAds' => ['group' => 'behavior', 'attribute' => 'post_content_max_ads'], + 'adsense-placementPostContentFormat' => ['group' => 'behavior', 'attribute' => 'post_content_format'], + 'adsense-placementPostBottomEnabled' => ['group' => 'behavior', 'attribute' => 'post_bottom_enabled'], + 'adsense-placementPostBottomFormat' => ['group' => 'behavior', 'attribute' => 'post_bottom_format'], + 'adsense-placementAfterRelatedEnabled' => ['group' => 'behavior', 'attribute' => 'after_related_enabled'], + 'adsense-placementAfterRelatedFormat' => ['group' => 'behavior', 'attribute' => 'after_related_format'], + + // BEHAVIOR (Rail Ads) + 'adsense-placementRailAdsEnabled' => ['group' => 'behavior', 'attribute' => 'rail_ads_enabled'], + 'adsense-placementRailLeftEnabled' => ['group' => 'behavior', 'attribute' => 'rail_left_enabled'], + 'adsense-placementRailRightEnabled' => ['group' => 'behavior', 'attribute' => 'rail_right_enabled'], + 'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'], + 'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'], + + // LAYOUT (Archive/Global locations + formats) + 'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'], + 'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'], + 'adsense-placementArchiveBetweenEvery' => ['group' => 'layout', 'attribute' => 'archive_between_every'], + 'adsense-placementArchiveBottomEnabled' => ['group' => 'layout', 'attribute' => 'archive_bottom_enabled'], + 'adsense-placementArchiveFormat' => ['group' => 'layout', 'attribute' => 'archive_format'], + 'adsense-placementHeaderBelowEnabled' => ['group' => 'layout', 'attribute' => 'header_below_enabled'], + 'adsense-placementFooterAboveEnabled' => ['group' => 'layout', 'attribute' => 'footer_above_enabled'], + 'adsense-placementGlobalFormat' => ['group' => 'layout', 'attribute' => 'global_format'], + + // FORMS (Exclusions + Delay) + 'adsense-placementExcludeCategories' => ['group' => 'forms', 'attribute' => 'exclude_categories'], + 'adsense-placementExcludePostTypes' => ['group' => 'forms', 'attribute' => 'exclude_post_types'], + 'adsense-placementExcludePostIds' => ['group' => 'forms', 'attribute' => 'exclude_post_ids'], + 'adsense-placementMinContentLength' => ['group' => 'forms', 'attribute' => 'min_content_length'], + 'adsense-placementDelayEnabled' => ['group' => 'forms', 'attribute' => 'delay_enabled'], + 'adsense-placementDelayTimeout' => ['group' => 'forms', 'attribute' => 'delay_timeout'], + ]; + } +} diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php new file mode 100644 index 00000000..1a13cf6f --- /dev/null +++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php @@ -0,0 +1,381 @@ +'; + $html .= '
'; + $html .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Control de Anuncios AdSense'; + $html .= '

'; + $html .= '

'; + $html .= ' Configura ubicaciones manuales de anuncios para evitar Auto Ads'; + $html .= '

'; + $html .= '
'; + $html .= '
'; + $html .= ''; + + // LAYOUT 2 COLUMNAS + $html .= '
'; + + // COLUMNA IZQUIERDA + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildCredentialsGroup($componentId); + $html .= $this->buildPostLocationsGroup($componentId); + $html .= '
'; + + // COLUMNA DERECHA + $html .= '
'; + $html .= $this->buildRailAdsGroup($componentId); + $html .= $this->buildArchiveLocationsGroup($componentId); + $html .= $this->buildExclusionsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildVisibilityGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Activacion y Visibilidad'; + $html .= '
'; + + // Switch: Enabled + $enabled = $this->renderer->getFieldValue($cid, 'visibility', 'is_enabled', false); + $html .= $this->buildSwitch($cid . 'Enabled', 'Activar Placement Manual', $enabled, 'bi-power'); + + // Switch: Disable Auto Ads + $disableAuto = $this->renderer->getFieldValue($cid, 'visibility', 'disable_auto_ads', true); + $html .= $this->buildSwitch($cid . 'DisableAutoAds', 'Deshabilitar Auto Ads de Google', $disableAuto, 'bi-shield-x'); + + // Switch: Show on Mobile + $showMobile = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch($cid . 'ShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone'); + + // Switch: Show on Desktop + $showDesktop = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch($cid . 'ShowOnDesktop', 'Mostrar en escritorio', $showDesktop, 'bi-display'); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildCredentialsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Credenciales AdSense'; + $html .= '
'; + + // Publisher ID + $pubId = $this->renderer->getFieldValue($cid, 'content', 'publisher_id', 'ca-pub-8476420265998726'); + $html .= $this->buildTextInput($cid . 'PublisherId', 'Publisher ID', $pubId, 'ca-pub-XXXXX'); + + // Slots (grid 2 columnas) + $html .= '
'; + $html .= '
'; + $slotDisplay = $this->renderer->getFieldValue($cid, 'content', 'slot_display', '2873062302'); + $html .= $this->buildTextInput($cid . 'SlotDisplay', 'Slot Display', $slotDisplay); + $html .= '
'; + $html .= '
'; + $slotAuto = $this->renderer->getFieldValue($cid, 'content', 'slot_auto', '8471732096'); + $html .= $this->buildTextInput($cid . 'SlotAuto', 'Slot Auto', $slotAuto); + $html .= '
'; + $html .= '
'; + $slotRelaxed = $this->renderer->getFieldValue($cid, 'content', 'slot_autorelaxed', '9205569855'); + $html .= $this->buildTextInput($cid . 'SlotAutorelaxed', 'Slot Autorelaxed', $slotRelaxed); + $html .= '
'; + $html .= '
'; + $slotInArticle = $this->renderer->getFieldValue($cid, 'content', 'slot_inarticle', '7285187368'); + $html .= $this->buildTextInput($cid . 'SlotInarticle', 'Slot In-Article', $slotInArticle); + $html .= '
'; + $html .= '
'; + $slotSkyscraper = $this->renderer->getFieldValue($cid, 'content', 'slot_skyscraper', ''); + $html .= $this->buildTextInput($cid . 'SlotSkyscraper', 'Slot Skyscraper (Rail Ads)', $slotSkyscraper); + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildPostLocationsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Ubicaciones en Posts'; + $html .= '
'; + + // Post Top + $postTopEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_top_enabled', true); + $html .= $this->buildSwitch($cid . 'PostTopEnabled', 'Despues de Featured Image', $postTopEnabled); + $html .= $this->buildSelect($cid . 'PostTopFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'post_top_format', 'auto'), + ['auto' => 'Auto (responsive)', 'in-article' => 'In-Article', 'display' => 'Display (728x90)', 'display-large' => 'Display Large (970x250)'] + ); + + // Post Content + $postContentEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_enabled', false); + $html .= $this->buildSwitch($cid . 'PostContentEnabled', 'Insertar dentro del contenido', $postContentEnabled); + + $html .= '
'; + $html .= '
'; + $afterPara = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_after_paragraphs', '3'); + $html .= $this->buildTextInput($cid . 'PostContentAfterParagraphs', 'Despues parrafo #', $afterPara); + $html .= '
'; + $html .= '
'; + $maxAds = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_max_ads', '2'); + $html .= $this->buildTextInput($cid . 'PostContentMaxAds', 'Max ads', $maxAds); + $html .= '
'; + $html .= '
'; + $html .= $this->buildSelect($cid . 'PostContentFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'post_content_format', 'in-article'), + ['in-article' => 'In-Article', 'auto' => 'Auto'] + ); + $html .= '
'; + $html .= '
'; + + // Post Bottom + $postBottomEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_enabled', true); + $html .= $this->buildSwitch($cid . 'PostBottomEnabled', 'Despues del contenido', $postBottomEnabled); + $html .= $this->buildSelect($cid . 'PostBottomFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_format', 'auto'), + ['auto' => 'Auto', 'in-article' => 'In-Article', 'display' => 'Display'] + ); + + // After Related + $afterRelatedEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'after_related_enabled', false); + $html .= $this->buildSwitch($cid . 'AfterRelatedEnabled', 'Despues de Related Posts', $afterRelatedEnabled); + $html .= $this->buildSelect($cid . 'AfterRelatedFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'after_related_format', 'autorelaxed'), + ['autorelaxed' => 'Autorelaxed', 'auto' => 'Auto'] + ); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildRailAdsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Rail Ads (Margenes Laterales)'; + $html .= '
'; + $html .= '

Anuncios fijos en los espacios laterales del viewport. Solo visibles en pantallas >= 1600px.

'; + + // Master switch + $railEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_ads_enabled', false); + $html .= $this->buildSwitch($cid . 'RailAdsEnabled', 'Activar Rail Ads', $railEnabled, 'bi-power'); + + // Left/Right toggles + $html .= '
'; + $html .= '
'; + $leftEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_left_enabled', true); + $html .= $this->buildSwitch($cid . 'RailLeftEnabled', 'Rail izquierdo', $leftEnabled); + $html .= '
'; + $html .= '
'; + $rightEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_right_enabled', true); + $html .= $this->buildSwitch($cid . 'RailRightEnabled', 'Rail derecho', $rightEnabled); + $html .= '
'; + $html .= '
'; + + // Format select + $railFormat = $this->renderer->getFieldValue($cid, 'behavior', 'rail_format', 'skyscraper'); + $html .= $this->buildSelect($cid . 'RailFormat', 'Formato', + $railFormat, + ['skyscraper' => 'Skyscraper (160x600)', 'half-page' => 'Half Page (300x600)'] + ); + + // Top offset + $topOffset = $this->renderer->getFieldValue($cid, 'behavior', 'rail_top_offset', '150'); + $html .= $this->buildTextInput($cid . 'RailTopOffset', 'Distancia desde arriba (px)', $topOffset); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildArchiveLocationsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Ubicaciones Archives/Globales'; + $html .= '
'; + + // Archive locations + $archiveTopEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_top_enabled', false); + $html .= $this->buildSwitch($cid . 'ArchiveTopEnabled', 'Arriba del listado', $archiveTopEnabled); + + $archiveBetweenEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_between_enabled', false); + $html .= $this->buildSwitch($cid . 'ArchiveBetweenEnabled', 'Entre posts del listado', $archiveBetweenEnabled); + + $archiveEvery = $this->renderer->getFieldValue($cid, 'layout', 'archive_between_every', '4'); + $html .= $this->buildTextInput($cid . 'ArchiveBetweenEvery', 'Mostrar cada X posts', $archiveEvery); + + $archiveBottomEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_bottom_enabled', false); + $html .= $this->buildSwitch($cid . 'ArchiveBottomEnabled', 'Abajo del listado', $archiveBottomEnabled); + + // Archive format (aplica a todas las ubicaciones archive) + $html .= $this->buildSelect($cid . 'ArchiveFormat', 'Formato para archives', + $this->renderer->getFieldValue($cid, 'layout', 'archive_format', 'autorelaxed'), + ['autorelaxed' => 'Autorelaxed', 'auto' => 'Auto'] + ); + + $html .= '
'; + $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 .= '
'; + $html .= '
'; + + return $html; + } + + private function buildExclusionsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Exclusiones y Rendimiento'; + $html .= '
'; + + // Exclusions + $excludeCats = $this->renderer->getFieldValue($cid, 'forms', 'exclude_categories', ''); + $html .= $this->buildTextarea($cid . 'ExcludeCategories', 'Excluir categorias (IDs)', $excludeCats, 'Ej: 5,12,23'); + + $excludeTypes = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_types', ''); + $html .= $this->buildTextarea($cid . 'ExcludePostTypes', 'Excluir tipos de post', $excludeTypes, 'Ej: page,attachment'); + + $excludeIds = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_ids', ''); + $html .= $this->buildTextarea($cid . 'ExcludePostIds', 'Excluir posts (IDs)', $excludeIds, 'Ej: 100,205,310'); + + $minLength = $this->renderer->getFieldValue($cid, 'forms', 'min_content_length', '500'); + $html .= $this->buildTextInput($cid . 'MinContentLength', 'Longitud minima de contenido', $minLength); + + // Delay settings + $delayEnabled = $this->renderer->getFieldValue($cid, 'forms', 'delay_enabled', true); + $html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga de anuncios', $delayEnabled, 'bi-hourglass-split'); + + $delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000'); + $html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + // === HELPERS === + + private function buildSwitch(string $id, string $label, $value, string $icon = ''): string + { + $checked = checked($value, true, false); + $iconHtml = $icon ? '' : ''; + + return sprintf( + '
+
+ + +
+
', + esc_attr($id), $checked, esc_attr($id), $iconHtml, esc_html($label) + ); + } + + private function buildTextInput(string $id, string $label, string $value, string $placeholder = ''): string + { + return sprintf( + '
+ + +
', + esc_attr($id), esc_html($label), esc_attr($id), esc_attr($value), esc_attr($placeholder) + ); + } + + private function buildTextarea(string $id, string $label, string $value, string $placeholder = ''): string + { + return sprintf( + '
+ + +
', + esc_attr($id), esc_html($label), esc_attr($id), esc_attr($placeholder), esc_textarea($value) + ); + } + + private function buildSelect(string $id, string $label, string $value, array $options): string + { + $optionsHtml = ''; + foreach ($options as $optValue => $optLabel) { + $selected = selected($value, $optValue, false); + $optionsHtml .= sprintf( + '', + esc_attr($optValue), + $selected, + esc_html($optLabel) + ); + } + + return sprintf( + '
+ + +
', + esc_attr($id), esc_html($label), esc_attr($id), $optionsHtml + ); + } +} diff --git a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php index 27fddea7..905f5725 100644 --- a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php +++ b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php @@ -111,6 +111,11 @@ final class AdminDashboardRenderer implements DashboardRendererInterface 'label' => 'Theme Settings', 'icon' => 'bi-gear', ], + 'adsense-placement' => [ + 'id' => 'adsense-placement', + 'label' => 'AdSense', + 'icon' => 'bi-megaphone', + ], ]; } diff --git a/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php b/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php index d32c3e90..acdc7790 100644 --- a/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php +++ b/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php @@ -32,6 +32,7 @@ final class FieldMapperProvider 'ContactForm', 'Footer', 'ThemeSettings', + 'AdsensePlacement', ]; public function __construct( diff --git a/Inc/adsense-placement.php b/Inc/adsense-placement.php new file mode 100644 index 00000000..970eb023 --- /dev/null +++ b/Inc/adsense-placement.php @@ -0,0 +1,196 @@ +getComponentSettingsRepository(); + $settings = $repository->getComponentSettings('adsense-placement'); + + if (empty($settings)) { + return ''; + } + + // Verificar exclusiones + if (roi_is_ad_excluded($settings)) { + return ''; + } + + // Obtener renderer desde DIContainer (DIP compliant) + $renderer = $container->getAdsensePlacementRenderer(); + + return $renderer->renderSlot($settings, $location); + + } catch (\Throwable $e) { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI AdSense: ' . $e->getMessage()); + } + return ''; + } +} + +/** + * Verifica si el contenido actual esta excluido + */ +function roi_is_ad_excluded(array $settings): bool +{ + $forms = $settings['forms'] ?? []; + + // Excluir categorias + $excludeCats = array_filter(array_map('trim', explode(',', $forms['exclude_categories'] ?? ''))); + if (!empty($excludeCats) && is_single()) { + $postCats = wp_get_post_categories(get_the_ID()); + if (array_intersect($excludeCats, $postCats)) { + return true; + } + } + + // Excluir tipos de post + $excludeTypes = array_filter(array_map('trim', explode(',', $forms['exclude_post_types'] ?? ''))); + if (!empty($excludeTypes) && in_array(get_post_type(), $excludeTypes, true)) { + return true; + } + + // Excluir posts especificos + $excludeIds = array_filter(array_map('trim', explode(',', $forms['exclude_post_ids'] ?? ''))); + if (!empty($excludeIds) && in_array((string)get_the_ID(), $excludeIds, true)) { + return true; + } + + return false; +} + +/** + * Renderiza los Rail Ads (margenes laterales del viewport) + * Se llama desde wp_footer para inyectar al final del body + * + * NOTA DIP: El renderer se obtiene del DIContainer, NO se instancia directamente. + */ +function roi_render_rail_ads(): string +{ + global $container; + + if ($container === null) { + return ''; + } + + try { + $repository = $container->getComponentSettingsRepository(); + $settings = $repository->getComponentSettings('adsense-placement'); + + if (empty($settings)) { + return ''; + } + + // Verificar exclusiones + if (roi_is_ad_excluded($settings)) { + return ''; + } + + // Obtener renderer desde DIContainer (DIP compliant) + $renderer = $container->getAdsensePlacementRenderer(); + + return $renderer->renderRailAds($settings); + + } catch (\Throwable $e) { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI AdSense Rail Ads: ' . $e->getMessage()); + } + return ''; + } +} + +/** + * Hook para inyectar Rail Ads en el footer + */ +add_action('wp_footer', function() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo roi_render_rail_ads(); +}, 50); + +/** + * Verifica si se debe deshabilitar Auto Ads + */ +function roi_should_disable_auto_ads(): bool +{ + global $container; + + if ($container === null) { + return false; + } + + try { + $repository = $container->getComponentSettingsRepository(); + $settings = $repository->getComponentSettings('adsense-placement'); + + $isEnabled = ($settings['visibility']['is_enabled'] ?? false) === true; + $disableAutoAds = ($settings['visibility']['disable_auto_ads'] ?? true) === true; + + return $isEnabled && $disableAutoAds; + } catch (\Throwable $e) { + return false; + } +} + +/** + * Carga el script principal de AdSense + */ +function roi_enqueue_adsense_script(): void +{ + global $container; + + if ($container === null || is_admin()) { + return; + } + + try { + $repository = $container->getComponentSettingsRepository(); + $settings = $repository->getComponentSettings('adsense-placement'); + + if (!($settings['visibility']['is_enabled'] ?? false)) { + return; + } + + $publisherId = $settings['content']['publisher_id'] ?? ''; + if (empty($publisherId)) { + return; + } + + $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true; + + if ($delayEnabled) { + echo '' . "\n"; + } else { + echo '' . "\n"; + } + + } catch (\Throwable $e) { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI AdSense: ' . $e->getMessage()); + } + } +} +add_action('wp_head', 'roi_enqueue_adsense_script', 5); diff --git a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php new file mode 100644 index 00000000..b1f2ec3c --- /dev/null +++ b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php @@ -0,0 +1,73 @@ +settings['behavior']['post_content_enabled'] ?? false)) { + return $content; + } + + // Verificar longitud minima + $minLength = (int)($this->settings['forms']['min_content_length'] ?? 500); + if (strlen(strip_tags($content)) < $minLength) { + return $content; + } + + $afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3); + $maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 2); + + // Dividir contenido en parrafos + $paragraphs = explode('

', $content); + $totalParagraphs = count($paragraphs); + + if ($totalParagraphs < $afterParagraphs + 1) { + return $content; + } + + $adsInserted = 0; + $newContent = ''; + + foreach ($paragraphs as $index => $paragraph) { + $newContent .= $paragraph; + + if ($index < $totalParagraphs - 1) { + $newContent .= '

'; + } + + $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)); + $adsInserted++; + } + } + + return $newContent; + } +} diff --git a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php new file mode 100644 index 00000000..ba729065 --- /dev/null +++ b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php @@ -0,0 +1,356 @@ +getVisibilityClasses( + $settings['visibility']['show_on_desktop'] ?? true, + $settings['visibility']['show_on_mobile'] ?? true + ); + + if ($visibilityClasses === null) { + return ''; // Ambos false = no renderizar + } + + // 3. Obtener configuracion de la ubicacion + $locationConfig = $this->getLocationConfig($settings, $location); + if (!$locationConfig['enabled']) { + return ''; + } + + // 4. Generar CSS (usando CSSGeneratorService) + $css = $this->cssGenerator->generate( + ".roi-ad-{$location}", + [ + 'display' => 'flex', + 'justify_content' => 'center', + 'margin_top' => '1rem', + 'margin_bottom' => '1rem', + ] + ); + + // 5. Generar HTML del anuncio + $html = $this->buildAdHTML( + $settings, + $locationConfig['format'], + $location, + $visibilityClasses + ); + + return "\n{$html}"; + } + + /** + * Tabla de decision Bootstrap para visibilidad + */ + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if (!$desktop && !$mobile) { + return null; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + /** + * Obtiene configuracion de una ubicacion especifica + */ + private function getLocationConfig(array $settings, string $location): array + { + $locationKey = str_replace('-', '_', $location); + + // Mapeo de ubicaciones a grupos y campos + $locationMap = [ + 'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'], + 'post_bottom' => ['group' => 'behavior', 'enabled' => 'post_bottom_enabled', 'format' => 'post_bottom_format'], + 'after_related' => ['group' => 'behavior', 'enabled' => 'after_related_enabled', 'format' => 'after_related_format'], + 'archive_top' => ['group' => 'layout', 'enabled' => 'archive_top_enabled', 'format' => 'archive_format'], + 'archive_between' => ['group' => 'layout', 'enabled' => 'archive_between_enabled', 'format' => 'archive_format'], + 'archive_bottom' => ['group' => 'layout', 'enabled' => 'archive_bottom_enabled', 'format' => 'archive_format'], + 'header_below' => ['group' => 'layout', 'enabled' => 'header_below_enabled', 'format' => 'global_format'], + 'footer_above' => ['group' => 'layout', 'enabled' => 'footer_above_enabled', 'format' => 'global_format'], + ]; + + if (!isset($locationMap[$locationKey])) { + return ['enabled' => false, 'format' => 'auto']; + } + + $map = $locationMap[$locationKey]; + $group = $settings[$map['group']] ?? []; + + return [ + 'enabled' => $group[$map['enabled']] ?? false, + 'format' => $group[$map['format']] ?? 'auto', + ]; + } + + /** + * Genera HTML del bloque de anuncio + */ + private function buildAdHTML(array $settings, string $format, string $location, string $visClasses): string + { + $publisherId = esc_attr($settings['content']['publisher_id'] ?? ''); + $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true; + + if (empty($publisherId)) { + return ''; + } + + // Obtener slot segun formato + $slotId = $this->getSlotByFormat($settings, $format); + if (empty($slotId)) { + return ''; + } + + $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript'; + $dataAttr = $delayEnabled ? ' data-adsense-push' : ''; + $locationClass = 'roi-ad-' . esc_attr(str_replace('_', '-', $location)); + + return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr); + } + + /** + * Obtiene el slot ID segun el formato + */ + private function getSlotByFormat(array $settings, string $format): string + { + $content = $settings['content'] ?? []; + + return match($format) { + 'display', 'display-large', 'display-square' => $content['slot_display'] ?? '', + 'in-article' => $content['slot_inarticle'] ?? '', + 'autorelaxed' => $content['slot_autorelaxed'] ?? '', + default => $content['slot_auto'] ?? '', + }; + } + + /** + * Genera el markup HTML segun formato de anuncio + * + * EXCEPCION DOCUMENTADA: CSS inline requerido por Google AdSense + * ---------------------------------------------------------------- + * Los atributos style="display:inline-block", style="display:block", + * style="text-align:center", etc. son ESPECIFICACION DE GOOGLE y NO + * pueden generarse via CSSGenerator. + * + * Documentacion oficial: + * - https://support.google.com/adsense/answer/9274516 + * - https://support.google.com/adsense/answer/9183460 + * + * Estos estilos son necesarios para que AdSense funcione correctamente + * y son inyectados tal como Google los especifica en su documentacion. + */ + private function generateAdMarkup( + string $format, + string $client, + string $slot, + string $locationClass, + string $visClasses, + string $scriptType, + string $dataAttr + ): string { + $allClasses = trim("{$locationClass} {$visClasses}"); + + return match($format) { + 'display' => $this->adDisplay($client, $slot, 728, 90, $allClasses, $scriptType, $dataAttr), + 'display-large' => $this->adDisplay($client, $slot, 970, 250, $allClasses, $scriptType, $dataAttr), + 'display-square' => $this->adDisplay($client, $slot, 300, 250, $allClasses, $scriptType, $dataAttr), + 'in-article' => $this->adInArticle($client, $slot, $allClasses, $scriptType, $dataAttr), + 'autorelaxed' => $this->adAutorelaxed($client, $slot, $allClasses, $scriptType, $dataAttr), + default => $this->adAuto($client, $slot, $allClasses, $scriptType, $dataAttr), + }; + } + + private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a): string + { + return sprintf( + '
+ + +
', + esc_attr($cl), $w, $h, esc_attr($c), esc_attr($s), $t, $a + ); + } + + private function adAuto(string $c, string $s, string $cl, string $t, string $a): string + { + return sprintf( + '
+ + +
', + esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a + ); + } + + private function adInArticle(string $c, string $s, string $cl, string $t, string $a): string + { + return sprintf( + '
+ + +
', + esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a + ); + } + + private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a): string + { + return sprintf( + '
+ + +
', + esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a + ); + } + + /** + * Renderiza Rail Ads (anuncios fijos en margenes laterales) + * Se inyectan via wp_footer y usan position: fixed + */ + public function renderRailAds(array $settings): string + { + // Verificar si Rail Ads estan habilitados + if (!($settings['visibility']['is_enabled'] ?? false)) { + return ''; + } + if (!($settings['behavior']['rail_ads_enabled'] ?? false)) { + return ''; + } + + $publisherId = esc_attr($settings['content']['publisher_id'] ?? ''); + $slotId = $settings['content']['slot_skyscraper'] ?? ''; + + if (empty($publisherId) || empty($slotId)) { + return ''; + } + + $leftEnabled = ($settings['behavior']['rail_left_enabled'] ?? true) === true; + $rightEnabled = ($settings['behavior']['rail_right_enabled'] ?? true) === true; + $format = $settings['behavior']['rail_format'] ?? 'skyscraper'; + $topOffset = (int)($settings['behavior']['rail_top_offset'] ?? 150); + $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true; + + // Dimensiones segun formato + $width = $format === 'half-page' ? 300 : 160; + $height = 600; + + $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript'; + $dataAttr = $delayEnabled ? ' data-adsense-push' : ''; + + // === CSS via CSSGenerator (NO hardcodeado) === + $cssRules = []; + + // Estilos base para Rail Ads + $cssRules[] = $this->cssGenerator->generate('.roi-rail-ad', [ + 'position' => 'fixed', + 'top' => $topOffset . 'px', + 'width' => $width . 'px', + 'z-index' => '100', + ]); + + // Posicion rail izquierdo + $cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-left', [ + 'left' => 'calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px)', + ]); + + // Posicion rail derecho + $cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-right', [ + 'right' => 'calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px)', + ]); + + // Media query para ocultar en pantallas < 1600px + // NOTA: Media queries se escriben directamente (patron consistente con FeaturedImageRenderer) + $cssRules[] = "@media (max-width: 1599px) { + .roi-rail-ad { display: none !important; } + }"; + + $css = implode("\n", $cssRules); + $html = "\n"; + + /** + * EXCEPCION DOCUMENTADA: CSS inline requerido por Google AdSense + * Los atributos style="display:inline-block;width:Xpx;height:Xpx" son + * especificacion de Google y NO pueden generarse via CSSGenerator. + * Ref: https://support.google.com/adsense/answer/9274516 + */ + + // Rail izquierdo + if ($leftEnabled) { + $html .= sprintf( + '
+ + +
', + $width, $height, esc_attr($publisherId), esc_attr($slotId), $scriptType, $dataAttr + ); + } + + // Rail derecho + if ($rightEnabled) { + $html .= sprintf( + '
+ + +
', + $width, $height, esc_attr($publisherId), esc_attr($slotId), $scriptType, $dataAttr + ); + } + + return $html; + } +} diff --git a/Shared/Infrastructure/Di/DIContainer.php b/Shared/Infrastructure/Di/DIContainer.php index 8da6cac5..79f8d0af 100644 --- a/Shared/Infrastructure/Di/DIContainer.php +++ b/Shared/Infrastructure/Di/DIContainer.php @@ -19,6 +19,7 @@ use ROITheme\Shared\Infrastructure\Services\CleanupService; use ROITheme\Shared\Infrastructure\Services\CSSGeneratorService; use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase; use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase; +use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer; /** * DIContainer - Contenedor de Inyección de Dependencias @@ -233,4 +234,23 @@ final class DIContainer return $this->instances['saveComponentSettingsUseCase']; } + + /** + * Obtener renderer de AdSense Placement + * + * Lazy initialization: Crea la instancia solo en la primera llamada + * Resuelve dependencia: getCSSGeneratorService() + * + * @return AdsensePlacementRenderer + */ + public function getAdsensePlacementRenderer(): AdsensePlacementRenderer + { + if (!isset($this->instances['adsensePlacementRenderer'])) { + $this->instances['adsensePlacementRenderer'] = new AdsensePlacementRenderer( + $this->getCSSGeneratorService() + ); + } + + return $this->instances['adsensePlacementRenderer']; + } } diff --git a/functions.php b/functions.php index d81e9d2b..c482b8ac 100644 --- a/functions.php +++ b/functions.php @@ -49,6 +49,7 @@ require_once get_template_directory() . '/Inc/template-tags.php'; require_once get_template_directory() . '/Inc/featured-image.php'; require_once get_template_directory() . '/Inc/category-badge.php'; require_once get_template_directory() . '/Inc/adsense-delay.php'; +require_once get_template_directory() . '/Inc/adsense-placement.php'; require_once get_template_directory() . '/Inc/related-posts.php'; // ELIMINADO: Inc/toc.php (FASE 6 - Clean Architecture: usa TableOfContentsRenderer) require_once get_template_directory() . '/Inc/apu-tables.php';