settings = $this->get_settings();
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_action('admin_init', [$this, 'handle_actions']);
add_action('admin_bar_menu', [$this, 'add_admin_bar_menu'], 100);
add_filter('plugin_action_links_' . plugin_basename(__FILE__), [$this, 'add_action_links']);
add_action('admin_enqueue_scripts', [$this, 'admin_styles']);
// Initialize optimization features
$this->init_optimizations();
// Initialize plugin compatibility hooks for cache invalidation
$this->init_plugin_compatibility();
// Initialize smart page cache purge hooks
$this->init_page_cache_purge_hooks();
}
/**
* Initialize hooks for smart page cache purging
* Automatically purges nginx cache when content changes
*/
private function init_page_cache_purge_hooks() {
// Only initialize if page cache is enabled
if (!$this->settings['page_cache']) {
return;
}
// Post changes
add_action('save_post', [$this, 'purge_post_cache'], 10, 3);
add_action('wp_trash_post', [$this, 'purge_post_cache_on_delete']);
add_action('delete_post', [$this, 'purge_post_cache_on_delete']);
add_action('edit_post', [$this, 'purge_post_cache_on_edit'], 10, 2);
// Comment changes
add_action('comment_post', [$this, 'purge_comment_cache'], 10, 2);
add_action('edit_comment', [$this, 'purge_comment_cache']);
add_action('wp_set_comment_status', [$this, 'purge_comment_cache']);
// Menu changes
add_action('wp_update_nav_menu', [$this, 'purge_all_cache']);
// Theme/Customizer changes
add_action('switch_theme', [$this, 'purge_all_cache']);
add_action('customize_save_after', [$this, 'purge_all_cache']);
add_action('update_option_theme_mods_' . get_template(), [$this, 'purge_all_cache']);
// Sidebar/Widget changes
add_action('update_option_sidebars_widgets', [$this, 'purge_all_cache']);
// Permalink structure changes
add_action('update_option_permalink_structure', [$this, 'purge_all_cache']);
// WooCommerce specific hooks
if (class_exists('WooCommerce')) {
add_action('woocommerce_product_set_stock', [$this, 'purge_woocommerce_product']);
add_action('woocommerce_variation_set_stock', [$this, 'purge_woocommerce_product']);
add_action('woocommerce_product_set_stock_status', [$this, 'purge_woocommerce_product_stock_status'], 10, 3);
add_action('woocommerce_update_product', [$this, 'purge_woocommerce_product_by_id']);
add_action('woocommerce_new_product', [$this, 'purge_woocommerce_product_by_id']);
add_action('woocommerce_delete_product', [$this, 'purge_woocommerce_product_by_id']);
add_action('woocommerce_trash_product', [$this, 'purge_woocommerce_product_by_id']);
}
}
/**
* Purge cache for a specific post and related pages
*/
public function purge_post_cache($post_id, $post = null, $update = true) {
// Skip autosaves and revisions
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
if (!$post) {
$post = get_post($post_id);
}
if (!$post || $post->post_status === 'auto-draft') {
return;
}
$paths_to_purge = $this->get_post_related_paths($post);
$this->purge_paths($paths_to_purge);
}
/**
* Purge cache when post is deleted or trashed
*/
public function purge_post_cache_on_delete($post_id) {
$post = get_post($post_id);
if (!$post) {
return;
}
$paths_to_purge = $this->get_post_related_paths($post);
$this->purge_paths($paths_to_purge);
}
/**
* Purge cache on post edit
*/
public function purge_post_cache_on_edit($post_id, $post) {
$this->purge_post_cache($post_id, $post, true);
}
/**
* Purge cache when a comment changes
*/
public function purge_comment_cache($comment_id, $approved = null) {
$comment = get_comment($comment_id);
if (!$comment) {
return;
}
$post = get_post($comment->comment_post_ID);
if ($post) {
$paths_to_purge = [get_permalink($post)];
$this->purge_paths($paths_to_purge);
}
}
/**
* Get all paths related to a post that should be purged
*/
private function get_post_related_paths($post) {
$paths = [];
// Post URL
$permalink = get_permalink($post);
if ($permalink) {
$paths[] = $permalink;
}
// Home page
$paths[] = home_url('/');
// Blog page (if different from home)
$blog_page_id = get_option('page_for_posts');
if ($blog_page_id) {
$paths[] = get_permalink($blog_page_id);
}
// Category archives
$categories = get_the_category($post->ID);
if ($categories) {
foreach ($categories as $category) {
$paths[] = get_category_link($category->term_id);
}
}
// Tag archives
$tags = get_the_tags($post->ID);
if ($tags) {
foreach ($tags as $tag) {
$paths[] = get_tag_link($tag->term_id);
}
}
// Author archive
$paths[] = get_author_posts_url($post->post_author);
// Date archives
$post_date = strtotime($post->post_date);
$paths[] = get_year_link(date('Y', $post_date));
$paths[] = get_month_link(date('Y', $post_date), date('m', $post_date));
$paths[] = get_day_link(date('Y', $post_date), date('m', $post_date), date('d', $post_date));
// Post type archive (for custom post types)
if ($post->post_type !== 'post' && $post->post_type !== 'page') {
$archive_link = get_post_type_archive_link($post->post_type);
if ($archive_link) {
$paths[] = $archive_link;
}
}
// Taxonomy archives for custom post types
$taxonomies = get_object_taxonomies($post->post_type);
foreach ($taxonomies as $taxonomy) {
if ($taxonomy !== 'category' && $taxonomy !== 'post_tag') {
$terms = get_the_terms($post->ID, $taxonomy);
if ($terms && !is_wp_error($terms)) {
foreach ($terms as $term) {
$paths[] = get_term_link($term);
}
}
}
}
// Remove duplicates and filter
$paths = array_unique(array_filter($paths, function($path) {
return !is_wp_error($path) && !empty($path);
}));
return $paths;
}
/**
* Purge cache for WooCommerce product (from stock change)
*/
public function purge_woocommerce_product($product) {
if (is_numeric($product)) {
$product_id = $product;
} else {
$product_id = $product->get_id();
}
$this->purge_woocommerce_product_by_id($product_id);
}
/**
* Purge cache for WooCommerce product stock status change
*/
public function purge_woocommerce_product_stock_status($product_id, $stock_status, $product) {
$this->purge_woocommerce_product_by_id($product_id);
}
/**
* Purge cache for WooCommerce product by ID
*/
public function purge_woocommerce_product_by_id($product_id) {
$paths = [];
// Product URL
$permalink = get_permalink($product_id);
if ($permalink) {
$paths[] = $permalink;
}
// Home page
$paths[] = home_url('/');
// Shop page
$shop_page_id = wc_get_page_id('shop');
if ($shop_page_id > 0) {
$paths[] = get_permalink($shop_page_id);
}
// Product categories
$terms = get_the_terms($product_id, 'product_cat');
if ($terms && !is_wp_error($terms)) {
foreach ($terms as $term) {
$paths[] = get_term_link($term);
}
}
// Product tags
$tags = get_the_terms($product_id, 'product_tag');
if ($tags && !is_wp_error($tags)) {
foreach ($tags as $tag) {
$paths[] = get_term_link($tag);
}
}
$paths = array_unique(array_filter($paths, function($path) {
return !is_wp_error($path) && !empty($path);
}));
$this->purge_paths($paths);
}
/**
* Purge all cache (for site-wide changes)
*/
public function purge_all_cache() {
$this->purge_paths(['/']);
}
/**
* Send purge request to Jabali Panel API
*/
private function purge_paths(array $paths) {
if (empty($paths)) {
return false;
}
// Get domain from site URL
$site_url = get_site_url();
$parsed = parse_url($site_url);
$domain = $parsed['host'] ?? '';
if (empty($domain)) {
return false;
}
// Generate secret from AUTH_KEY
$secret = defined('AUTH_KEY') ? substr(md5(AUTH_KEY), 0, 32) : '';
if (empty($secret)) {
return false;
}
// Convert full URLs to paths
$normalized_paths = [];
foreach ($paths as $url) {
if ($url === '/') {
$normalized_paths[] = '/';
} else {
$parsed_url = parse_url($url);
$path = $parsed_url['path'] ?? '/';
$normalized_paths[] = $path;
}
}
$normalized_paths = array_unique($normalized_paths);
// Check if we should purge all (if home is in the list)
$purge_all = in_array('/', $normalized_paths) && count($normalized_paths) > 5;
// Call Jabali Panel internal API
$response = wp_remote_post('https://127.0.0.1/api/internal/page-cache-purge', [
'timeout' => 10,
'sslverify' => false,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => json_encode([
'domain' => $domain,
'paths' => $purge_all ? [] : $normalized_paths,
'purge_all' => $purge_all,
'secret' => $secret,
]),
]);
if (is_wp_error($response)) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('Jabali Cache: Failed to purge page cache - ' . $response->get_error_message());
}
return false;
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
if (defined('WP_DEBUG') && WP_DEBUG) {
$body = wp_remote_retrieve_body($response);
error_log('Jabali Cache: Failed to purge page cache - HTTP ' . $code . ' - ' . $body);
}
return false;
}
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('Jabali Cache: Purged ' . count($normalized_paths) . ' paths');
}
return true;
}
/**
* Initialize hooks for theme/plugin asset regeneration
* Uses general WordPress hooks instead of plugin-specific ones
*/
private function init_plugin_compatibility() {
// General: Catch ALL option updates that might trigger CSS/JS regeneration
add_action('updated_option', [$this, 'on_option_updated'], 10, 3);
// General: Theme/plugin updates
add_action('upgrader_process_complete', [$this, 'flush_all_builder_cache'], 10, 2);
add_action('switch_theme', [$this, 'flush_all_builder_cache']);
add_action('customize_save_after', [$this, 'flush_all_builder_cache']);
// General: When any cache clear action is triggered by popular builders
// These are catch-all hooks that most builders fire
add_action('wp_cache_flush', [$this, 'flush_all_builder_cache']);
add_action('litespeed_purge_all', [$this, 'flush_all_builder_cache']);
// WooCommerce product changes (affects displayed content)
add_action('woocommerce_delete_product_transients', [$this, 'flush_woocommerce_cache']);
add_action('woocommerce_clear_product_transients', [$this, 'flush_woocommerce_cache']);
}
/**
* Patterns in option names that indicate CSS/JS regeneration needed
*/
private function get_builder_option_patterns(): array {
return [
// Page builders
'elementor', 'uagb', 'spectra', 'fl_builder', 'beaver', 'et_divi', 'et_builder',
'vc_', 'wpb_', 'generateblocks', 'kadence', 'brizy', 'oxygen', 'breakdance',
// Themes
'astra', 'theme_mods_', 'flavor',
// Optimization plugins
'autoptimize', 'wp_rocket', 'sg_optimizer', 'litespeed', 'w3tc', 'wp_optimize',
// CSS/JS settings
'_css', '_js', '_style', '_script', '_assets', '_cache',
];
}
/**
* Check if option update should trigger cache flush
*/
public function on_option_updated($option, $old_value, $new_value) {
// Skip if values are identical
if ($old_value === $new_value) {
return;
}
$option_lower = strtolower($option);
foreach ($this->get_builder_option_patterns() as $pattern) {
if (strpos($option_lower, $pattern) !== false) {
$this->flush_all_builder_cache();
return;
}
}
}
/**
* Flush ALL builder-related transients and page cache
* Single method that handles all builders/themes
*/
public function flush_all_builder_cache($upgrader = null, $options = null) {
// Prevent multiple flushes in same request
static $flushed = false;
if ($flushed) {
return;
}
$flushed = true;
// Flush Redis transients matching any builder pattern
$patterns = [
// Page builders
'elementor_', '_elementor', 'uagb_', '_uagb', 'spectra_', '_spectra',
'fl_builder_', '_fl_builder', 'et_core_', 'et_builder_', '_et_core', '_et_builder',
'vc_', '_vc_', 'generateblocks_', 'kadence_', 'kt_', 'brizy_', 'oxygen_', 'breakdance_',
// Themes
'astra_', '_astra', 'theme_mod_',
// Optimization
'autoptimize_', 'wprocket_', 'litespeed_',
// Generic
'_css_', '_js_', '_style_', '_assets_',
];
$this->flush_transients_by_patterns($patterns);
// Purge nginx page cache
if ($this->settings['page_cache']) {
$this->purge_all_cache();
}
$this->log_cache_flush('Builder/Theme');
}
/**
* Flush WooCommerce related transients
*/
public function flush_woocommerce_cache() {
$this->flush_transients_by_patterns(['wc_', '_wc_', 'woocommerce_']);
$this->log_cache_flush('WooCommerce');
}
/**
* Flush transients matching specific patterns from Redis
*
* @param array $patterns Array of patterns to match (e.g., ['uagb_', 'spectra_'])
* @return int Number of keys deleted
*/
private function flush_transients_by_patterns(array $patterns) {
if (!$this->is_redis_connected()) {
return 0;
}
try {
$redis = new Redis();
$host = defined('JABALI_CACHE_HOST') ? JABALI_CACHE_HOST : '127.0.0.1';
$port = defined('JABALI_CACHE_PORT') ? JABALI_CACHE_PORT : 6379;
$db = defined('JABALI_CACHE_DATABASE') ? JABALI_CACHE_DATABASE : 0;
$prefix = defined('JABALI_CACHE_PREFIX') ? JABALI_CACHE_PREFIX : '';
if (!$redis->connect($host, $port, 2)) {
return 0;
}
// Authenticate if credentials are defined
if (defined('JABALI_CACHE_REDIS_USER') && defined('JABALI_CACHE_REDIS_PASS')) {
$redis->auth(['user' => JABALI_CACHE_REDIS_USER, 'pass' => JABALI_CACHE_REDIS_PASS]);
} elseif (defined('JABALI_CACHE_REDIS_PASS') && JABALI_CACHE_REDIS_PASS) {
$redis->auth(JABALI_CACHE_REDIS_PASS);
}
$redis->select($db);
$deleted = 0;
foreach ($patterns as $pattern) {
// Search for transient keys matching the pattern
// WordPress stores transients as: {prefix}transient_{name} and {prefix}_transient_{name}
$scan_patterns = [
$prefix . '*transient*' . $pattern . '*',
$prefix . '*' . $pattern . '*transient*',
$prefix . '*' . $pattern . '*',
];
foreach ($scan_patterns as $scan_pattern) {
$cursor = null;
do {
$keys = $redis->scan($cursor, $scan_pattern, 100);
if ($keys !== false && !empty($keys)) {
foreach ($keys as $key) {
$redis->del($key);
$deleted++;
}
}
} while ($cursor > 0);
}
}
$redis->close();
return $deleted;
} catch (Exception $e) {
error_log('Jabali Cache: Failed to flush transients - ' . $e->getMessage());
return 0;
}
}
/**
* Log cache flush event for debugging
*/
private function log_cache_flush($plugin_name) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('Jabali Cache: Flushed Redis transients triggered by %s asset regeneration', $plugin_name));
}
}
public function get_settings() {
$defaults = [
'page_cache' => true,
'object_cache' => true,
'html_minify' => false,
'minify_css' => true,
'minify_js' => true,
'lazy_load' => false,
'lazy_load_iframes' => false,
'browser_cache' => false,
'expired_cache' => false,
'remove_query_strings' => false,
'disable_emojis' => false,
'disable_embeds' => false,
'defer_js' => false,
'cache_debug' => true,
'local_google_fonts' => false,
// LCP Optimization settings
'lcp_preload' => true,
'preconnect_hints' => true,
'preload_fonts' => true,
'delay_third_party' => false,
];
$saved = get_option(self::OPTION_KEY, []);
return array_merge($defaults, $saved);
}
public function init_optimizations() {
// Page Cache bypass - send no-cache headers when disabled
if (!$this->settings['page_cache'] && !is_admin()) {
add_action('send_headers', [$this, 'disable_page_cache_headers'], 1);
}
// HTML Minification
if ($this->settings['html_minify'] && !is_admin()) {
add_action('template_redirect', [$this, 'start_html_buffer'], 1);
}
// CSS Minification
if ($this->settings['minify_css'] && !is_admin()) {
add_filter('style_loader_tag', [$this, 'minify_inline_css'], 10, 4);
add_action('wp_head', [$this, 'start_css_capture'], 1);
add_action('wp_head', [$this, 'end_css_capture'], 999);
}
// JS Minification
if ($this->settings['minify_js'] && !is_admin()) {
add_filter('script_loader_tag', [$this, 'minify_inline_js'], 10, 3);
}
// Defer JS
if ($this->settings['defer_js'] && !is_admin()) {
add_filter('script_loader_tag', [$this, 'defer_js'], 10, 3);
}
// Lazy Loading
if ($this->settings['lazy_load'] && !is_admin()) {
add_filter('the_content', [$this, 'add_lazy_loading']);
add_filter('post_thumbnail_html', [$this, 'add_lazy_loading']);
add_filter('get_avatar', [$this, 'add_lazy_loading']);
add_filter('widget_text', [$this, 'add_lazy_loading']);
}
// Lazy Load iframes
if ($this->settings['lazy_load_iframes'] && !is_admin()) {
add_filter('the_content', [$this, 'add_lazy_loading_iframes']);
add_filter('embed_oembed_html', [$this, 'add_lazy_loading_iframes']);
}
// Remove Query Strings
if ($this->settings['remove_query_strings'] && !is_admin()) {
add_filter('script_loader_src', [$this, 'remove_query_strings'], 15);
add_filter('style_loader_src', [$this, 'remove_query_strings'], 15);
}
// Disable Emojis
if ($this->settings['disable_emojis']) {
$this->disable_emojis();
}
// Disable Embeds
if ($this->settings['disable_embeds']) {
$this->disable_embeds();
}
// Expired Cache (No-Cache Headers)
if ($this->settings['expired_cache'] && !is_admin()) {
add_action('template_redirect', [$this, 'send_expired_cache_headers'], 1);
add_action('send_headers', [$this, 'send_expired_cache_headers']);
}
// Debug mode - add debug info for admins
if ($this->settings['cache_debug'] && !is_admin()) {
add_action('wp_footer', [$this, 'output_debug_info'], 999);
}
// Local Google Fonts
if ($this->settings['local_google_fonts'] && !is_admin()) {
add_filter('style_loader_src', [$this, 'localize_google_fonts_css'], 10, 2);
add_filter('wp_resource_hints', [$this, 'remove_google_fonts_preconnect'], 10, 2);
}
// LCP Image Optimization (preload + fetchpriority)
if ($this->settings['lcp_preload'] && !is_admin()) {
add_filter('the_content', [$this, 'optimize_lcp_image'], 5);
add_filter('post_thumbnail_html', [$this, 'optimize_lcp_image'], 5);
add_action('wp_head', [$this, 'output_lcp_preload'], 1);
}
// Preconnect Hints
if ($this->settings['preconnect_hints'] && !is_admin()) {
add_action('wp_head', [$this, 'output_preconnect_hints'], 1);
}
// Font Preload
if ($this->settings['preload_fonts'] && !is_admin()) {
add_action('wp_head', [$this, 'output_font_preload'], 2);
}
// Delay Third-Party Scripts
if ($this->settings['delay_third_party'] && !is_admin()) {
add_filter('script_loader_tag', [$this, 'delay_third_party_scripts'], 10, 3);
}
}
/**
* CSS Minification - minify inline styles in style tags
*/
public function minify_inline_css($html, $handle, $href, $media) {
return $html; // Return unchanged, we don't minify external CSS files
}
private $css_buffer_active = false;
public function start_css_capture() {
// Not implemented - inline CSS minification would require output buffering
// which conflicts with page cache. External files are better cached by nginx.
}
public function end_css_capture() {
// Not implemented
}
/**
* Minify inline JS within script tags
*/
public function minify_inline_js($tag, $handle, $src) {
// Don't touch external scripts
if (!empty($src)) {
return $tag;
}
// Skip certain handles that shouldn't be minified
$skip_handles = ['jquery-core', 'jquery-migrate', 'wp-embed'];
if (in_array($handle, $skip_handles)) {
return $tag;
}
// Simple minification for inline scripts - just collapse whitespace
// Being conservative to avoid breaking scripts
$tag = preg_replace_callback('/';
}, $tag);
return $tag;
}
/**
* Output debug info for admins
*/
public function output_debug_info() {
// Only show for admins with cache_debug param or setting enabled
if (!current_user_can('manage_options')) {
return;
}
// Check if ?cache_debug=1 is set
if (!isset($_GET['cache_debug']) && !$this->settings['cache_debug']) {
return;
}
// Only output if explicitly requested via query param
if (!isset($_GET['cache_debug']) || $_GET['cache_debug'] !== '1') {
return;
}
$debug_info = [];
$debug_info['plugin_version'] = self::VERSION;
$debug_info['page_cache'] = $this->settings['page_cache'] ? 'enabled' : 'disabled';
$debug_info['object_cache'] = $this->is_drop_in_installed() ? 'enabled' : 'disabled';
$debug_info['redis_connected'] = $this->is_redis_connected() ? 'yes' : 'no';
$debug_info['minify_css'] = $this->settings['minify_css'] ? 'enabled' : 'disabled';
$debug_info['minify_js'] = $this->settings['minify_js'] ? 'enabled' : 'disabled';
$debug_info['defer_js'] = $this->settings['defer_js'] ? 'enabled' : 'disabled';
$debug_info['lazy_load'] = $this->settings['lazy_load'] ? 'enabled' : 'disabled';
$debug_info['logged_in'] = is_user_logged_in() ? 'yes' : 'no';
$debug_info['bypass_reason'] = $this->get_cache_bypass_reason();
$debug_info['page_generated'] = date('Y-m-d H:i:s');
$debug_info['queries'] = get_num_queries();
$debug_info['memory_peak'] = size_format(memory_get_peak_usage(true));
echo "\n\n";
}
/**
* Get reason why page cache might be bypassed
*/
private function get_cache_bypass_reason() {
if (is_user_logged_in()) {
return 'logged_in_user';
}
if (isset($_POST) && !empty($_POST)) {
return 'post_request';
}
if (is_admin()) {
return 'admin_page';
}
// Check for WooCommerce cart/checkout
if (function_exists('is_cart') && is_cart()) {
return 'woocommerce_cart';
}
if (function_exists('is_checkout') && is_checkout()) {
return 'woocommerce_checkout';
}
if (function_exists('is_account_page') && is_account_page()) {
return 'woocommerce_account';
}
// Check for cookies that trigger bypass
if (isset($_COOKIE['wordpress_logged_in']) || isset($_COOKIE['wp-postpass'])) {
return 'bypass_cookie';
}
if (isset($_COOKIE['woocommerce_cart_hash']) || isset($_COOKIE['woocommerce_items_in_cart'])) {
return 'woocommerce_cookie';
}
return 'none';
}
// HTML Minification
public function start_html_buffer() {
ob_start([$this, 'minify_html']);
}
public function minify_html($html) {
if (empty($html)) {
return $html;
}
// Don't minify feeds, sitemaps, or admin
if (is_feed() || is_admin()) {
return $html;
}
// Preserve scripts and styles
$preserved = [];
$html = preg_replace_callback('/<(script|style|pre|textarea)[^>]*>.*?<\/\1>/is', function($match) use (&$preserved) {
$key = '';
$preserved[$key] = $match[0];
return $key;
}, $html);
// Remove HTML comments (except IE conditionals and PRESERVE markers)
$html = preg_replace('//s', '', $html);
// Remove whitespace between tags
$html = preg_replace('/>\s+', '><', $html);
// Remove extra whitespace
$html = preg_replace('/\s{2,}/', ' ', $html);
// Remove whitespace around block elements
$html = preg_replace('/\s*(<\/?(?:html|head|body|div|section|article|header|footer|nav|aside|main|p|h[1-6]|ul|ol|li|table|tr|td|th|form|fieldset|dl|dt|dd|blockquote|hr|br)[^>]*>)\s*/i', '$1', $html);
// Restore preserved content
$html = str_replace(array_keys($preserved), array_values($preserved), $html);
return trim($html);
}
// Lazy Loading
public function add_lazy_loading($content) {
if (empty($content)) {
return $content;
}
// Add loading="lazy" to images that don't have it
$content = preg_replace_callback('/
]+)>/i', function($match) {
$img = $match[0];
// Skip if already has loading attribute
if (strpos($img, 'loading=') !== false) {
return $img;
}
// Skip base64 images and SVGs
if (strpos($img, 'data:image') !== false || strpos($img, '.svg') !== false) {
return $img;
}
// Add loading="lazy"
return str_replace('
]+)>/i', function($match) {
$iframe = $match[0];
if (strpos($iframe, 'loading=') !== false) {
return $iframe;
}
return str_replace('