feat(pagespeed): implement YouTube Facade Pattern - Phase 2.4
PageSpeed Optimization to reduce TBT by ~2000ms: - Add YoutubeFacade module following Clean Architecture - Replace YouTube iframes with thumbnail + play button - Load real iframe only on user click (lazy-load) - Reduces initial page blocking time significantly Files added: - Public/YoutubeFacade/Infrastructure/Wordpress/YoutubeFacadeHooksRegistrar.php - Public/YoutubeFacade/Infrastructure/Services/YoutubeFacadeContentFilter.php - Public/YoutubeFacade/Infrastructure/Ui/YoutubeFacadeRenderer.php - Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css - Public/YoutubeFacade/Infrastructure/Ui/Assets/Js/youtube-facade.js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Public\YoutubeFacade\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Public\YoutubeFacade\Infrastructure\Ui\YoutubeFacadeRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters post content to replace YouTube iframes with facades
|
||||||
|
*
|
||||||
|
* Detects YouTube iframes (including those wrapped in .video-wrapper)
|
||||||
|
* and replaces them with lightweight facade HTML.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Public\YoutubeFacade
|
||||||
|
* @since 1.0.6
|
||||||
|
*/
|
||||||
|
final class YoutubeFacadeContentFilter
|
||||||
|
{
|
||||||
|
private YoutubeFacadeRenderer $renderer;
|
||||||
|
|
||||||
|
public function __construct(YoutubeFacadeRenderer $renderer)
|
||||||
|
{
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter content to replace YouTube iframes with facades
|
||||||
|
*
|
||||||
|
* @param string $content Post content
|
||||||
|
* @return string Filtered content
|
||||||
|
*/
|
||||||
|
public function filter(string $content): string
|
||||||
|
{
|
||||||
|
// Match YouTube iframes (with or without video-wrapper)
|
||||||
|
// Pattern handles both youtube.com/embed/ and youtube-nocookie.com/embed/
|
||||||
|
$pattern = '/<div class="video-wrapper">\s*<iframe[^>]*src="https?:\/\/(?:www\.)?(?:youtube\.com|youtube-nocookie\.com)\/embed\/([a-zA-Z0-9_-]+)[^"]*"[^>]*><\/iframe>\s*<\/div>/is';
|
||||||
|
|
||||||
|
$content = preg_replace_callback($pattern, function ($matches) {
|
||||||
|
$videoId = $matches[1];
|
||||||
|
return $this->renderer->render($videoId);
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// Also match standalone iframes without wrapper
|
||||||
|
$patternStandalone = '/<iframe[^>]*src="https?:\/\/(?:www\.)?(?:youtube\.com|youtube-nocookie\.com)\/embed\/([a-zA-Z0-9_-]+)[^"]*"[^>]*><\/iframe>/is';
|
||||||
|
|
||||||
|
$content = preg_replace_callback($patternStandalone, function ($matches) {
|
||||||
|
$videoId = $matches[1];
|
||||||
|
return $this->renderer->render($videoId);
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* YouTube Facade Styles
|
||||||
|
*
|
||||||
|
* PageSpeed Optimization Phase 2.4:
|
||||||
|
* Styles for the YouTube facade pattern (thumbnail + play button)
|
||||||
|
*
|
||||||
|
* @package ROITheme
|
||||||
|
* @since 1.0.6
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
FACADE CONTAINER
|
||||||
|
Inherits video-wrapper base styles
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.youtube-facade {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
THUMBNAIL
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.youtube-facade__thumbnail {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
PLAY BUTTON
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.youtube-facade__play {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
transition: transform 0.2s ease, filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-facade__play:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-facade__play:hover .youtube-facade__play-bg {
|
||||||
|
fill: #f00;
|
||||||
|
fill-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-facade__play:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-facade__play:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
IFRAME (after activation)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.youtube-facade iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
LOADING STATE
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.youtube-facade--loading .youtube-facade__play {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-facade--loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: -24px 0 0 -24px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: youtube-facade-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes youtube-facade-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* YouTube Facade - Lazy Load Handler
|
||||||
|
*
|
||||||
|
* PageSpeed Optimization Phase 2.4:
|
||||||
|
* - Loads YouTube iframe only on user click
|
||||||
|
* - Reduces TBT by ~2000ms
|
||||||
|
*
|
||||||
|
* @package ROITheme
|
||||||
|
* @since 1.0.6
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize facade click handlers
|
||||||
|
*/
|
||||||
|
function initYoutubeFacades() {
|
||||||
|
var facades = document.querySelectorAll('.youtube-facade[data-video-id]');
|
||||||
|
|
||||||
|
facades.forEach(function(facade) {
|
||||||
|
facade.addEventListener('click', handleFacadeClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on facade - load real iframe
|
||||||
|
* @param {Event} event Click event
|
||||||
|
*/
|
||||||
|
function handleFacadeClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var facade = event.currentTarget;
|
||||||
|
var videoId = facade.getAttribute('data-video-id');
|
||||||
|
|
||||||
|
if (!videoId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
facade.classList.add('youtube-facade--loading');
|
||||||
|
|
||||||
|
// Create iframe
|
||||||
|
var iframe = document.createElement('iframe');
|
||||||
|
iframe.setAttribute('src', 'https://www.youtube-nocookie.com/embed/' + videoId + '?autoplay=1&rel=0');
|
||||||
|
iframe.setAttribute('title', 'YouTube video');
|
||||||
|
iframe.setAttribute('frameborder', '0');
|
||||||
|
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
|
||||||
|
iframe.setAttribute('allowfullscreen', '');
|
||||||
|
|
||||||
|
// Wait for iframe to load before showing
|
||||||
|
iframe.addEventListener('load', function() {
|
||||||
|
facade.classList.remove('youtube-facade--loading');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear facade content and add iframe
|
||||||
|
facade.innerHTML = '';
|
||||||
|
facade.appendChild(iframe);
|
||||||
|
|
||||||
|
// Remove click handler (one-time activation)
|
||||||
|
facade.removeEventListener('click', handleFacadeClick);
|
||||||
|
facade.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initYoutubeFacades);
|
||||||
|
} else {
|
||||||
|
initYoutubeFacades();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Public\YoutubeFacade\Infrastructure\Ui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders YouTube facade HTML (thumbnail + play button)
|
||||||
|
*
|
||||||
|
* PageSpeed Optimization:
|
||||||
|
* - Shows YouTube thumbnail image (lazy-loaded)
|
||||||
|
* - Displays play button overlay
|
||||||
|
* - Real iframe loads only on user click (via JS)
|
||||||
|
* - Reduces TBT by ~2000ms
|
||||||
|
*
|
||||||
|
* @package ROITheme\Public\YoutubeFacade
|
||||||
|
* @since 1.0.6
|
||||||
|
*/
|
||||||
|
final class YoutubeFacadeRenderer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Render facade HTML for a YouTube video
|
||||||
|
*
|
||||||
|
* @param string $videoId YouTube video ID
|
||||||
|
* @return string Facade HTML
|
||||||
|
*/
|
||||||
|
public function render(string $videoId): string
|
||||||
|
{
|
||||||
|
$videoId = esc_attr($videoId);
|
||||||
|
|
||||||
|
// Use maxresdefault with fallback to hqdefault
|
||||||
|
// YouTube provides thumbnails at: maxresdefault, sddefault, hqdefault, mqdefault, default
|
||||||
|
$thumbnailUrl = "https://i.ytimg.com/vi/{$videoId}/maxresdefault.jpg";
|
||||||
|
|
||||||
|
$html = <<<HTML
|
||||||
|
<div class="youtube-facade video-wrapper" data-video-id="{$videoId}">
|
||||||
|
<img
|
||||||
|
class="youtube-facade__thumbnail"
|
||||||
|
src="{$thumbnailUrl}"
|
||||||
|
alt="Video thumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
onerror="this.src='https://i.ytimg.com/vi/{$videoId}/hqdefault.jpg'"
|
||||||
|
/>
|
||||||
|
<button class="youtube-facade__play" aria-label="Play video">
|
||||||
|
<svg viewBox="0 0 68 48" width="68" height="48">
|
||||||
|
<path class="youtube-facade__play-bg" d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#212121" fill-opacity="0.8"/>
|
||||||
|
<path class="youtube-facade__play-icon" d="M 45,24 27,14 27,34" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Public\YoutubeFacade\Infrastructure\Wordpress;
|
||||||
|
|
||||||
|
use ROITheme\Public\YoutubeFacade\Infrastructure\Services\YoutubeFacadeContentFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers WordPress hooks for YouTube Facade Pattern
|
||||||
|
*
|
||||||
|
* PageSpeed Optimization Phase 2.4:
|
||||||
|
* - Reduces TBT by ~2000ms by lazy-loading YouTube iframes
|
||||||
|
* - Shows thumbnail + play button instead of full iframe
|
||||||
|
* - Loads real iframe only on user click
|
||||||
|
*
|
||||||
|
* @package ROITheme\Public\YoutubeFacade
|
||||||
|
* @since 1.0.6
|
||||||
|
*/
|
||||||
|
final class YoutubeFacadeHooksRegistrar
|
||||||
|
{
|
||||||
|
private YoutubeFacadeContentFilter $contentFilter;
|
||||||
|
|
||||||
|
public function __construct(YoutubeFacadeContentFilter $contentFilter)
|
||||||
|
{
|
||||||
|
$this->contentFilter = $contentFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all hooks
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Filter post content to replace YouTube iframes with facades
|
||||||
|
add_filter('the_content', [$this->contentFilter, 'filter'], 20);
|
||||||
|
|
||||||
|
// Enqueue facade assets
|
||||||
|
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets'], 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue CSS and JS for YouTube facades
|
||||||
|
*/
|
||||||
|
public function enqueueAssets(): void
|
||||||
|
{
|
||||||
|
// Only on single posts where videos might appear
|
||||||
|
if (!is_single()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'roi-youtube-facade',
|
||||||
|
get_template_directory_uri() . '/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css',
|
||||||
|
[],
|
||||||
|
ROI_VERSION,
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'roi-youtube-facade',
|
||||||
|
get_template_directory_uri() . '/Public/YoutubeFacade/Infrastructure/Ui/Assets/Js/youtube-facade.js',
|
||||||
|
[],
|
||||||
|
ROI_VERSION,
|
||||||
|
[
|
||||||
|
'in_footer' => true,
|
||||||
|
'strategy' => 'defer',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,6 +156,17 @@ try {
|
|||||||
);
|
);
|
||||||
$themeSettingsInjector->register();
|
$themeSettingsInjector->register();
|
||||||
|
|
||||||
|
// === YOUTUBE FACADE (PageSpeed Optimization Phase 2.4) ===
|
||||||
|
// Lazy-load YouTube iframes to reduce TBT by ~2000ms
|
||||||
|
$youtubeFacadeRenderer = new \ROITheme\Public\YoutubeFacade\Infrastructure\Ui\YoutubeFacadeRenderer();
|
||||||
|
$youtubeFacadeFilter = new \ROITheme\Public\YoutubeFacade\Infrastructure\Services\YoutubeFacadeContentFilter(
|
||||||
|
$youtubeFacadeRenderer
|
||||||
|
);
|
||||||
|
$youtubeFacadeHooksRegistrar = new \ROITheme\Public\YoutubeFacade\Infrastructure\Wordpress\YoutubeFacadeHooksRegistrar(
|
||||||
|
$youtubeFacadeFilter
|
||||||
|
);
|
||||||
|
$youtubeFacadeHooksRegistrar->register();
|
||||||
|
|
||||||
// Log en modo debug
|
// Log en modo debug
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
error_log('ROI Theme: Admin Panel initialized successfully');
|
error_log('ROI Theme: Admin Panel initialized successfully');
|
||||||
|
|||||||
Reference in New Issue
Block a user