feat(visibility): sistema de visibilidad por tipo de página

- Añadir PageVisibility use case y repositorio
- Implementar PageTypeDetector para detectar home/single/page/archive
- Actualizar FieldMappers con soporte show_on_[page_type]
- Extender FormBuilders con UI de visibilidad por página
- Refactorizar Renderers para evaluar visibilidad dinámica
- Limpiar schemas removiendo campos de visibilidad legacy
- Añadir MigrationCommand para migrar configuraciones existentes
- Implementar adsense-loader.js para carga lazy de ads
- Actualizar front-page.php con nueva estructura
- Extender DIContainer con nuevos servicios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-12-03 09:16:34 -06:00
parent 7fb5eda108
commit 8735962f52
66 changed files with 2614 additions and 573 deletions

View File

@@ -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'],
];
}
}

View File

@@ -57,6 +57,7 @@ final class AdsensePlacementFormBuilder
$html .= $this->buildRailAdsGroup($componentId);
$html .= $this->buildAnchorAdsGroup($componentId);
$html .= $this->buildVignetteAdsGroup($componentId);
$html .= $this->buildSearchResultsGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
@@ -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 = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #fd7e14;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-search me-2" style="color: #fd7e14;"></i>';
$html .= ' Resultados de Busqueda';
$html .= ' <span class="badge bg-secondary ms-2">ROI APU Search</span>';
$html .= ' </h5>';
$html .= ' <p class="small text-muted mb-3">Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.</p>';
// 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 .= '<div class="border rounded p-3 mb-3" style="background: #fff8f0;">';
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
$html .= ' <span class="badge" style="background: #fd7e14;">ANUNCIO SUPERIOR</span>';
$html .= ' <small class="text-muted">Debajo del campo de busqueda</small>';
$html .= '</div>';
$html .= '<div class="row g-2">';
$html .= ' <div class="col-md-6">';
$topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
$html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
$html .= ' </div>';
$html .= ' <div class="col-md-6">';
$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 .= ' </div>';
$html .= '</div>';
$html .= '</div>';
// Anuncios entre resultados
$html .= '<div class="border rounded p-3" style="background: #fff8f0;">';
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
$html .= ' <span class="badge" style="background: #fd7e14;">ENTRE RESULTADOS</span>';
$html .= ' <small class="text-muted">Intercalados con los resultados</small>';
$html .= '</div>';
$html .= '<div class="row g-2 mb-2">';
$html .= ' <div class="col-md-6">';
$betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
$html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
$html .= ' </div>';
$html .= ' <div class="col-md-6">';
$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 .= ' </div>';
$html .= '</div>';
$html .= '<div class="row g-2 mb-2">';
$html .= ' <div class="col-md-6">';
$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 .= ' </div>';
$html .= ' <div class="col-md-6">';
$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 .= ' </div>';
$html .= '</div>';
$html .= '<div class="row g-2">';
$html .= ' <div class="col-md-6">';
$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 .= ' </div>';
$html .= '</div>';
$html .= '</div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildExclusionsGroup(string $cid): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
// 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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -120,16 +120,38 @@ final class CtaLetsTalkFormBuilder
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -100,17 +100,38 @@ final class FeaturedImageFormBuilder
$html .= ' </div>';
$html .= ' </div>';
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';

View File

@@ -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'],

View File

@@ -102,18 +102,38 @@ final class HeroFormBuilder
$html .= ' </div>';
$html .= ' </div>';
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-2 mt-3">';
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
// 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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -105,16 +105,38 @@ final class NavbarFormBuilder
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
// 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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -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 .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
@@ -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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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'],

View File

@@ -105,19 +105,38 @@ final class TopNotificationBarFormBuilder
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-2 mt-3">';
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
// =============================================
// Checkboxes de visibilidad por tipo de página
// Grupo especial: _page_visibility
// =============================================
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">';
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en tipos de pagina';
$html .= ' </p>';
$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 .= ' <div class="row g-2">';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
$html .= ' </div>';
$html .= ' <div class="col-md-4">';
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
$html .= ' </div>';
$html .= ' </div>';
// 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 = ' <div class="form-check form-check-checkbox mb-2">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -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');

View File

@@ -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);

View File

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

View File

@@ -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'] ?? [];

View File

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

View File

@@ -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'] ?? [];

View File

@@ -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;
}
/**

View File

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

View File

@@ -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 <head> 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;
}
}

View File

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

View File

@@ -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'] ?? [];

View File

@@ -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) {

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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"
}
}
},

View File

@@ -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"
}
}
},

View File

@@ -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",

View File

@@ -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"
}
}
},

View File

@@ -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"
}
}
},

View File

@@ -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",

View File

@@ -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)",

View File

@@ -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"
}
}
},

View File

@@ -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"
}
}
},

View File

@@ -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",

View File

@@ -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",

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\EvaluatePageVisibility;
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
use ROITheme\Shared\Domain\Constants\VisibilityDefaults;
/**
* Caso de uso: Evaluar si un componente debe mostrarse en la página actual
*
* @package ROITheme\Shared\Application\UseCases\EvaluatePageVisibility
*/
final class EvaluatePageVisibilityUseCase
{
// NOTA: Usa VisibilityDefaults::DEFAULT_VISIBILITY para cumplir DRY
public function __construct(
private readonly PageTypeDetectorInterface $pageTypeDetector,
private readonly PageVisibilityRepositoryInterface $visibilityRepository
) {}
/**
* Evalúa si el componente debe mostrarse en la página actual
*/
public function execute(string $componentName): bool
{
$config = $this->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;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Constants;
/**
* Constantes de visibilidad por defecto para componentes
*
* Centraliza los valores por defecto para cumplir con DRY.
* Usado por:
* - EvaluatePageVisibilityUseCase (cuando no hay config en BD)
* - MigratePageVisibilityService (para crear registros iniciales)
*
* @package ROITheme\Shared\Domain\Constants
*/
final class VisibilityDefaults
{
/**
* Configuración de visibilidad por defecto para nuevos componentes
*
* - Home: SÍ mostrar (página principal)
* - Posts: SÍ mostrar (artículos del blog)
* - Pages: SÍ mostrar (páginas estáticas)
* - Archives: NO mostrar (listados de categorías/tags)
* - Search: NO mostrar (resultados de búsqueda)
*/
public const DEFAULT_VISIBILITY = [
'show_on_home' => 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',
];
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Contracts;
use ROITheme\Shared\Domain\ValueObjects\PageType;
/**
* Contrato para detectar el tipo de página actual
*
* @package ROITheme\Shared\Domain\Contracts
*/
interface PageTypeDetectorInterface
{
/**
* Detecta y retorna el tipo de página actual
*/
public function detect(): PageType;
public function isHome(): bool;
public function isPost(): bool;
public function isPage(): bool;
public function isArchive(): bool;
public function isSearch(): bool;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Contracts;
/**
* Contrato para acceder a la configuración de visibilidad por página
*
* @package ROITheme\Shared\Domain\Contracts
*/
interface PageVisibilityRepositoryInterface
{
/**
* Obtiene la configuración de visibilidad de un componente
*
* @param string $componentName Nombre del componente (kebab-case)
* @return array<string, bool> 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<string, bool> $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<string> 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<string, bool> $defaults Valores por defecto
*/
public function createDefaultVisibility(string $componentName, array $defaults): void;
}

View File

@@ -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)
];
/**

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\ValueObjects;
/**
* Value Object que representa los tipos de página válidos
*
* @package ROITheme\Shared\Domain\ValueObjects
*/
final class PageType
{
public const HOME = 'home';
public const POST = 'post';
public const PAGE = 'page';
public const ARCHIVE = 'archive';
public const SEARCH = 'search';
public const UNKNOWN = 'unknown';
private const VALID_TYPES = [
self::HOME,
self::POST,
self::PAGE,
self::ARCHIVE,
self::SEARCH,
self::UNKNOWN,
];
private function __construct(
private readonly string $value
) {}
public static function fromString(string $type): self
{
if (!in_array($type, self::VALID_TYPES, true)) {
return new self(self::UNKNOWN);
}
return new self($type);
}
public static function home(): self
{
return new self(self::HOME);
}
public static function post(): self
{
return new self(self::POST);
}
public static function page(): self
{
return new self(self::PAGE);
}
public static function archive(): self
{
return new self(self::ARCHIVE);
}
public static function search(): self
{
return new self(self::SEARCH);
}
public function value(): string
{
return $this->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',
};
}
}

View File

@@ -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('/<h2[^>]*>\s*<span[^>]*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: <h2><span data-shortcode="tcb_post_title"...>...</span></h2>
$result = preg_replace('/<h2[^>]*>\s*<span[^>]*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('/<p[^>]*>.*?\[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('/<p[^>]*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

View File

@@ -0,0 +1,352 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\CLI;
use WP_CLI;
/**
* Comando WP-CLI para limpiar contenido Thrive congelado de páginas
*
* 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%)
*
* 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('/<h2[^>]*>.*?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(
'/<h2[^>]*>.*?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(
'/<p[^>]*>.*?\[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(
'/<p[^>]*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';
}
}
}

View File

@@ -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'];
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
/**
* Implementación WordPress del repositorio de visibilidad
*
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
*/
final class WordPressPageVisibilityRepository implements PageVisibilityRepositoryInterface
{
private const GROUP_NAME = '_page_visibility';
private const TABLE_SUFFIX = 'roi_theme_component_settings';
private const VISIBILITY_FIELDS = [
'show_on_home',
'show_on_posts',
'show_on_pages',
'show_on_archives',
'show_on_search',
];
/**
* Constructor con inyección de dependencias
*
* IMPORTANTE: Sigue el patrón existente de WordPressComponentSettingsRepository
* donde $wpdb se inyecta por constructor, no se usa global.
*/
public function __construct(
private readonly \wpdb $wpdb
) {}
public function getVisibilityConfig(string $componentName): array
{
$table = $this->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);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
use ROITheme\Shared\Domain\Constants\VisibilityDefaults;
/**
* Servicio para migrar configuración de visibilidad inicial
*
* @package ROITheme\Shared\Infrastructure\Services
*/
final class MigratePageVisibilityService
{
// NOTA: Usa VisibilityDefaults::DEFAULT_VISIBILITY para cumplir DRY
public function __construct(
private readonly PageVisibilityRepositoryInterface $visibilityRepository
) {}
/**
* Ejecuta la migración para todos los componentes
*
* @return array{created: int, skipped: int}
*/
public function migrate(): array
{
$created = 0;
$skipped = 0;
$components = $this->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,
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Infrastructure\Di\DIContainer;
/**
* Facade/Helper para evaluar visibilidad de componentes
*
* PROPÓSITO:
* Permite que los Renderers existentes evalúen visibilidad sin modificar sus constructores.
* Actúa como un Service Locator limitado a este único propósito.
*
* USO EN RENDERERS:
* ```php
* if (!PageVisibilityHelper::shouldShow('cta-box-sidebar')) {
* return '';
* }
* ```
*
* @package ROITheme\Shared\Infrastructure\Services
*/
final class PageVisibilityHelper
{
/**
* Evalúa si un componente debe mostrarse en la página actual
*
* @param string $componentName Nombre del componente (kebab-case)
* @return bool True si debe mostrarse
*/
public static function shouldShow(string $componentName): bool
{
$container = DIContainer::getInstance();
$useCase = $container->getEvaluatePageVisibilityUseCase();
return $useCase->execute($componentName);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
use ROITheme\Shared\Domain\ValueObjects\PageType;
/**
* Implementación WordPress del detector de tipo de página
*
* @package ROITheme\Shared\Infrastructure\Services
*/
final class WordPressPageTypeDetector implements PageTypeDetectorInterface
{
public function detect(): PageType
{
if ($this->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();
}
}

View File

@@ -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();
?>
<!-- Hero Title Section (Template líneas 322-345) -->
<div class="container-fluid py-5 mb-4 hero-title">
<div class="container">
<?php while (have_posts()) : the_post(); ?>
<main id="main-content" class="site-main" role="main">
<!-- Hero Section - Componente dinámico -->
<?php
if (function_exists('roi_render_component')) {
echo roi_render_component('hero');
}
?>
<!-- Main Content Grid -->
<div class="container">
<div class="row">
<!-- Main Content Column (col-lg-9) -->
<div class="col-lg-9">
<!-- Featured Image - Componente dinámico -->
<?php
while ( have_posts() ) :
the_post();
// Categories Section
$categories = get_the_category();
if ( ! empty( $categories ) && count( $categories ) > 0 ) :
?>
<div class="mb-3 d-flex justify-content-center">
<div class="d-flex gap-2 flex-wrap justify-content-center">
<?php
// Limit to 3 categories max
$cat_count = 0;
foreach ( $categories as $category ) :
if ( $cat_count >= 3 ) break;
if ( $category->slug === 'uncategorized' ) continue;
?>
<a href="<?php echo esc_url( get_category_link( $category->term_id ) ); ?>" class="category-badge category-badge-hero">
<i class="bi bi-folder-fill me-1"></i>
<?php echo esc_html( $category->name ); ?>
</a>
<?php
$cat_count++;
endforeach;
?>
</div>
</div>
<?php endif; ?>
<!-- Page Title -->
<h1 class="display-5 fw-bold text-center">
<?php the_title(); ?>
</h1>
<?php endwhile; ?>
</div><!-- .container -->
</div><!-- .hero-title -->
<main id="main-content" class="site-main front-page" role="main">
<!-- Container Bootstrap (Template línea 347) -->
<div class="container">
<?php
while ( have_posts() ) :
the_post();
if (function_exists('roi_render_component')) {
echo roi_render_component('featured-image');
}
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<!-- Front Page Content -->
<div class="entry-content">
<!-- Page Content -->
<article id="post-<?php the_ID(); ?>" <?php post_class('post-content'); ?>>
<?php
the_content();
// Display page links for paginated pages
wp_link_pages(
array(
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'roi-theme' ),
wp_link_pages(array(
'before' => '<div class="page-links">' . esc_html__('Pages:', 'roi-theme'),
'after' => '</div>',
)
);
));
?>
</div><!-- .entry-content -->
</article>
<!-- Front Page Footer -->
<?php if ( get_edit_post_link() ) : ?>
<footer class="entry-footer">
<!-- Share Buttons - Componente dinámico -->
<?php
// Edit post link for logged-in users with permission
edit_post_link(
sprintf(
wp_kses(
/* translators: %s: Page title. Only visible to screen readers. */
__( 'Edit<span class="screen-reader-text"> "%s"</span>', 'roi-theme' ),
array(
'span' => array(
'class' => array(),
),
)
),
get_the_title()
),
'<span class="edit-link">',
'</span>'
);
if (function_exists('roi_render_component')) {
echo roi_render_component('social-share');
}
?>
</footer><!-- .entry-footer -->
<?php endif; ?>
</article><!-- #post-<?php the_ID(); ?> -->
<!-- CTA Post - Componente dinámico -->
<?php
// Display comments section if enabled
if ( comments_open() || get_comments_number() ) :
comments_template();
endif;
endwhile; // End of the loop.
/**
* Hook to display additional content on front page
* This can be used to add featured posts, testimonials, etc.
*/
do_action( 'roi_front_page_content' );
if (function_exists('roi_render_component')) {
echo roi_render_component('cta-post');
}
?>
</div><!-- .container -->
<!-- Related Posts - Componente dinámico -->
<?php
if (function_exists('roi_render_component')) {
echo roi_render_component('related-post');
}
?>
</main><!-- #main-content -->
<!-- Ad After Related Posts -->
<?php
if (function_exists('roi_render_ad_slot')) {
echo roi_render_ad_slot('after-related');
}
?>
</div><!-- .col-lg-9 -->
<!-- Sidebar Column (col-lg-3) -->
<div class="col-lg-3">
<div class="sidebar-sticky">
<!-- Table of Contents - Componente dinámico -->
<?php
if (function_exists('roi_render_component')) {
echo roi_render_component('table-of-contents');
}
?>
<!-- CTA Box Sidebar - Componente dinámico -->
<?php
if (function_exists('roi_render_component')) {
echo roi_render_component('cta-box-sidebar');
}
?>
</div>
</div>
</div><!-- .row -->
</div><!-- .container -->
</main><!-- #main-content -->
<?php endwhile; ?>
<!-- Contact Form Section - Componente dinámico -->
<?php
if (function_exists('roi_render_component')) {
echo roi_render_component('contact-form');
}
?>
<?php
get_footer();

View File

@@ -380,3 +380,127 @@ add_action('after_setup_theme', function() {
// desde la base de datos a través de sus respectivos Renderers.
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
// =============================================================================
// =============================================================================
// HELPER FUNCTION: roi_get_adsense_search_config()
// =============================================================================
/**
* Obtiene la configuracion de AdSense para resultados de busqueda
*
* Esta funcion es la API publica que el plugin roi-apu-search consume.
* El plugin NO debe acceder directamente a la tabla del tema.
*
* OPTIMIZACION: Una sola query carga todos los settings del componente.
*
* @return array Configuracion para JavaScript
*/
function roi_get_adsense_search_config(): array {
global $wpdb;
// =========================================================================
// CARGAR TODOS LOS SETTINGS EN UNA SOLA QUERY
// =========================================================================
$table = $wpdb->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'),
],
];
}

60
minify-css.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
/**
* Simple CSS Minifier Script
* Run from command line: php minify-css.php
*/
function minify_css($css) {
// Remove comments
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
// Remove space after colons
$css = str_replace(': ', ':', $css);
// Remove whitespace
$css = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ', ' '), '', $css);
// Remove space before and after specific characters
$css = preg_replace('/\s*([{};,>+~])\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";