diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
index a5846e36..bf4aae2e 100644
--- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
+++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
@@ -95,6 +95,16 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
+
+ // SEARCH RESULTS (ROI APU Search)
+ 'adsense-placementSearchAdsEnabled' => ['group' => 'search_results', 'attribute' => 'search_ads_enabled'],
+ 'adsense-placementSearchTopAdEnabled' => ['group' => 'search_results', 'attribute' => 'search_top_ad_enabled'],
+ 'adsense-placementSearchTopAdFormat' => ['group' => 'search_results', 'attribute' => 'search_top_ad_format'],
+ 'adsense-placementSearchBetweenEnabled' => ['group' => 'search_results', 'attribute' => 'search_between_enabled'],
+ 'adsense-placementSearchBetweenMax' => ['group' => 'search_results', 'attribute' => 'search_between_max'],
+ 'adsense-placementSearchBetweenFormat' => ['group' => 'search_results', 'attribute' => 'search_between_format'],
+ 'adsense-placementSearchBetweenPosition' => ['group' => 'search_results', 'attribute' => 'search_between_position'],
+ 'adsense-placementSearchBetweenEvery' => ['group' => 'search_results', 'attribute' => 'search_between_every'],
];
}
}
diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
index 5fb1bed4..b70f6da7 100644
--- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
+++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
@@ -57,6 +57,7 @@ final class AdsensePlacementFormBuilder
$html .= $this->buildRailAdsGroup($componentId);
$html .= $this->buildAnchorAdsGroup($componentId);
$html .= $this->buildVignetteAdsGroup($componentId);
+ $html .= $this->buildSearchResultsGroup($componentId);
$html .= ' ';
$html .= '';
@@ -708,6 +709,101 @@ final class AdsensePlacementFormBuilder
return $html;
}
+ /**
+ * Seccion para anuncios en resultados de busqueda (ROI APU Search)
+ */
+ private function buildSearchResultsGroup(string $cid): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Resultados de Busqueda';
+ $html .= ' ROI APU Search ';
+ $html .= ' ';
+ $html .= '
Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.
';
+
+ // Master switch
+ $searchAdsEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_ads_enabled', false);
+ $html .= $this->buildSwitch($cid . 'SearchAdsEnabled', 'Activar ads en busqueda', $searchAdsEnabled, 'bi-power');
+
+ // Anuncio superior
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ANUNCIO SUPERIOR ';
+ $html .= ' Debajo del campo de busqueda ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
+ $html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
+ $html .= '
';
+ $html .= '
';
+ $topFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_format', 'auto');
+ $html .= $this->buildSelect($cid . 'SearchTopAdFormat', 'Formato',
+ (string)$topFormat,
+ ['auto' => 'Auto (responsive)', 'display' => 'Display (fijo)', 'in-article' => 'In-Article (fluid)']
+ );
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+
+ // Anuncios entre resultados
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ENTRE RESULTADOS ';
+ $html .= ' Intercalados con los resultados ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
+ $html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
+ $html .= '
';
+ $html .= '
';
+ $betweenMax = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_max', '1');
+ $html .= $this->buildSelect($cid . 'SearchBetweenMax', 'Maximo ads',
+ (string)$betweenMax,
+ ['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios (max)']
+ );
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $betweenFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_format', 'in-article');
+ $html .= $this->buildSelect($cid . 'SearchBetweenFormat', 'Formato',
+ (string)$betweenFormat,
+ ['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)', 'autorelaxed' => 'Autorelaxed (feed)']
+ );
+ $html .= '
';
+ $html .= '
';
+ $betweenPosition = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_position', 'random');
+ $html .= $this->buildSelect($cid . 'SearchBetweenPosition', 'Posicion',
+ (string)$betweenPosition,
+ ['random' => 'Aleatorio', 'fixed' => 'Fijo (cada N)', 'first_half' => 'Primera mitad']
+ );
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $betweenEvery = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_every', '5');
+ $html .= $this->buildSelect($cid . 'SearchBetweenEvery', 'Cada N resultados (si es fijo)',
+ (string)$betweenEvery,
+ ['3' => 'Cada 3', '4' => 'Cada 4', '5' => 'Cada 5', '6' => 'Cada 6', '7' => 'Cada 7', '8' => 'Cada 8', '10' => 'Cada 10']
+ );
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
private function buildExclusionsGroup(string $cid): string
{
$html = '';
diff --git a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php
index 552d8c87..20329657 100644
--- a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php
+++ b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php
@@ -26,7 +26,13 @@ final class ContactFormFieldMapper implements FieldMapperInterface
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
diff --git a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
index 9ff18a35..57cc938c 100644
--- a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
+++ b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
@@ -93,17 +93,38 @@ final class ContactFormFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
- $html .= '
';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todos ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -598,4 +619,26 @@ final class ContactFormFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php b/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php
index 1a594c73..0a544154 100644
--- a/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php
+++ b/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php
@@ -30,7 +30,13 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'ctaVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'ctaVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'ctaVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'ctaVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
diff --git a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php
index 35f58809..08c7840c 100644
--- a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php
+++ b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php
@@ -94,18 +94,40 @@ final class CtaBoxSidebarFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- // show_on_pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= '
';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todos ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ // Obtener valores de _page_visibility (grupo especial)
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ // Grid 3 columnas según Design System
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -515,4 +537,29 @@ final class CtaBoxSidebarFormBuilder
return $html;
}
+
+ /**
+ * Genera un checkbox de visibilidad por tipo de pagina
+ *
+ * Sigue Design System: form-check-checkbox es obligatorio
+ */
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string
+ {
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php
index 0249355e..c5cc4804 100644
--- a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php
+++ b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php
@@ -26,7 +26,13 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'ctaLetsTalkVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'ctaLetsTalkVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
diff --git a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
index a1e76b00..fecea221 100644
--- a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
+++ b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
@@ -120,16 +120,38 @@ final class CtaLetsTalkFormBuilder
$html .= '
';
$html .= ' ';
- // Select: Show on Pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
- $html .= ' ';
- $html .= '
Mostrar en ';
- $html .= '
';
- $html .= ' Todas las páginas ';
- $html .= ' Solo página de inicio ';
- $html .= ' Solo posts individuales ';
- $html .= ' Solo páginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -447,4 +469,26 @@ final class CtaLetsTalkFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = ' ';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php
index 332e97b6..1c7f44ad 100644
--- a/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php
+++ b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php
@@ -26,7 +26,13 @@ final class CtaPostFieldMapper implements FieldMapperInterface
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'ctaPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'ctaPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
diff --git a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
index 5575894a..7406f633 100644
--- a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
+++ b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
@@ -85,17 +85,38 @@ final class CtaPostFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= ' ';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todos ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -437,4 +458,26 @@ final class CtaPostFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = ' ';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php
index a1d51e0b..b7a08d27 100644
--- a/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php
+++ b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php
@@ -26,7 +26,13 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
diff --git a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
index 05660e3e..6f47cb96 100644
--- a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
+++ b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
@@ -100,17 +100,38 @@ final class FeaturedImageFormBuilder
$html .= ' ';
$html .= ' ';
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= ' ';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todas las paginas ';
- $html .= ' Solo posts individuales ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -119,6 +140,28 @@ final class FeaturedImageFormBuilder
return $html;
}
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = ' ';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
private function buildContentGroup(string $componentId): string
{
$html = '';
diff --git a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
index 0ad8eebd..0fd229ed 100644
--- a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
+++ b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
@@ -26,9 +26,15 @@ final class HeroFieldMapper implements FieldMapperInterface
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
+ // Page Visibility (grupo especial _page_visibility)
+ 'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
+
// Content
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
index a9e13ec0..d6fe0337 100644
--- a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
+++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
@@ -102,18 +102,38 @@ final class HeroFormBuilder
$html .= '
';
$html .= ' ';
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= ' ';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todas las páginas ';
- $html .= ' Solo posts individuales ';
- $html .= ' Solo páginas ';
- $html .= ' Solo página de inicio ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
// Switch: CSS Crítico
@@ -427,4 +447,26 @@ final class HeroFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php
index 2e25f22b..af1f9244 100644
--- a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php
+++ b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php
@@ -26,10 +26,16 @@ final class NavbarFieldMapper implements FieldMapperInterface
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
- 'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
+ // Page Visibility (grupo especial _page_visibility)
+ 'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
+
// Layout
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
index 596ad0d7..f71f64fb 100644
--- a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
+++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
@@ -105,16 +105,38 @@ final class NavbarFormBuilder
$html .= '
';
$html .= ' ';
- // Select: Show on Pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
- $html .= ' ';
- $html .= '
Mostrar en ';
- $html .= '
';
- $html .= ' Todas las páginas ';
- $html .= ' Solo página de inicio ';
- $html .= ' Solo posts individuales ';
- $html .= ' Solo páginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
// Switch: Sticky
@@ -527,4 +549,26 @@ final class NavbarFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php
index b5237712..6bcae3ce 100644
--- a/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php
+++ b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php
@@ -29,7 +29,13 @@ final class RelatedPostFieldMapper implements FieldMapperInterface
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
diff --git a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
index cde1c5d9..4cf3db80 100644
--- a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
+++ b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
@@ -86,17 +86,38 @@ final class RelatedPostFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= '
';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todos ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -498,4 +519,26 @@ final class RelatedPostFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php
index c0dc6162..827e9d70 100644
--- a/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php
+++ b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php
@@ -26,7 +26,13 @@ final class SocialShareFieldMapper implements FieldMapperInterface
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
diff --git a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
index 2fc4403a..7cf4741e 100644
--- a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
+++ b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
@@ -94,18 +94,38 @@ final class SocialShareFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- // show_on_pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= '
';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todos ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -526,4 +546,26 @@ final class SocialShareFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php
index 72dc09a7..8860fc5b 100644
--- a/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php
+++ b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php
@@ -26,7 +26,13 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
- 'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
+
+ // Page Visibility (grupo especial _page_visibility)
+ 'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
// Content
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
diff --git a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php
index 6f0650f9..c0866d1e 100644
--- a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php
+++ b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php
@@ -94,18 +94,38 @@ final class TableOfContentsFormBuilder
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
- // show_on_pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
- $html .= '
';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todas las paginas ';
- $html .= ' Solo posts ';
- $html .= ' Solo paginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
$html .= '
';
@@ -585,4 +605,26 @@ final class TableOfContentsFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php
index 5dd5997b..041a5501 100644
--- a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php
+++ b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php
@@ -26,9 +26,15 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
- 'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
+ // Page Visibility (grupo especial _page_visibility)
+ 'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
+ 'topBarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
+ 'topBarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
+ 'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
+ 'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
+
// Content
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php
index cd130552..5fed2d32 100644
--- a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php
+++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php
@@ -105,19 +105,38 @@ final class TopNotificationBarFormBuilder
$html .= '
';
$html .= ' ';
- // Select: Show on Pages
- $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
- $html .= ' ';
- $html .= '
';
- $html .= ' ';
- $html .= ' Mostrar en';
- $html .= ' ';
- $html .= '
';
- $html .= ' Todas las páginas ';
- $html .= ' Solo página de inicio ';
- $html .= ' Solo posts individuales ';
- $html .= ' Solo páginas ';
- $html .= ' ';
+ // =============================================
+ // Checkboxes de visibilidad por tipo de página
+ // Grupo especial: _page_visibility
+ // =============================================
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mostrar en tipos de pagina';
+ $html .= '
';
+
+ $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
+ $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
+ $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
+ $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
+ $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
+ $html .= '
';
$html .= '
';
// Switch: CSS Crítico
@@ -319,4 +338,26 @@ final class TopNotificationBarFormBuilder
return $html;
}
+
+ private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/Assets/Js/adsense-loader.js b/Assets/Js/adsense-loader.js
index 373d2ee9..37747bf5 100644
--- a/Assets/Js/adsense-loader.js
+++ b/Assets/Js/adsense-loader.js
@@ -182,10 +182,72 @@
}, CONFIG.timeout);
}
+ /**
+ * Activa slots de AdSense insertados dinamicamente
+ * Escucha el evento 'roi-adsense-activate' disparado por otros scripts
+ */
+ function setupDynamicAdsListener() {
+ window.addEventListener('roi-adsense-activate', function() {
+ debugLog('Evento roi-adsense-activate recibido');
+
+ // Si AdSense aun no ha cargado, forzar carga ahora
+ if (!adsenseLoaded) {
+ debugLog('AdSense no cargado, forzando carga...');
+ loadAdSense();
+ return;
+ }
+
+ // AdSense ya cargado - activar nuevos slots
+ debugLog('Activando nuevos slots dinamicos...');
+ activateDynamicSlots();
+ });
+ }
+
+ /**
+ * Activa slots de AdSense que fueron insertados despues de la carga inicial
+ */
+ function activateDynamicSlots() {
+ // Buscar scripts de push que aun no han sido ejecutados
+ var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
+
+ if (pendingPushScripts.length === 0) {
+ debugLog('No hay slots pendientes por activar');
+ return;
+ }
+
+ debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
+
+ // Asegurar que adsbygoogle existe
+ window.adsbygoogle = window.adsbygoogle || [];
+
+ pendingPushScripts.forEach(function(oldScript) {
+ try {
+ // Crear nuevo script ejecutable
+ var newScript = document.createElement('script');
+ newScript.type = 'text/javascript';
+ newScript.innerHTML = oldScript.innerHTML;
+
+ // Reemplazar el placeholder con el script real
+ oldScript.parentNode.replaceChild(newScript, oldScript);
+ } catch (e) {
+ debugLog('Error activando slot: ' + e.message);
+ }
+ });
+ }
+
/**
* Inicializa el cargador retrasado de AdSense
*/
function init() {
+ // =========================================================================
+ // NUEVO: Siempre configurar listener para ads dinamicos
+ // IMPORTANTE: Esto debe ejecutarse ANTES del early return
+ // porque los ads dinamicos pueden necesitar activarse aunque
+ // el delay global este deshabilitado
+ // =========================================================================
+ setupDynamicAdsListener();
+ debugLog('Listener para ads dinamicos configurado');
+
// Verificar si el retardo de AdSense está habilitado
if (!window.roiAdsenseDelayed) {
debugLog('Retardo de AdSense no habilitado');
diff --git a/Inc/featured-image.php b/Inc/featured-image.php
index ff2e763a..1e8e040c 100644
--- a/Inc/featured-image.php
+++ b/Inc/featured-image.php
@@ -50,6 +50,13 @@ function roi_get_featured_image($post_id = null, $size = 'roi-featured-large', $
return ''; // No placeholder - retornar vacío
}
+ // Verificar que el archivo físico exista, no solo el attachment ID
+ $thumbnailId = get_post_thumbnail_id($post_id);
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return ''; // Archivo no existe en servidor
+ }
+
// Obtener tipo de post
$post_type = get_post_type($post_id);
@@ -145,6 +152,13 @@ function roi_get_post_thumbnail($post_id = null, $with_link = true) {
return ''; // No placeholder - retornar vacío
}
+ // Verificar que el archivo físico exista
+ $thumbnailId = get_post_thumbnail_id($post_id);
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return '';
+ }
+
// Obtener la imagen con clases Bootstrap
$image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array(
'class' => 'img-fluid post-thumbnail',
@@ -216,6 +230,13 @@ function roi_get_post_thumbnail_small($post_id = null, $with_link = true) {
return ''; // No placeholder - retornar vacío
}
+ // Verificar que el archivo físico exista
+ $thumbnailId = get_post_thumbnail_id($post_id);
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return '';
+ }
+
// Obtener la imagen
$image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array(
'class' => 'img-fluid post-thumbnail-small',
@@ -287,6 +308,13 @@ function roi_should_show_featured_image($post_id = null) {
return false;
}
+ // Verificar que el archivo físico exista
+ $thumbnailId = get_post_thumbnail_id($post_id);
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return false;
+ }
+
// Obtener tipo de post
$post_type = get_post_type($post_id);
@@ -338,6 +366,13 @@ function roi_get_featured_image_url($post_id = null, $size = 'roi-featured-large
return ''; // No placeholder - retornar vacío
}
+ // Verificar que el archivo físico exista
+ $thumbnailId = get_post_thumbnail_id($post_id);
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return '';
+ }
+
// Obtener URL de la imagen
$image_url = get_the_post_thumbnail_url($post_id, $size);
diff --git a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
index 86096dcf..a7f412d4 100644
--- a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
+++ b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* ContactFormRenderer - Renderiza formulario de contacto con webhook
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class ContactFormRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'contact-form';
+
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
@@ -34,7 +37,7 @@ final class ContactFormRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -67,7 +70,7 @@ final class ContactFormRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'contact-form';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -76,22 +79,6 @@ final class ContactFormRenderer implements RendererInterface
return $value === true || $value === '1' || $value === 1;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'all';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
diff --git a/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php b/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
index 1e4ee4f8..42b847a5 100644
--- a/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
+++ b/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
@@ -27,6 +28,12 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class CtaBoxSidebarRenderer implements RendererInterface
{
+ /**
+ * Nombre del componente para visibilidad
+ * Evita strings hardcodeados y facilita mantenimiento
+ */
+ private const COMPONENT_NAME = 'cta-box-sidebar';
+
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
@@ -39,7 +46,8 @@ final class CtaBoxSidebarRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ // Evaluar visibilidad por tipo de página (usa Helper, NO cambia constructor)
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -52,7 +60,7 @@ final class CtaBoxSidebarRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'cta-box-sidebar';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -60,22 +68,6 @@ final class CtaBoxSidebarRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
diff --git a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
index b15a3e52..d4b238fd 100644
--- a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
+++ b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* Class CtaLetsTalkRenderer
@@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class CtaLetsTalkRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'cta-lets-talk';
+
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
@@ -54,7 +57,7 @@ final class CtaLetsTalkRenderer implements RendererInterface
}
// Validar visibilidad por página
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -77,7 +80,7 @@ final class CtaLetsTalkRenderer implements RendererInterface
*/
public function supports(string $componentType): bool
{
- return $componentType === 'cta-lets-talk';
+ return $componentType === self::COMPONENT_NAME;
}
/**
@@ -91,25 +94,6 @@ final class CtaLetsTalkRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- /**
- * Verificar si debe mostrarse en la página actual
- *
- * @param array $data Datos del componente
- * @return bool
- */
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'all';
-
- return match ($showOn) {
- 'all' => true,
- 'home' => is_front_page(),
- 'posts' => is_single(),
- 'pages' => is_page(),
- default => true,
- };
- }
-
/**
* Calcular clases de visibilidad responsive
*
diff --git a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
index 2ee6ea19..9982b72a 100644
--- a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
+++ b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaPost\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class CtaPostRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'cta-post';
+
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
@@ -34,7 +37,7 @@ final class CtaPostRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -46,7 +49,7 @@ final class CtaPostRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'cta-post';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -55,22 +58,6 @@ final class CtaPostRenderer implements RendererInterface
return $value === true || $value === '1' || $value === 1;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
diff --git a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
index 430ccdbc..ec44ee79 100644
--- a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
+++ b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* FeaturedImageRenderer - Renderiza la imagen destacada del post
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class FeaturedImageRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'featured-image';
+
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
@@ -39,7 +42,7 @@ final class FeaturedImageRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -63,7 +66,7 @@ final class FeaturedImageRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'featured-image';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -71,25 +74,24 @@ final class FeaturedImageRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function hasPostThumbnail(): bool
{
- return is_singular() && has_post_thumbnail();
+ if (!is_singular() || !has_post_thumbnail()) {
+ return false;
+ }
+
+ // Verificar que el archivo físico exista, no solo el attachment ID
+ $thumbnailId = get_post_thumbnail_id();
+ if (!$thumbnailId) {
+ return false;
+ }
+
+ $filePath = get_attached_file($thumbnailId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return false;
+ }
+
+ return true;
}
/**
diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php
index 04389b3d..39b77341 100644
--- a/Public/Hero/Infrastructure/Ui/HeroRenderer.php
+++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* Class HeroRenderer
@@ -33,6 +34,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class HeroRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'hero';
+
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
@@ -48,7 +51,7 @@ final class HeroRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -68,7 +71,7 @@ final class HeroRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'hero';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -76,24 +79,6 @@ final class HeroRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'home':
- return is_front_page() || is_home();
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
/**
* Generar CSS usando CSSGeneratorService
*
diff --git a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
index 11cf590f..85cd8a87 100644
--- a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
+++ b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
use Walker_Nav_Menu;
/**
@@ -28,6 +29,8 @@ use Walker_Nav_Menu;
*/
final class NavbarRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'navbar';
+
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
@@ -43,6 +46,10 @@ final class NavbarRenderer implements RendererInterface
return '';
}
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
+ return '';
+ }
+
$html = $this->buildMenu($data);
// Si is_critical=true, CSS ya fue inyectado en por CriticalCSSService
@@ -281,7 +288,7 @@ final class NavbarRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'navbar';
+ return $componentType === self::COMPONENT_NAME;
}
}
diff --git a/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
index 2d2dcc1c..23436d6a 100644
--- a/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
+++ b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* RelatedPostRenderer - Renderiza seccion de posts relacionados
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class RelatedPostRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'related-post';
+
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
@@ -34,7 +37,7 @@ final class RelatedPostRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -51,7 +54,7 @@ final class RelatedPostRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'related-post';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -60,22 +63,6 @@ final class RelatedPostRenderer implements RendererInterface
return $value === true || $value === '1' || $value === 1;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
diff --git a/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
index c3a14276..e48e4fb8 100644
--- a/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
+++ b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\SocialShare\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class SocialShareRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'social-share';
+
private const NETWORKS = [
'facebook' => [
'field' => 'show_facebook',
@@ -84,7 +87,7 @@ final class SocialShareRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -96,7 +99,7 @@ final class SocialShareRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'social-share';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -105,22 +108,6 @@ final class SocialShareRenderer implements RendererInterface
return $value === true || $value === '1' || $value === 1;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
diff --git a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
index 8493ed26..d09e5361 100644
--- a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
+++ b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\TableOfContents\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
use DOMDocument;
use DOMXPath;
@@ -30,6 +31,8 @@ use DOMXPath;
*/
final class TableOfContentsRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'table-of-contents';
+
private array $headingCounter = [];
public function __construct(
@@ -44,7 +47,7 @@ final class TableOfContentsRenderer implements RendererInterface
return '';
}
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -63,7 +66,7 @@ final class TableOfContentsRenderer implements RendererInterface
public function supports(string $componentType): bool
{
- return $componentType === 'table-of-contents';
+ return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
@@ -71,22 +74,6 @@ final class TableOfContentsRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
-
- switch ($showOn) {
- case 'all':
- return true;
- case 'posts':
- return is_single();
- case 'pages':
- return is_page();
- default:
- return true;
- }
- }
-
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if (!$desktop && !$mobile) {
diff --git a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
index 1f08d3dd..fa25e9cc 100644
--- a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
+++ b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
@@ -6,6 +6,7 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
+use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* Class TopNotificationBarRenderer
@@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component;
*/
final class TopNotificationBarRenderer implements RendererInterface
{
+ private const COMPONENT_NAME = 'top-notification-bar';
+
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
@@ -54,7 +57,7 @@ final class TopNotificationBarRenderer implements RendererInterface
}
// Validar visibilidad por página
- if (!$this->shouldShowOnCurrentPage($data)) {
+ if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
@@ -78,7 +81,7 @@ final class TopNotificationBarRenderer implements RendererInterface
*/
public function supports(string $componentType): bool
{
- return $componentType === 'top-notification-bar';
+ return $componentType === self::COMPONENT_NAME;
}
/**
@@ -92,46 +95,6 @@ final class TopNotificationBarRenderer implements RendererInterface
return ($data['visibility']['is_enabled'] ?? false) === true;
}
- /**
- * Verificar si debe mostrarse en la página actual
- *
- * @param array $data Datos del componente
- * @return bool
- */
- private function shouldShowOnCurrentPage(array $data): bool
- {
- $showOn = $data['visibility']['show_on_pages'] ?? 'all';
-
- return match ($showOn) {
- 'all' => true,
- 'home' => is_front_page(),
- 'posts' => is_single(),
- 'pages' => is_page(),
- 'custom' => $this->isInCustomPages($data),
- default => true,
- };
- }
-
- /**
- * Verificar si está en páginas personalizadas
- *
- * @param array $data Datos del componente
- * @return bool
- */
- private function isInCustomPages(array $data): bool
- {
- $pageIds = $data['visibility']['custom_page_ids'] ?? '';
-
- if (empty($pageIds)) {
- return false;
- }
-
- $allowedIds = array_map('trim', explode(',', $pageIds));
- $currentId = (string) get_the_ID();
-
- return in_array($currentId, $allowedIds, true);
- }
-
/**
* Verificar si el componente fue dismissed por el usuario
*
diff --git a/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css b/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css
index 049f7160..6b72b421 100644
--- a/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css
+++ b/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css
@@ -110,3 +110,14 @@
transform: rotate(360deg);
}
}
+
+/* ========================================
+ FIX: Legacy wrapper with padding-top
+ Removes duplicate aspect-ratio from parent
+ containers that use the old padding-top trick
+ (prevents double spacing above videos)
+ ======================================== */
+
+div[style*="padding-top"]:has(> .youtube-facade) {
+ padding-top: 0 !important;
+}
diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json
index c22386a7..93716768 100644
--- a/Schemas/adsense-placement.json
+++ b/Schemas/adsense-placement.json
@@ -437,6 +437,72 @@
}
}
},
+ "search_results": {
+ "label": "Resultados de Busqueda (ROI APU Search)",
+ "priority": 73,
+ "fields": {
+ "search_ads_enabled": {
+ "type": "boolean",
+ "label": "Activar ads en busqueda",
+ "default": false,
+ "editable": true,
+ "description": "Insertar anuncios en resultados del buscador APU"
+ },
+ "search_top_ad_enabled": {
+ "type": "boolean",
+ "label": "Anuncio fijo arriba",
+ "default": true,
+ "editable": true,
+ "description": "Mostrar anuncio debajo del campo de busqueda"
+ },
+ "search_top_ad_format": {
+ "type": "select",
+ "label": "Formato anuncio superior",
+ "default": "auto",
+ "editable": true,
+ "options": ["auto", "display", "in-article"]
+ },
+ "search_between_enabled": {
+ "type": "boolean",
+ "label": "Anuncios entre resultados",
+ "default": true,
+ "editable": true
+ },
+ "search_between_max": {
+ "type": "select",
+ "label": "Maximo anuncios entre resultados",
+ "default": "1",
+ "editable": true,
+ "options": ["1", "2", "3"],
+ "description": "Maximo 3 por politicas AdSense"
+ },
+ "search_between_format": {
+ "type": "select",
+ "label": "Formato entre resultados",
+ "default": "in-article",
+ "editable": true,
+ "options": ["auto", "in-article", "autorelaxed"]
+ },
+ "search_between_position": {
+ "type": "select",
+ "label": "Posicion de anuncios",
+ "default": "random",
+ "editable": true,
+ "options": {
+ "random": "Aleatorio",
+ "fixed": "Fijo (cada N resultados)",
+ "first_half": "Primera mitad"
+ }
+ },
+ "search_between_every": {
+ "type": "select",
+ "label": "Cada N resultados (si es fijo)",
+ "default": "5",
+ "editable": true,
+ "options": ["3", "4", "5", "6", "7", "8", "10"]
+ }
+ }
+ },
"layout": {
"label": "Ubicaciones Archivos/Globales",
"priority": 80,
diff --git a/Schemas/contact-form.json b/Schemas/contact-form.json
index 3b801ed6..b03cf5d8 100644
--- a/Schemas/contact-form.json
+++ b/Schemas/contact-form.json
@@ -27,14 +27,6 @@
"default": true,
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "all",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
}
}
},
diff --git a/Schemas/cta-box-sidebar.json b/Schemas/cta-box-sidebar.json
index 645eb54f..957c5e2a 100644
--- a/Schemas/cta-box-sidebar.json
+++ b/Schemas/cta-box-sidebar.json
@@ -27,14 +27,6 @@
"default": false,
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
}
}
},
diff --git a/Schemas/cta-lets-talk.json b/Schemas/cta-lets-talk.json
index bdea8bd7..7f7aef0a 100644
--- a/Schemas/cta-lets-talk.json
+++ b/Schemas/cta-lets-talk.json
@@ -29,20 +29,6 @@
"editable": true,
"description": "Muestra el botón en pantallas móviles (<992px). Por defecto oculto para ahorrar espacio en navbar móvil"
},
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "all",
- "editable": true,
- "required": true,
- "options": {
- "all": "Todas las páginas",
- "home": "Solo página de inicio",
- "posts": "Solo posts individuales",
- "pages": "Solo páginas"
- },
- "description": "Define en qué páginas se mostrará el botón"
- },
"is_critical": {
"type": "boolean",
"label": "CSS Crítico",
diff --git a/Schemas/cta-post.json b/Schemas/cta-post.json
index 965a4e40..4d6d0034 100644
--- a/Schemas/cta-post.json
+++ b/Schemas/cta-post.json
@@ -27,14 +27,6 @@
"default": true,
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
}
}
},
diff --git a/Schemas/featured-image.json b/Schemas/featured-image.json
index 70ca7f60..84444980 100644
--- a/Schemas/featured-image.json
+++ b/Schemas/featured-image.json
@@ -30,19 +30,6 @@
"editable": true,
"required": true,
"description": "Muestra la imagen en dispositivos moviles (<768px)"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "required": true,
- "options": {
- "all": "Todas las paginas",
- "posts": "Solo posts individuales",
- "pages": "Solo paginas"
- },
- "description": "Define en que tipo de contenido se muestra la imagen"
}
}
},
diff --git a/Schemas/hero.json b/Schemas/hero.json
index 5a82caf9..b76e86af 100644
--- a/Schemas/hero.json
+++ b/Schemas/hero.json
@@ -31,20 +31,6 @@
"required": true,
"description": "Muestra el hero en dispositivos móviles (<768px)"
},
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "required": true,
- "options": {
- "all": "Todas las páginas",
- "posts": "Solo posts individuales",
- "pages": "Solo páginas",
- "home": "Solo página de inicio"
- },
- "description": "Define en qué tipo de contenido se mostrará el hero"
- },
"is_critical": {
"type": "boolean",
"label": "CSS Crítico",
diff --git a/Schemas/navbar.json b/Schemas/navbar.json
index d81a77bc..e61008f9 100644
--- a/Schemas/navbar.json
+++ b/Schemas/navbar.json
@@ -29,19 +29,6 @@
"editable": true,
"description": "Muestra el menú en dispositivos de escritorio (≥768px)"
},
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "all",
- "editable": true,
- "options": {
- "all": "Todas las páginas",
- "home": "Solo página de inicio",
- "posts": "Solo posts individuales",
- "pages": "Solo páginas"
- },
- "description": "Define en qué páginas se muestra el navbar"
- },
"sticky_enabled": {
"type": "boolean",
"label": "Navbar fijo (sticky)",
diff --git a/Schemas/related-post.json b/Schemas/related-post.json
index 218c6f3b..9c89e725 100644
--- a/Schemas/related-post.json
+++ b/Schemas/related-post.json
@@ -27,14 +27,6 @@
"default": true,
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
}
}
},
diff --git a/Schemas/social-share.json b/Schemas/social-share.json
index 456abe55..e7a13930 100644
--- a/Schemas/social-share.json
+++ b/Schemas/social-share.json
@@ -27,14 +27,6 @@
"default": true,
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
- },
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
}
}
},
diff --git a/Schemas/table-of-contents.json b/Schemas/table-of-contents.json
index f48efdc6..a5a9ea8f 100644
--- a/Schemas/table-of-contents.json
+++ b/Schemas/table-of-contents.json
@@ -28,14 +28,6 @@
"editable": true,
"description": "Muestra el componente en pantallas < 992px"
},
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "posts",
- "editable": true,
- "options": ["all", "posts", "pages"],
- "description": "Tipos de contenido donde se muestra"
- },
"is_critical": {
"type": "boolean",
"label": "CSS Crítico",
diff --git a/Schemas/top-notification-bar.json b/Schemas/top-notification-bar.json
index 87f0134f..049075e8 100644
--- a/Schemas/top-notification-bar.json
+++ b/Schemas/top-notification-bar.json
@@ -15,20 +15,6 @@
"required": true,
"description": "Activa o desactiva la barra de notificación superior"
},
- "show_on_pages": {
- "type": "select",
- "label": "Mostrar en",
- "default": "all",
- "editable": true,
- "required": true,
- "options": {
- "all": "Todas las páginas",
- "home": "Solo página de inicio",
- "posts": "Solo posts individuales",
- "pages": "Solo páginas"
- },
- "description": "Define en qué páginas se mostrará la barra"
- },
"show_on_desktop": {
"type": "boolean",
"label": "Mostrar en desktop",
diff --git a/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php
new file mode 100644
index 00000000..57fc8e94
--- /dev/null
+++ b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php
@@ -0,0 +1,46 @@
+visibilityRepository->getVisibilityConfig($componentName);
+
+ if (empty($config)) {
+ // Usar constante compartida (DRY)
+ $config = VisibilityDefaults::DEFAULT_VISIBILITY;
+ }
+
+ $pageType = $this->pageTypeDetector->detect();
+ $visibilityField = $pageType->toVisibilityField();
+
+ return $this->toBool($config[$visibilityField] ?? true);
+ }
+
+ private function toBool(mixed $value): bool
+ {
+ return $value === true || $value === '1' || $value === 1;
+ }
+}
diff --git a/Shared/Domain/Constants/VisibilityDefaults.php b/Shared/Domain/Constants/VisibilityDefaults.php
new file mode 100644
index 00000000..ca810d2b
--- /dev/null
+++ b/Shared/Domain/Constants/VisibilityDefaults.php
@@ -0,0 +1,45 @@
+ true,
+ 'show_on_posts' => true,
+ 'show_on_pages' => true,
+ 'show_on_archives' => false,
+ 'show_on_search' => false,
+ ];
+
+ /**
+ * Lista de campos de visibilidad válidos
+ */
+ public const VISIBILITY_FIELDS = [
+ 'show_on_home',
+ 'show_on_posts',
+ 'show_on_pages',
+ 'show_on_archives',
+ 'show_on_search',
+ ];
+}
diff --git a/Shared/Domain/Contracts/PageTypeDetectorInterface.php b/Shared/Domain/Contracts/PageTypeDetectorInterface.php
new file mode 100644
index 00000000..75086568
--- /dev/null
+++ b/Shared/Domain/Contracts/PageTypeDetectorInterface.php
@@ -0,0 +1,25 @@
+ Mapa de campo => habilitado
+ */
+ public function getVisibilityConfig(string $componentName): array;
+
+ /**
+ * Guarda la configuración de visibilidad de un componente
+ *
+ * @param string $componentName Nombre del componente
+ * @param array
$config Configuración a guardar
+ */
+ public function saveVisibilityConfig(string $componentName, array $config): void;
+
+ /**
+ * Verifica si existe configuración de visibilidad para un componente
+ */
+ public function hasVisibilityConfig(string $componentName): bool;
+
+ /**
+ * Obtiene lista de todos los componentes registrados
+ *
+ * @return array Lista de nombres de componentes
+ */
+ public function getAllComponentNames(): array;
+
+ /**
+ * Crea configuración de visibilidad por defecto para un componente
+ *
+ * @param string $componentName Nombre del componente
+ * @param array $defaults Valores por defecto
+ */
+ public function createDefaultVisibility(string $componentName, array $defaults): void;
+}
diff --git a/Shared/Domain/ValueObjects/ComponentConfiguration.php b/Shared/Domain/ValueObjects/ComponentConfiguration.php
index 58de9dc5..3801287b 100644
--- a/Shared/Domain/ValueObjects/ComponentConfiguration.php
+++ b/Shared/Domain/ValueObjects/ComponentConfiguration.php
@@ -93,6 +93,9 @@ final readonly class ComponentConfiguration
'widget_3', // Widget 3 del footer (menú)
'newsletter', // Sección newsletter del footer
'footer_bottom', // Pie del footer (copyright)
+
+ // Sistema de visibilidad por página
+ '_page_visibility', // Visibilidad por tipo de página (home, posts, pages, archives, search)
];
/**
diff --git a/Shared/Domain/ValueObjects/PageType.php b/Shared/Domain/ValueObjects/PageType.php
new file mode 100644
index 00000000..8e0e02e5
--- /dev/null
+++ b/Shared/Domain/ValueObjects/PageType.php
@@ -0,0 +1,90 @@
+value;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+
+ /**
+ * Retorna el nombre del campo de visibilidad correspondiente
+ */
+ public function toVisibilityField(): string
+ {
+ return match ($this->value) {
+ self::HOME => 'show_on_home',
+ self::POST => 'show_on_posts',
+ self::PAGE => 'show_on_pages',
+ self::ARCHIVE => 'show_on_archives',
+ self::SEARCH => 'show_on_search',
+ default => 'show_on_posts',
+ };
+ }
+}
diff --git a/Shared/Infrastructure/Api/WordPress/MigrationCommand.php b/Shared/Infrastructure/Api/WordPress/MigrationCommand.php
index 404b7f4c..9de77fe3 100644
--- a/Shared/Infrastructure/Api/WordPress/MigrationCommand.php
+++ b/Shared/Infrastructure/Api/WordPress/MigrationCommand.php
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Api\WordPress;
+use ROITheme\Shared\Infrastructure\Di\DIContainer;
+
/**
* WP-CLI Command para Sincronización de Schemas
*
@@ -297,6 +299,298 @@ final class MigrationCommand
'stats' => $stats
];
}
+
+ /**
+ * Migra configuración de visibilidad para todos los componentes
+ *
+ * ## EXAMPLES
+ *
+ * wp roi-theme migrate-visibility
+ *
+ * @when after_wp_load
+ */
+ public function migrate_visibility(): void
+ {
+ $container = DIContainer::getInstance();
+ $service = $container->getMigratePageVisibilityService();
+
+ $result = $service->migrate();
+
+ \WP_CLI::success(sprintf(
+ 'Migración completada: %d creados, %d omitidos',
+ $result['created'],
+ $result['skipped']
+ ));
+ }
+
+ /**
+ * Shortcodes que DEBEN ser preservados
+ */
+ private const PROTECTED_SHORTCODES = ['[roi_apu_search', '[roi_'];
+
+ /**
+ * Máximo porcentaje de contenido que puede eliminarse
+ */
+ private const MAX_CONTENT_LOSS_PERCENT = 50;
+
+ /**
+ * Limpia contenido Thrive congelado de páginas (H2 y paginación)
+ *
+ * LIMPIEZA QUIRÚRGICA CON VALIDACIONES DE SEGURIDAD:
+ * - Elimina H2 con data-shortcode="tcb_post_title"
+ * - Elimina paginación rota ([tcb_pagination_current_page], [tcb_pagination_total_pages])
+ * - PRESERVA todo el demás contenido incluyendo shortcodes [roi_apu_search]
+ * - Verifica que shortcodes importantes NO sean eliminados
+ * - Aborta si se detecta pérdida excesiva de contenido (>50%)
+ *
+ * ## OPTIONS
+ *
+ * [--dry-run]
+ * : Mostrar qué se limpiaría sin modificar nada (OBLIGATORIO primero)
+ *
+ * [--force]
+ * : Ejecutar la limpieza real después de verificar dry-run
+ *
+ * [--include-others]
+ * : Incluir otras páginas afectadas (Blog, Curso)
+ *
+ * ## EXAMPLES
+ *
+ * # Ver qué se limpiaría (modo seguro) - SIEMPRE PRIMERO
+ * wp roi-theme clean_thrive --dry-run
+ *
+ * # Ejecutar limpieza real (requiere --force)
+ * wp roi-theme clean_thrive --force
+ *
+ * @when after_wp_load
+ */
+ public function clean_thrive(array $args, array $assoc_args): void
+ {
+ $affectedPageIds = [
+ 107264, 107312, 107340, 107345, 107351, 107357, 107362,
+ 107369, 107374, 107379, 107384, 107389, 107395, 107399,
+ 107403, 107407, 107411, 107416, 107421, 107425, 185752
+ ];
+ $otherAffectedIds = [252030, 290709];
+
+ $dryRun = isset($assoc_args['dry-run']);
+ $includeOthers = isset($assoc_args['include-others']);
+ $force = isset($assoc_args['force']);
+
+ $pageIds = $affectedPageIds;
+ if ($includeOthers) {
+ $pageIds = array_merge($pageIds, $otherAffectedIds);
+ }
+
+ \WP_CLI::line('');
+ \WP_CLI::line('╔══════════════════════════════════════════════════════════════════╗');
+ \WP_CLI::line('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║');
+ \WP_CLI::line('║ Con validaciones de seguridad para proteger shortcodes ║');
+ \WP_CLI::line('╚══════════════════════════════════════════════════════════════════╝');
+ \WP_CLI::line('');
+
+ if ($dryRun) {
+ \WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido');
+ } else {
+ \WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false);
+ \WP_CLI::line('');
+ \WP_CLI::line('Para ejecutar la limpieza real, primero revisa el dry-run:');
+ \WP_CLI::line(' wp roi-theme clean_thrive --dry-run');
+ \WP_CLI::line('');
+ \WP_CLI::line('Si el dry-run es correcto y deseas ejecutar:');
+ \WP_CLI::line(' wp roi-theme clean_thrive --force');
+
+ if (!$force) {
+ return;
+ }
+ \WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido');
+ }
+
+ \WP_CLI::line('');
+ \WP_CLI::line('Páginas a procesar: ' . count($pageIds));
+ \WP_CLI::line('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES));
+ \WP_CLI::line('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%');
+ \WP_CLI::line('');
+
+ $totalH2Removed = 0;
+ $totalPaginationRemoved = 0;
+ $totalBytesFreed = 0;
+ $pagesModified = 0;
+ $pagesSkipped = 0;
+ $errors = [];
+
+ foreach ($pageIds as $id) {
+ $page = get_post($id);
+ if (!$page) {
+ \WP_CLI::warning("Página {$id} no encontrada, saltando...");
+ continue;
+ }
+
+ $originalContent = $page->post_content;
+ $originalSize = strlen($originalContent);
+
+ $hasThrive = strpos($originalContent, 'tcb_post_title') !== false ||
+ strpos($originalContent, 'tcb_pagination') !== false;
+
+ if (!$hasThrive) {
+ \WP_CLI::line(sprintf("[SIN THRIVE] ID %d: %s", $id, mb_substr($page->post_title, 0, 50)));
+ continue;
+ }
+
+ $h2Count = preg_match_all('/]*>\s*]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', $originalContent);
+ $protectedBefore = $this->countProtectedShortcodes($originalContent);
+ $cleanResult = $this->cleanThriveContentSafely($originalContent);
+
+ if ($cleanResult['error']) {
+ $errors[] = "ID {$id}: {$cleanResult['error']}";
+ \WP_CLI::error(sprintf("[ERROR] ID %d: %s - %s", $id, mb_substr($page->post_title, 0, 40), $cleanResult['error']), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ $cleanedContent = $cleanResult['content'];
+ $newSize = strlen($cleanedContent);
+ $protectedAfter = $this->countProtectedShortcodes($cleanedContent);
+
+ if ($protectedAfter < $protectedBefore) {
+ $errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})";
+ \WP_CLI::error(sprintf("[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)", $id, $protectedBefore, $protectedAfter), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ $lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0;
+ if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) {
+ $errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)";
+ \WP_CLI::error(sprintf("[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)", $id, $lossPercent, self::MAX_CONTENT_LOSS_PERCENT), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ $hasChanges = $originalContent !== $cleanedContent;
+ $bytesSaved = $originalSize - $newSize;
+ $paginationRemoved = (strpos($originalContent, 'tcb_pagination_current_page') !== false && strpos($cleanedContent, 'tcb_pagination_current_page') === false) ? 1 : 0;
+
+ if ($hasChanges) {
+ $pagesModified++;
+ $totalH2Removed += $h2Count;
+ $totalPaginationRemoved += $paginationRemoved;
+ $totalBytesFreed += $bytesSaved;
+
+ $status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]';
+ \WP_CLI::line(sprintf("%s ID %d: %s", $status, $id, mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : '')));
+ \WP_CLI::line(sprintf(" → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%", $h2Count, $paginationRemoved ? 'Sí' : 'No', $lossPercent));
+ \WP_CLI::line(sprintf(" → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s", $protectedAfter, $this->formatBytes($bytesSaved)));
+
+ if (!$dryRun && $force) {
+ wp_update_post(['ID' => $id, 'post_content' => $cleanedContent]);
+ }
+ } else {
+ \WP_CLI::line(sprintf("[SIN CAMBIOS] ID %d: %s", $id, mb_substr($page->post_title, 0, 50)));
+ }
+ }
+
+ \WP_CLI::line('');
+ \WP_CLI::line('════════════════════════════════════════════════════════════════════');
+ \WP_CLI::line('RESUMEN:');
+ \WP_CLI::line(sprintf(' Páginas modificadas: %d', $pagesModified));
+ \WP_CLI::line(sprintf(' Páginas omitidas: %d', $pagesSkipped));
+ \WP_CLI::line(sprintf(' Total H2 eliminados: %d', $totalH2Removed));
+ \WP_CLI::line(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved));
+ \WP_CLI::line(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed)));
+ \WP_CLI::line('════════════════════════════════════════════════════════════════════');
+
+ if (count($errors) > 0) {
+ \WP_CLI::line('');
+ \WP_CLI::warning('ERRORES ENCONTRADOS:');
+ foreach ($errors as $error) {
+ \WP_CLI::line(" - {$error}");
+ }
+ }
+
+ if ($dryRun && $pagesModified > 0 && count($errors) === 0) {
+ \WP_CLI::line('');
+ \WP_CLI::success('Dry-run completado SIN errores.');
+ \WP_CLI::line('');
+ \WP_CLI::warning('Para ejecutar la limpieza real:');
+ \WP_CLI::line(' wp roi-theme clean_thrive --force');
+ } elseif (!$dryRun && $force && $pagesModified > 0) {
+ \WP_CLI::line('');
+ \WP_CLI::success('Limpieza completada exitosamente.');
+ \WP_CLI::line('');
+ \WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.');
+ }
+ }
+
+ /**
+ * Limpia el contenido con validaciones de seguridad
+ * @return array{content: string, error: string|null}
+ */
+ private function cleanThriveContentSafely(string $content): array
+ {
+ $originalContent = $content;
+
+ // Patrón específico: H2 que contiene span con data-shortcode="tcb_post_title"
+ // Estructura: ...
+ $result = preg_replace('/]*>\s*]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', '', $content);
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2'];
+ }
+ $content = $result;
+
+ $result = preg_replace('/]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s', '', $content);
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación'];
+ }
+ $content = $result;
+
+ $result = preg_replace('/
]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s', '', $content);
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones'];
+ }
+ $content = $result;
+
+ $content = str_replace('[tcb_pagination_current_page]', '', $content);
+ $content = str_replace('[tcb_pagination_total_pages]', '', $content);
+
+ $result = preg_replace('/(\r?\n){3,}/', "\n\n", $content);
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas'];
+ }
+ $content = trim($result);
+
+ if (empty($content) && !empty($originalContent)) {
+ return ['content' => $originalContent, 'error' => 'El contenido quedó vacío'];
+ }
+
+ return ['content' => $content, 'error' => null];
+ }
+
+ /**
+ * Cuenta shortcodes protegidos en el contenido
+ */
+ private function countProtectedShortcodes(string $content): int
+ {
+ $count = 0;
+ foreach (self::PROTECTED_SHORTCODES as $shortcode) {
+ $count += substr_count($content, $shortcode);
+ }
+ return $count;
+ }
+
+ /**
+ * Formatea bytes a formato legible
+ */
+ private function formatBytes(int $bytes): string
+ {
+ if ($bytes < 1024) {
+ return $bytes . ' B';
+ } elseif ($bytes < 1048576) {
+ return round($bytes / 1024, 1) . ' KB';
+ } else {
+ return round($bytes / 1048576, 2) . ' MB';
+ }
+ }
}
// Registrar comando WP-CLI
diff --git a/Shared/Infrastructure/CLI/CleanThriveContentCommand.php b/Shared/Infrastructure/CLI/CleanThriveContentCommand.php
new file mode 100644
index 00000000..c068b8ba
--- /dev/null
+++ b/Shared/Infrastructure/CLI/CleanThriveContentCommand.php
@@ -0,0 +1,352 @@
+50%)
+ *
+ * USO:
+ * wp roi-theme clean_thrive --dry-run # Ver qué se limpiaría (OBLIGATORIO primero)
+ * wp roi-theme clean_thrive # Ejecutar limpieza real
+ *
+ * SEGURIDAD:
+ * - Verifica preservación de shortcodes [roi_apu_search]
+ * - Máximo 50% de reducción de contenido permitida
+ * - Valida cada preg_replace para evitar null returns
+ */
+final class CleanThriveContentCommand
+{
+ /**
+ * IDs de páginas buscar-apus afectadas
+ */
+ private const AFFECTED_PAGE_IDS = [
+ 107264, 107312, 107340, 107345, 107351, 107357, 107362,
+ 107369, 107374, 107379, 107384, 107389, 107395, 107399,
+ 107403, 107407, 107411, 107416, 107421, 107425, 185752
+ ];
+
+ /**
+ * Otras páginas con contenido Thrive (Blog, Curso)
+ */
+ private const OTHER_AFFECTED_IDS = [252030, 290709];
+
+ /**
+ * Shortcodes que DEBEN ser preservados
+ */
+ private const PROTECTED_SHORTCODES = [
+ '[roi_apu_search',
+ '[roi_',
+ ];
+
+ /**
+ * Máximo porcentaje de contenido que puede eliminarse
+ */
+ private const MAX_CONTENT_LOSS_PERCENT = 50;
+
+ public function __invoke(array $args, array $assoc_args): void
+ {
+ $dryRun = isset($assoc_args['dry-run']);
+ $includeOthers = isset($assoc_args['include-others']);
+ $force = isset($assoc_args['force']);
+
+ $pageIds = self::AFFECTED_PAGE_IDS;
+ if ($includeOthers) {
+ $pageIds = array_merge($pageIds, self::OTHER_AFFECTED_IDS);
+ }
+
+ WP_CLI::log('');
+ WP_CLI::log('╔══════════════════════════════════════════════════════════════════╗');
+ WP_CLI::log('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║');
+ WP_CLI::log('║ Con validaciones de seguridad para proteger shortcodes ║');
+ WP_CLI::log('╚══════════════════════════════════════════════════════════════════╝');
+ WP_CLI::log('');
+
+ if ($dryRun) {
+ WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido');
+ } else {
+ WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false);
+ WP_CLI::log('');
+ WP_CLI::log('Para ejecutar la limpieza real, primero revisa el dry-run:');
+ WP_CLI::log(' wp roi-theme clean_thrive --dry-run');
+ WP_CLI::log('');
+ WP_CLI::log('Si el dry-run es correcto y deseas ejecutar:');
+ WP_CLI::log(' wp roi-theme clean_thrive --force');
+
+ if (!$force) {
+ return;
+ }
+ WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido');
+ }
+
+ WP_CLI::log('');
+ WP_CLI::log('Páginas a procesar: ' . count($pageIds));
+ WP_CLI::log('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES));
+ WP_CLI::log('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%');
+ WP_CLI::log('');
+
+ $totalH2Removed = 0;
+ $totalPaginationRemoved = 0;
+ $totalBytesFreed = 0;
+ $pagesModified = 0;
+ $pagesSkipped = 0;
+ $errors = [];
+
+ foreach ($pageIds as $id) {
+ $page = get_post($id);
+ if (!$page) {
+ WP_CLI::warning("Página {$id} no encontrada, saltando...");
+ continue;
+ }
+
+ $originalContent = $page->post_content;
+ $originalSize = strlen($originalContent);
+
+ // Verificar si tiene contenido Thrive que limpiar
+ $hasThrive = strpos($originalContent, 'tcb_post_title') !== false ||
+ strpos($originalContent, 'tcb_pagination') !== false;
+
+ if (!$hasThrive) {
+ WP_CLI::log(sprintf(
+ "[SIN THRIVE] ID %d: %s",
+ $id,
+ mb_substr($page->post_title, 0, 50)
+ ));
+ continue;
+ }
+
+ // Contar elementos antes de limpiar
+ $h2Count = preg_match_all('/
]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s', $originalContent);
+
+ // Contar shortcodes protegidos antes
+ $protectedBefore = $this->countProtectedShortcodes($originalContent);
+
+ // Limpiar contenido con validación
+ $cleanResult = $this->cleanContentSafely($originalContent);
+
+ if ($cleanResult['error']) {
+ $errors[] = "ID {$id}: {$cleanResult['error']}";
+ WP_CLI::error(sprintf(
+ "[ERROR] ID %d: %s - %s",
+ $id,
+ mb_substr($page->post_title, 0, 40),
+ $cleanResult['error']
+ ), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ $cleanedContent = $cleanResult['content'];
+ $newSize = strlen($cleanedContent);
+
+ // Contar shortcodes protegidos después
+ $protectedAfter = $this->countProtectedShortcodes($cleanedContent);
+
+ // VALIDACIÓN CRÍTICA: Verificar shortcodes protegidos
+ if ($protectedAfter < $protectedBefore) {
+ $errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})";
+ WP_CLI::error(sprintf(
+ "[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)",
+ $id,
+ $protectedBefore,
+ $protectedAfter
+ ), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ // Verificar pérdida excesiva de contenido
+ $lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0;
+ if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) {
+ $errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)";
+ WP_CLI::error(sprintf(
+ "[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)",
+ $id,
+ $lossPercent,
+ self::MAX_CONTENT_LOSS_PERCENT
+ ), false);
+ $pagesSkipped++;
+ continue;
+ }
+
+ // Verificar si hubo cambios
+ $hasChanges = $originalContent !== $cleanedContent;
+ $bytesSaved = $originalSize - $newSize;
+
+ // Contar paginación removida
+ $paginationRemoved = (
+ strpos($originalContent, 'tcb_pagination_current_page') !== false &&
+ strpos($cleanedContent, 'tcb_pagination_current_page') === false
+ ) ? 1 : 0;
+
+ if ($hasChanges) {
+ $pagesModified++;
+ $totalH2Removed += $h2Count;
+ $totalPaginationRemoved += $paginationRemoved;
+ $totalBytesFreed += $bytesSaved;
+
+ $status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]';
+ WP_CLI::log(sprintf(
+ "%s ID %d: %s",
+ $status,
+ $id,
+ mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : '')
+ ));
+ WP_CLI::log(sprintf(
+ " → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%",
+ $h2Count,
+ $paginationRemoved ? 'Sí' : 'No',
+ $lossPercent
+ ));
+ WP_CLI::log(sprintf(
+ " → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s",
+ $protectedAfter,
+ $this->formatBytes($bytesSaved)
+ ));
+
+ if (!$dryRun && $force) {
+ wp_update_post([
+ 'ID' => $id,
+ 'post_content' => $cleanedContent
+ ]);
+ }
+ } else {
+ WP_CLI::log(sprintf(
+ "[SIN CAMBIOS] ID %d: %s",
+ $id,
+ mb_substr($page->post_title, 0, 50)
+ ));
+ }
+ }
+
+ WP_CLI::log('');
+ WP_CLI::log('════════════════════════════════════════════════════════════════════');
+ WP_CLI::log('RESUMEN:');
+ WP_CLI::log(sprintf(' Páginas modificadas: %d', $pagesModified));
+ WP_CLI::log(sprintf(' Páginas omitidas: %d', $pagesSkipped));
+ WP_CLI::log(sprintf(' Total H2 eliminados: %d', $totalH2Removed));
+ WP_CLI::log(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved));
+ WP_CLI::log(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed)));
+ WP_CLI::log('════════════════════════════════════════════════════════════════════');
+
+ if (count($errors) > 0) {
+ WP_CLI::log('');
+ WP_CLI::warning('ERRORES ENCONTRADOS:');
+ foreach ($errors as $error) {
+ WP_CLI::log(" - {$error}");
+ }
+ }
+
+ if ($dryRun && $pagesModified > 0 && count($errors) === 0) {
+ WP_CLI::log('');
+ WP_CLI::success('Dry-run completado SIN errores.');
+ WP_CLI::log('');
+ WP_CLI::warning('Para ejecutar la limpieza real:');
+ WP_CLI::log(' wp roi-theme clean_thrive --force');
+ } elseif (!$dryRun && $force && $pagesModified > 0) {
+ WP_CLI::log('');
+ WP_CLI::success('Limpieza completada exitosamente.');
+ WP_CLI::log('');
+ WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.');
+ }
+ }
+
+ /**
+ * Limpia el contenido con validaciones de seguridad
+ *
+ * @return array{content: string, error: string|null}
+ */
+ private function cleanContentSafely(string $content): array
+ {
+ $originalContent = $content;
+
+ // 1. Eliminar H2 con data-shortcode="tcb_post_title"
+ $result = preg_replace(
+ '/]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s',
+ '',
+ $content
+ );
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2'];
+ }
+ $content = $result;
+
+ // 2. Eliminar paginación Thrive rota
+ $result = preg_replace(
+ '/ ]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s',
+ '',
+ $content
+ );
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación'];
+ }
+ $content = $result;
+
+ // 3. Eliminar botones de paginación Thrive
+ $result = preg_replace(
+ '/
]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s',
+ '',
+ $content
+ );
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones'];
+ }
+ $content = $result;
+
+ // 4. Eliminar shortcodes Thrive huérfanos
+ $content = str_replace('[tcb_pagination_current_page]', '', $content);
+ $content = str_replace('[tcb_pagination_total_pages]', '', $content);
+
+ // 5. Limpiar múltiples líneas vacías (con validación)
+ $result = preg_replace('/(\r?\n){3,}/', "\n\n", $content);
+ if ($result === null) {
+ return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas'];
+ }
+ $content = $result;
+
+ // 6. Trim
+ $content = trim($content);
+
+ // Validación final: no retornar vacío si original tenía contenido
+ if (empty($content) && !empty($originalContent)) {
+ return ['content' => $originalContent, 'error' => 'El contenido quedó vacío'];
+ }
+
+ return ['content' => $content, 'error' => null];
+ }
+
+ /**
+ * Cuenta shortcodes protegidos en el contenido
+ */
+ private function countProtectedShortcodes(string $content): int
+ {
+ $count = 0;
+ foreach (self::PROTECTED_SHORTCODES as $shortcode) {
+ $count += substr_count($content, $shortcode);
+ }
+ return $count;
+ }
+
+ /**
+ * Formatea bytes a formato legible
+ */
+ private function formatBytes(int $bytes): string
+ {
+ if ($bytes < 1024) {
+ return $bytes . ' B';
+ } elseif ($bytes < 1048576) {
+ return round($bytes / 1024, 1) . ' KB';
+ } else {
+ return round($bytes / 1048576, 2) . ' MB';
+ }
+ }
+}
diff --git a/Shared/Infrastructure/Di/DIContainer.php b/Shared/Infrastructure/Di/DIContainer.php
index 8cf122f5..77054ccb 100644
--- a/Shared/Infrastructure/Di/DIContainer.php
+++ b/Shared/Infrastructure/Di/DIContainer.php
@@ -22,6 +22,12 @@ use ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector;
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
+use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
+use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
+use ROITheme\Shared\Infrastructure\Services\WordPressPageTypeDetector;
+use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressPageVisibilityRepository;
+use ROITheme\Shared\Application\UseCases\EvaluatePageVisibility\EvaluatePageVisibilityUseCase;
+use ROITheme\Shared\Infrastructure\Services\MigratePageVisibilityService;
/**
* DIContainer - Contenedor de Inyección de Dependencias
@@ -46,10 +52,38 @@ final class DIContainer
{
private array $instances = [];
+ /**
+ * Instancia singleton del contenedor
+ * @var self|null
+ */
+ private static ?self $instance = null;
+
+ /**
+ * Obtiene la instancia singleton del contenedor
+ *
+ * NOTA: Se debe haber creado una instancia previamente en functions.php
+ * El constructor registra automáticamente la instancia.
+ *
+ * @return self
+ * @throws \RuntimeException Si no se ha inicializado el contenedor
+ */
+ public static function getInstance(): self
+ {
+ if (self::$instance === null) {
+ throw new \RuntimeException(
+ 'DIContainer no ha sido inicializado. Asegúrate de que functions.php se haya ejecutado primero.'
+ );
+ }
+ return self::$instance;
+ }
+
public function __construct(
private \wpdb $wpdb,
private string $schemasPath
- ) {}
+ ) {
+ // Registrar como instancia singleton
+ self::$instance = $this;
+ }
/**
* Obtener repositorio de componentes
@@ -272,4 +306,61 @@ final class DIContainer
return $this->instances['criticalCSSCollector'];
}
+
+ // ===============================
+ // Page Visibility System
+ // ===============================
+
+ /**
+ * Obtiene el repositorio de visibilidad de página
+ *
+ * IMPORTANTE: Inyecta $wpdb para consistencia con el resto del código
+ * (WordPressComponentSettingsRepository también recibe $wpdb por constructor)
+ */
+ public function getPageVisibilityRepository(): PageVisibilityRepositoryInterface
+ {
+ if (!isset($this->instances['pageVisibilityRepository'])) {
+ // Inyectar $wpdb siguiendo el patrón existente
+ $this->instances['pageVisibilityRepository'] = new WordPressPageVisibilityRepository($this->wpdb);
+ }
+ return $this->instances['pageVisibilityRepository'];
+ }
+
+ /**
+ * Obtiene el detector de tipo de página
+ */
+ public function getPageTypeDetector(): PageTypeDetectorInterface
+ {
+ if (!isset($this->instances['pageTypeDetector'])) {
+ $this->instances['pageTypeDetector'] = new WordPressPageTypeDetector();
+ }
+ return $this->instances['pageTypeDetector'];
+ }
+
+ /**
+ * Obtiene el caso de uso de evaluación de visibilidad
+ */
+ public function getEvaluatePageVisibilityUseCase(): EvaluatePageVisibilityUseCase
+ {
+ if (!isset($this->instances['evaluatePageVisibilityUseCase'])) {
+ $this->instances['evaluatePageVisibilityUseCase'] = new EvaluatePageVisibilityUseCase(
+ $this->getPageTypeDetector(),
+ $this->getPageVisibilityRepository()
+ );
+ }
+ return $this->instances['evaluatePageVisibilityUseCase'];
+ }
+
+ /**
+ * Obtiene el servicio de migración de visibilidad
+ */
+ public function getMigratePageVisibilityService(): MigratePageVisibilityService
+ {
+ if (!isset($this->instances['migratePageVisibilityService'])) {
+ $this->instances['migratePageVisibilityService'] = new MigratePageVisibilityService(
+ $this->getPageVisibilityRepository()
+ );
+ }
+ return $this->instances['migratePageVisibilityService'];
+ }
}
diff --git a/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php b/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php
new file mode 100644
index 00000000..420ab1d0
--- /dev/null
+++ b/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php
@@ -0,0 +1,146 @@
+wpdb->prefix . self::TABLE_SUFFIX;
+
+ $results = $this->wpdb->get_results(
+ $this->wpdb->prepare(
+ "SELECT attribute_name, attribute_value
+ FROM {$table}
+ WHERE component_name = %s
+ AND group_name = %s",
+ $componentName,
+ self::GROUP_NAME
+ ),
+ ARRAY_A
+ );
+
+ if (empty($results)) {
+ return [];
+ }
+
+ $config = [];
+ foreach ($results as $row) {
+ $config[$row['attribute_name']] = $row['attribute_value'] === '1';
+ }
+
+ return $config;
+ }
+
+ public function saveVisibilityConfig(string $componentName, array $config): void
+ {
+ $table = $this->wpdb->prefix . self::TABLE_SUFFIX;
+
+ foreach ($config as $field => $enabled) {
+ if (!in_array($field, self::VISIBILITY_FIELDS, true)) {
+ continue;
+ }
+
+ $exists = $this->wpdb->get_var($this->wpdb->prepare(
+ "SELECT COUNT(*) FROM {$table}
+ WHERE component_name = %s
+ AND group_name = %s
+ AND attribute_name = %s",
+ $componentName,
+ self::GROUP_NAME,
+ $field
+ ));
+
+ $value = $enabled ? '1' : '0';
+
+ if ($exists) {
+ $this->wpdb->update(
+ $table,
+ [
+ 'attribute_value' => $value,
+ 'updated_at' => current_time('mysql'),
+ ],
+ [
+ 'component_name' => $componentName,
+ 'group_name' => self::GROUP_NAME,
+ 'attribute_name' => $field,
+ ]
+ );
+ } else {
+ $this->wpdb->insert($table, [
+ 'component_name' => $componentName,
+ 'group_name' => self::GROUP_NAME,
+ 'attribute_name' => $field,
+ 'attribute_value' => $value,
+ 'is_editable' => 1,
+ 'created_at' => current_time('mysql'),
+ 'updated_at' => current_time('mysql'),
+ ]);
+ }
+ }
+ }
+
+ public function hasVisibilityConfig(string $componentName): bool
+ {
+ $table = $this->wpdb->prefix . self::TABLE_SUFFIX;
+
+ $count = $this->wpdb->get_var($this->wpdb->prepare(
+ "SELECT COUNT(*) FROM {$table}
+ WHERE component_name = %s
+ AND group_name = %s",
+ $componentName,
+ self::GROUP_NAME
+ ));
+
+ return (int) $count > 0;
+ }
+
+ public function getAllComponentNames(): array
+ {
+ $table = $this->wpdb->prefix . self::TABLE_SUFFIX;
+
+ $results = $this->wpdb->get_col(
+ "SELECT DISTINCT component_name FROM {$table} ORDER BY component_name"
+ );
+
+ return $results ?: [];
+ }
+
+ public function createDefaultVisibility(string $componentName, array $defaults): void
+ {
+ if ($this->hasVisibilityConfig($componentName)) {
+ return;
+ }
+
+ $this->saveVisibilityConfig($componentName, $defaults);
+ }
+}
diff --git a/Shared/Infrastructure/Services/MigratePageVisibilityService.php b/Shared/Infrastructure/Services/MigratePageVisibilityService.php
new file mode 100644
index 00000000..c86db621
--- /dev/null
+++ b/Shared/Infrastructure/Services/MigratePageVisibilityService.php
@@ -0,0 +1,53 @@
+visibilityRepository->getAllComponentNames();
+
+ foreach ($components as $componentName) {
+ if ($this->visibilityRepository->hasVisibilityConfig($componentName)) {
+ $skipped++;
+ continue;
+ }
+
+ // Usar constante compartida (DRY)
+ $this->visibilityRepository->createDefaultVisibility(
+ $componentName,
+ VisibilityDefaults::DEFAULT_VISIBILITY
+ );
+ $created++;
+ }
+
+ return [
+ 'created' => $created,
+ 'skipped' => $skipped,
+ ];
+ }
+}
diff --git a/Shared/Infrastructure/Services/PageVisibilityHelper.php b/Shared/Infrastructure/Services/PageVisibilityHelper.php
new file mode 100644
index 00000000..ba23de84
--- /dev/null
+++ b/Shared/Infrastructure/Services/PageVisibilityHelper.php
@@ -0,0 +1,39 @@
+getEvaluatePageVisibilityUseCase();
+
+ return $useCase->execute($componentName);
+ }
+}
diff --git a/Shared/Infrastructure/Services/WordPressPageTypeDetector.php b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php
new file mode 100644
index 00000000..4d7eeb09
--- /dev/null
+++ b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php
@@ -0,0 +1,65 @@
+isHome()) {
+ return PageType::home();
+ }
+
+ if ($this->isPost()) {
+ return PageType::post();
+ }
+
+ if ($this->isPage()) {
+ return PageType::page();
+ }
+
+ if ($this->isSearch()) {
+ return PageType::search();
+ }
+
+ if ($this->isArchive()) {
+ return PageType::archive();
+ }
+
+ return PageType::fromString(PageType::UNKNOWN);
+ }
+
+ public function isHome(): bool
+ {
+ return is_front_page();
+ }
+
+ public function isPost(): bool
+ {
+ return is_single() && !is_front_page();
+ }
+
+ public function isPage(): bool
+ {
+ return is_page() && !is_front_page();
+ }
+
+ public function isArchive(): bool
+ {
+ return is_archive();
+ }
+
+ public function isSearch(): bool
+ {
+ return is_search();
+ }
+}
diff --git a/front-page.php b/front-page.php
index 7cb6b59c..e0c96ac6 100644
--- a/front-page.php
+++ b/front-page.php
@@ -2,7 +2,8 @@
/**
* The template for displaying the static front page
*
- * Structure replicates template index.html lines 322-345 (Hero Section)
+ * Replica la estructura de single.php para consistencia visual.
+ * Grid layout: col-lg-9 (contenido) + col-lg-3 (sidebar)
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#front-page
*
@@ -13,118 +14,105 @@
get_header();
?>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0 ) :
- ?>
-
-
- = 3 ) break;
- if ( $category->slug === 'uncategorized' ) continue;
- ?>
-
-
- name ); ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
>
-
-
-
- '
' . esc_html__( 'Pages:', 'roi-theme' ),
- 'after' => '
',
- )
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
+
+ >
+
+ wp_link_pages(array(
+ 'before' => '' . esc_html__('Pages:', 'roi-theme'),
+ 'after' => '
',
+ ));
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
prefix . 'roi_theme_component_settings';
+ $rows = $wpdb->get_results($wpdb->prepare(
+ "SELECT group_name, attribute_name, attribute_value
+ FROM {$table}
+ WHERE component_name = %s",
+ 'adsense-placement'
+ ));
+
+ if (empty($rows)) {
+ return ['enabled' => false];
+ }
+
+ // Organizar en array asociativo por grupo/atributo
+ $settings = [];
+ foreach ($rows as $row) {
+ if (!isset($settings[$row->group_name])) {
+ $settings[$row->group_name] = [];
+ }
+ // Decodificar valor
+ $value = $row->attribute_value;
+ if ($value === '1') $value = true;
+ elseif ($value === '0') $value = false;
+ else {
+ $decoded = json_decode($value, true);
+ if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
+ $value = $decoded;
+ }
+ }
+ $settings[$row->group_name][$row->attribute_name] = $value;
+ }
+
+ // Helper para obtener valor con default
+ $get = function(string $group, string $attr, $default = null) use ($settings) {
+ return $settings[$group][$attr] ?? $default;
+ };
+
+ // =========================================================================
+ // VALIDAR CONDICIONES GLOBALES
+ // =========================================================================
+
+ // AdSense global deshabilitado
+ if ($get('visibility', 'is_enabled', false) !== true) {
+ return ['enabled' => false];
+ }
+
+ // Ads en busqueda deshabilitados
+ if ($get('search_results', 'search_ads_enabled', false) !== true) {
+ return ['enabled' => false];
+ }
+
+ // Publisher ID vacio
+ $publisherId = $get('content', 'publisher_id', '');
+ if (empty($publisherId)) {
+ return ['enabled' => false];
+ }
+
+ // =========================================================================
+ // VALIDAR EXCLUSIONES (igual que el resto del sistema)
+ // =========================================================================
+
+ // Ocultar para usuarios logueados
+ if ($get('visibility', 'hide_for_logged_in', false) === true && is_user_logged_in()) {
+ return ['enabled' => false];
+ }
+
+ // Visibilidad por dispositivo
+ $isMobile = wp_is_mobile();
+ if ($isMobile && $get('visibility', 'show_on_mobile', true) !== true) {
+ return ['enabled' => false];
+ }
+ if (!$isMobile && $get('visibility', 'show_on_desktop', true) !== true) {
+ return ['enabled' => false];
+ }
+
+ // =========================================================================
+ // CONSTRUIR CONFIGURACION
+ // =========================================================================
+ return [
+ 'enabled' => true,
+ 'publisherId' => $publisherId,
+ 'slots' => [
+ 'auto' => $get('content', 'slot_auto', ''),
+ 'inArticle' => $get('content', 'slot_inarticle', ''),
+ 'autorelaxed' => $get('content', 'slot_autorelaxed', ''),
+ 'display' => $get('content', 'slot_display', ''),
+ ],
+ 'topAd' => [
+ 'enabled' => $get('search_results', 'search_top_ad_enabled', true) === true,
+ 'format' => $get('search_results', 'search_top_ad_format', 'auto'),
+ ],
+ 'betweenAds' => [
+ 'enabled' => $get('search_results', 'search_between_enabled', true) === true,
+ 'max' => min(3, max(1, (int) $get('search_results', 'search_between_max', '1'))),
+ 'format' => $get('search_results', 'search_between_format', 'in-article'),
+ 'position' => $get('search_results', 'search_between_position', 'random'),
+ 'every' => (int) $get('search_results', 'search_between_every', '5'),
+ ],
+ 'delay' => [
+ 'enabled' => $get('forms', 'delay_enabled', true) === true,
+ 'timeout' => (int) $get('forms', 'delay_timeout', '5000'),
+ ],
+ ];
+}
diff --git a/minify-css.php b/minify-css.php
new file mode 100644
index 00000000..18996c02
--- /dev/null
+++ b/minify-css.php
@@ -0,0 +1,60 @@
++~])\s*/', '$1', $css);
+
+ // Remove last semicolon before closing brace
+ $css = str_replace(';}', '}', $css);
+
+ // Trim
+ $css = trim($css);
+
+ return $css;
+}
+
+$files = [
+ 'Assets/Css/css-global-accessibility.css' => 'Assets/Css/css-global-accessibility.min.css',
+ 'Assets/Css/style.css' => 'Assets/Css/style.min.css',
+];
+
+$base_path = __DIR__ . '/';
+
+foreach ($files as $source => $dest) {
+ $source_path = $base_path . $source;
+ $dest_path = $base_path . $dest;
+
+ if (file_exists($source_path)) {
+ $css = file_get_contents($source_path);
+ $minified = minify_css($css);
+
+ file_put_contents($dest_path, $minified);
+
+ $original_size = strlen($css);
+ $minified_size = strlen($minified);
+ $savings = $original_size - $minified_size;
+ $percent = round(($savings / $original_size) * 100, 1);
+
+ echo "Minified: $source\n";
+ echo " Original: " . number_format($original_size) . " bytes\n";
+ echo " Minified: " . number_format($minified_size) . " bytes\n";
+ echo " Savings: " . number_format($savings) . " bytes ($percent%)\n\n";
+ } else {
+ echo "File not found: $source\n";
+ }
+}
+
+echo "Done!\n";