- functions-addon.php: Validacion centralizada con wp_is_mobile() Componentes con show_on_mobile=false NO se renderizan en mobile Previene CLS de elementos ocultos con CSS - FeaturedImageRenderer: Agrega aspect-ratio 16/9 para reservar espacio Imagen usa object-fit:cover con position:absolute Metodo generateCSS() ahora publico para CriticalCSSService - CriticalCSSService: Agrega featured-image a CRITICAL_RENDERERS CSS se inyecta en <head> antes de que cargue contenido 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
7.1 KiB
PHP
231 lines
7.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
|
use ROITheme\Shared\Domain\Entities\Component;
|
|
|
|
/**
|
|
* FeaturedImageRenderer - Renderiza la imagen destacada del post
|
|
*
|
|
* RESPONSABILIDAD: Generar HTML y CSS de la imagen destacada
|
|
*
|
|
* CARACTERISTICAS:
|
|
* - Integracion con get_the_post_thumbnail()
|
|
* - Estilos configurables desde BD
|
|
* - Efecto hover opcional
|
|
* - Soporte responsive
|
|
*
|
|
* Cumple con:
|
|
* - DIP: Recibe CSSGeneratorInterface por constructor
|
|
* - SRP: Una responsabilidad (renderizar featured image)
|
|
* - Clean Architecture: Infrastructure puede usar WordPress
|
|
*
|
|
* @package ROITheme\Public\FeaturedImage\Infrastructure\Ui
|
|
*/
|
|
final class FeaturedImageRenderer implements RendererInterface
|
|
{
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator
|
|
) {}
|
|
|
|
public function render(Component $component): string
|
|
{
|
|
$data = $component->getData();
|
|
|
|
if (!$this->isEnabled($data)) {
|
|
return '';
|
|
}
|
|
|
|
if (!$this->shouldShowOnCurrentPage($data)) {
|
|
return '';
|
|
}
|
|
|
|
if (!$this->hasPostThumbnail()) {
|
|
return '';
|
|
}
|
|
|
|
$html = $this->buildHTML($data);
|
|
|
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
|
$isCritical = $data['visibility']['is_critical'] ?? false;
|
|
|
|
if ($isCritical) {
|
|
return $html; // Solo HTML, sin CSS inline
|
|
}
|
|
|
|
// CSS inline para componentes no críticos
|
|
$css = $this->generateCSS($data);
|
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
|
}
|
|
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'featured-image';
|
|
}
|
|
|
|
private function isEnabled(array $data): bool
|
|
{
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Generar CSS usando CSSGeneratorService
|
|
*
|
|
* Este método es público para que CriticalCSSService pueda
|
|
* generar CSS crítico antes de wp_head sin duplicar lógica.
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string CSS generado
|
|
*/
|
|
public function generateCSS(array $data): string
|
|
{
|
|
$spacing = $data['spacing'] ?? [];
|
|
$effects = $data['visual_effects'] ?? [];
|
|
$visibility = $data['visibility'] ?? [];
|
|
|
|
$marginTop = $spacing['margin_top'] ?? '1rem';
|
|
$marginBottom = $spacing['margin_bottom'] ?? '2rem';
|
|
// Aspect ratio para prevenir CLS - reserva espacio antes de que imagen cargue
|
|
$aspectRatio = $spacing['aspect_ratio'] ?? '16/9';
|
|
|
|
$borderRadius = $effects['border_radius'] ?? '12px';
|
|
$boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)';
|
|
$hoverEffect = $effects['hover_effect'] ?? true;
|
|
$hoverScale = $effects['hover_scale'] ?? '1.02';
|
|
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
|
|
|
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
|
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
|
|
|
$cssRules = [];
|
|
|
|
// Container styles con aspect-ratio para prevenir CLS
|
|
$cssRules[] = $this->cssGenerator->generate('.featured-image-container', [
|
|
'border-radius' => $borderRadius,
|
|
'overflow' => 'hidden',
|
|
'box-shadow' => $boxShadow,
|
|
'margin-top' => $marginTop,
|
|
'margin-bottom' => $marginBottom,
|
|
'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease",
|
|
'aspect-ratio' => $aspectRatio,
|
|
'position' => 'relative',
|
|
'background-color' => '#f0f0f0',
|
|
]);
|
|
|
|
// Image styles - object-fit para llenar el contenedor con aspect-ratio
|
|
$cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [
|
|
'width' => '100%',
|
|
'height' => '100%',
|
|
'object-fit' => 'cover',
|
|
'display' => 'block',
|
|
'transition' => "transform {$transitionDuration} ease",
|
|
'position' => 'absolute',
|
|
'top' => '0',
|
|
'left' => '0',
|
|
]);
|
|
|
|
// Hover effect
|
|
if ($hoverEffect) {
|
|
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover', [
|
|
'box-shadow' => '0 12px 32px rgba(0, 0, 0, 0.15)',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover img', [
|
|
'transform' => "scale({$hoverScale})",
|
|
]);
|
|
}
|
|
|
|
// Link styles (remove default link styling)
|
|
$cssRules[] = $this->cssGenerator->generate('.featured-image-container a', [
|
|
'display' => 'block',
|
|
'line-height' => '0',
|
|
]);
|
|
|
|
// Responsive visibility
|
|
if (!$showOnMobile) {
|
|
$cssRules[] = "@media (max-width: 767.98px) {
|
|
.featured-image-container { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
if (!$showOnDesktop) {
|
|
$cssRules[] = "@media (min-width: 768px) {
|
|
.featured-image-container { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
return implode("\n", $cssRules);
|
|
}
|
|
|
|
private function buildHTML(array $data): string
|
|
{
|
|
$content = $data['content'] ?? [];
|
|
|
|
$imageSize = $content['image_size'] ?? 'roi-featured-large';
|
|
$lazyLoading = $content['lazy_loading'] ?? false; // LCP: no lazy por defecto
|
|
$linkToMedia = $content['link_to_media'] ?? false;
|
|
|
|
$imgAttr = [
|
|
'class' => 'img-fluid featured-image',
|
|
'alt' => get_the_title(),
|
|
'fetchpriority' => 'high', // LCP optimization
|
|
];
|
|
|
|
if ($lazyLoading) {
|
|
$imgAttr['loading'] = 'lazy';
|
|
unset($imgAttr['fetchpriority']); // No fetchpriority si es lazy
|
|
}
|
|
|
|
$thumbnail = get_the_post_thumbnail(null, $imageSize, $imgAttr);
|
|
|
|
if (empty($thumbnail)) {
|
|
return '';
|
|
}
|
|
|
|
$html = '<div class="featured-image-container">';
|
|
|
|
if ($linkToMedia) {
|
|
$fullImageUrl = get_the_post_thumbnail_url(null, 'full');
|
|
$html .= sprintf(
|
|
'<a href="%s" target="_blank" rel="noopener" aria-label="%s">',
|
|
esc_url($fullImageUrl),
|
|
esc_attr__('Ver imagen en tamano completo', 'roi-theme')
|
|
);
|
|
}
|
|
|
|
$html .= $thumbnail;
|
|
|
|
if ($linkToMedia) {
|
|
$html .= '</a>';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
}
|