Files
wpmu-dev-analysis/02-TECHNICAL-SPECIFICATION.md

17 KiB
Raw Permalink Blame History

Technical Specification: Custom Caching & Image Optimization Plugins

Document Purpose

This spec defines the exact architecture, API, and integration points for the two replacement plugins:

  1. help4bis-performance-cache — HTTP caching + database cleanup
  2. help4bis-image-optimizer — Image compression, WebP, lazy load, responsive images

Plugin 1: help4bis-performance-cache

Scope

  • HTTP cache headers (browser-level, not page HTML caching)
  • Database cleanup (revisions, orphaned meta, expired transients)
  • Cache invalidation hooks
  • Elementor CSS cache integration
  • Simple admin UI (settings, clear button)

NOT in scope

  • Page HTML caching (complex, low ROI given Elementor sites are mostly dynamic)
  • Redis/memcached integration (single-server, not needed)
  • CloudFlare integration
  • Lazy loading (handled by image optimizer)

File Structure

help4bis-performance-cache/
├── help4bis-performance-cache.php     # Plugin header + activation hooks
├── admin/
│   ├── class-settings-page.php        # WP admin page
│   ├── class-cache-manager.php        # Clear cache handler
│   └── partials/
│       └── settings.html              # Settings form
├── includes/
│   ├── class-cache.php                # HTTP header logic
│   ├── class-exclusions.php           # Minify exclusion manager
│   ├── class-elementor-integration.php # Elementor cache invalidation
│   └── class-database-cleanup.php     # Revision/transient cleanup
├── hooks/
│   ├── class-invalidation.php         # Post save/delete hooks
│   └── class-scheduled-cleanup.php    # Cron jobs
└── tests/
    ├── test-cache.php
    ├── test-invalidation.php
    └── test-elementor-integration.php

Configuration (in wp_options)

// Single option containing all settings
'h4b_cache_settings' => array(
  'enabled' => true,
  'browser_cache_css' => '1y',         // CSS browser cache
  'browser_cache_js' => '1y',          // JS browser cache
  'browser_cache_images' => '1y',      // Images browser cache
  'browser_cache_media' => '1y',       // Media files browser cache
  'browser_cache_html' => '0',         // HTML (always no-cache for dynamic)
  'auto_cleanup' => true,
  'cleanup_frequency' => 'weekly',     // weekly, biweekly, monthly
  'cleanup_day' => 'friday',
  'cleanup_time' => '03:00',           // 3 AM AEST
  'cleanup_revisions_keep' => 30,      // Days of revisions to keep
  'cleanup_transient_expired' => true, // Delete expired transients
  'cleanup_orphaned_meta' => true,     // Delete orphaned postmeta
  'exclude_scripts' => array(          // Scripts that skip minify (if added later)
    'jquery',
    'elementor-pro-app',
    // ... (copy from Hummingbird config)
  ),
  'exclude_styles' => array(),         // Styles that skip minify
);

HTTP Headers Set by Plugin

On all requests, set these headers:

Cache-Control: public, max-age=31536000   (for .css, .js, .jpg, .png, .gif, .woff, .woff2)
Cache-Control: no-cache, no-store, must-revalidate (for .html, /wp-admin/, /wp-json/)
Expires: <1 year from now>
Vary: Accept-Encoding

For static assets, also set:

ETag: "<hash-of-file>"
Last-Modified: <file-mtime>

Elementor Integration

Problem: Elementor caches compiled CSS in _elementor_css and posts with CSS file at:

/wp-content/uploads/elementor/css/post-{POST_ID}.css

Solution: Hook into Elementor's save action and clear our cache:

add_action('elementor/editor/before_save', function($post_id) {
  // Clear Elementor's CSS cache for this post
  delete_post_meta($post_id, '_elementor_css');
  delete_post_meta($post_id, '_elementor_page_assets');
  
  // Also clear the CSS file
  $css_file = WP_CONTENT_DIR . "/uploads/elementor/css/post-{$post_id}.css";
  if (file_exists($css_file)) {
    unlink($css_file);
  }
});

Admin UI

Settings page at: Dashboard → Performance → Cache

Fields:

  • Enable caching
  • Browser cache duration (select: 6m / 1y / custom)
  • Auto-cleanup enabled
  • Cleanup frequency (select: weekly / biweekly / monthly)
  • Cleanup day + time (select day, input time)
  • Delete post revisions older than X days (input: 30)
  • Delete expired transients
  • Delete orphaned postmeta
  • Button: "Clear cache now"

Status display:

  • Last cache clear: [timestamp]
  • Last cleanup run: [timestamp]
  • Cache size on disk: [MB]

Activation Checklist

On plugin activation:

  • Check WordPress version >= 5.0
  • Check PHP >= 7.4
  • Create default settings in wp_options
  • Schedule first cleanup cron job
  • Log activation in error_log

On plugin deactivation:

  • Unschedule cleanup cron job
  • Do NOT delete settings (user might reactivate)
  • Log deactivation

Plugin 2: help4bis-image-optimizer

Scope

  • Lossy compression (JPEG quality adjustment)
  • WebP generation on upload
  • Lazy load injection (native HTML5)
  • Responsive image srcset generation
  • Format fallback (WebP with JPEG fallback)
  • Bulk optimization for existing images
  • Elementor integration (image URL rewriting)

NOT in scope

  • CDN delivery (local only)
  • Advanced ML-based format detection (use simple rules: JPEG→WebP, PNG→WebP if large)
  • AVIF format (too new, poor browser support)
  • Responsive image srcset for post_content HTML (only for featured images + gallery)

File Structure

help4bis-image-optimizer/
├── help4bis-image-optimizer.php
├── admin/
│   ├── class-settings-page.php
│   ├── class-bulk-optimizer.php      # Background processing existing images
│   ├── class-status-dashboard.php    # Stats: images optimized, space saved
│   └── partials/
│       ├── settings.html
│       ├── bulk-optimizer.html
│       └── dashboard.html
├── includes/
│   ├── class-upload-handler.php      # Hook wp_handle_upload
│   ├── class-compressor.php          # ImageMagick wrapper
│   ├── class-webp-generator.php      # WebP conversion
│   ├── class-responsive-images.php   # Srcset generation
│   ├── class-lazy-load.php           # lazy load injection
│   ├── class-elementor-integration.php # Override Elementor image URLs
│   ├── class-image-metadata.php      # Store optimization status in postmeta
│   └── class-fallback-handler.php    # Graceful degradation if ImageMagick fails
├── hooks/
│   ├── class-filters.php             # img tag filters
│   └── class-actions.php             # Upload actions
├── vendor/
│   └── image-processor.php           # ImageMagick abstraction layer
└── tests/
    ├── test-compression.php
    ├── test-webp-generation.php
    ├── test-lazy-load.php
    └── test-elementor-integration.php

Configuration (in wp_options)

'h4b_image_optimizer_settings' => array(
  'enabled' => true,
  'compression_enabled' => true,
  'jpeg_quality' => 78,                // 70-85, balance quality/size
  'png_aggressive' => false,           // Use pngquant if available
  'webp_enabled' => true,
  'webp_quality' => 78,                // Same as JPEG
  'lazy_load_enabled' => true,
  'lazy_load_threshold' => 0,          // 0 = all images, 1000 = skip first 1000px
  'responsive_srcset' => true,         // Generate srcset for featured images
  'backup_originals' => true,          // Keep backup of pre-optimization images
  'bulk_optimize_batch_size' => 5,     // Process 5 images per cron run (don't overload)
  'preserve_exif' => false,            // Strip metadata
  'skip_images_smaller_than' => 50000, // Don't compress tiny images (50KB)
  'imagemagick_available' => true,     // Auto-detected on activation
  'fallback_mode' => 'preserve',       // preserve=upload original, skip=don't upload
);

HTTP Upload Handler

Hook: wp_handle_upload_prefilter / wp_handle_upload

Process:

  1. User uploads image via WordPress media uploader
  2. WordPress saves the file (e.g., /uploads/2026/05/image.jpg)
  3. Our plugin intercepts:
    add_filter('wp_handle_upload', function($upload) {
      if (is_image($upload['file'])) {
        compress_image($upload['file']);
        generate_webp($upload['file']);
        update_image_metadata($upload['file']);
      }
      return $upload;
    });
    
  4. Compression + WebP generation (1-5 seconds per image)
  5. Store metadata: _h4b_optimized = true, _h4b_webp_path = ...

Image Compression Logic

class Compressor {
  public function compress_jpeg($src, $quality = 78) {
    $cmd = sprintf(
      "convert '%s' -quality %d -strip '%s'",
      escapeshellarg($src),
      intval($quality),
      escapeshellarg($src)
    );
    exec($cmd, $output, $ret);
    return $ret === 0;  // 0 = success
  }

  public function compress_png($src, $aggressive = false) {
    if ($aggressive && command_exists('pngquant')) {
      // Reduce palette to 256 colors
      $cmd = sprintf("pngquant 256 --strip '%s' -o '%s'",
        escapeshellarg($src), escapeshellarg($src));
      exec($cmd);
    } else {
      // Just strip metadata
      $cmd = sprintf("convert '%s' -strip '%s'",
        escapeshellarg($src), escapeshellarg($src));
      exec($cmd);
    }
  }
}

WebP Generation

class WebpGenerator {
  public function generate_webp($src) {
    $webp_path = preg_replace('/\.(jpg|jpeg|png)$/i', '.webp', $src);
    
    if (file_exists($src)) {
      // Use cwebp if available (faster), else ImageMagick
      if (command_exists('cwebp')) {
        $cmd = sprintf("cwebp -q 78 '%s' -o '%s'",
          escapeshellarg($src), escapeshellarg($webp_path));
      } else {
        $cmd = sprintf("convert '%s' -define webp:method=6 '%s'",
          escapeshellarg($src), escapeshellarg($webp_path));
      }
      
      exec($cmd, $output, $ret);
      
      // Check output size; if WebP isn't smaller, delete it
      if ($ret === 0 && filesize($webp_path) < filesize($src)) {
        return true;  // WebP created and is smaller
      } else {
        @unlink($webp_path);
        return false; // WebP not beneficial, stick with original
      }
    }
  }
}

Lazy Load Injection

HTML before:

<img src="/uploads/2026/05/image.jpg" alt="..." class="wp-image-123" />

HTML after (our plugin):

<img 
  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"
  data-src="/uploads/2026/05/image.jpg"
  alt="..."
  class="wp-image-123"
  loading="lazy"
  width="800"
  height="600"
/>

Implementation:

add_filter('the_content', function($content) {
  return preg_replace_callback(
    '/<img\s+([^>]*?)src="([^"]*?)"([^>]*)>/i',
    function($matches) {
      $attrs = $matches[1] . $matches[3];
      $src = $matches[2];
      
      // Don't lazy-load if already has loading="lazy"
      if (strpos($attrs, 'loading=') !== false) {
        return $matches[0];
      }
      
      // Create placeholder SVG (1x1 transparent)
      $placeholder = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E";
      
      return sprintf(
        '<img %s src="%s" data-src="%s" loading="lazy" />',
        $attrs,
        $placeholder,
        esc_url($src)
      );
    },
    $content
  );
});

Browser native support: loading="lazy" is supported in all modern browsers (2024+). Old browsers ignore the attribute and load normally.

Responsive Images (Srcset)

For featured images, generate multiple sizes:

// On upload, if it's a featured image, create these sizes:
// - thumbnail: 150×150
// - medium: 300×300
// - medium_large: 768×768
// - large: 1024×1024
// - full: original size

// Then inject into featured image markup:
$sizes_srcset = array();
foreach ([150, 300, 768, 1024, 0] as $size) {
  $src = wp_get_attachment_image_src($attach_id, $size);
  if ($src) {
    $sizes_srcset[] = sprintf("%s %dw", $src[0], $src[1]);
  }
}

// Result:
<img 
  src="..." 
  srcset="image-150.jpg 150w, image-300.jpg 300w, image-768.jpg 768w, ..."
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt="..."
/>

Elementor Integration

Problem: Elementor stores image URLs in _elementor_data JSON. When we show optimized images, Elementor's cache might be stale.

Solution: On image optimization, invalidate Elementor's post assets:

add_action('h4b_image_optimized', function($attachment_id) {
  // Find all Elementor posts that reference this image
  $posts = get_posts(array(
    'meta_query' => array(
      array(
        'key' => '_elementor_data',
        'compare' => 'LIKE',
        'value' => $attachment_id,
      ),
    ),
  ));
  
  foreach ($posts as $post) {
    // Clear Elementor's CSS and asset cache for this post
    delete_post_meta($post->ID, '_elementor_page_assets');
    delete_post_meta($post->ID, '_elementor_css');
  }
});

Also, hook into Elementor's image rendering:

add_filter('elementor/rendering/render_element/final_html', function($html) {
  // Inject lazy load into all img tags within Elementor rendered output
  return preg_replace_callback('/<img\s+([^>]*)>/i', function($matches) {
    // Add lazy load if not present
    if (strpos($matches[1], 'loading=') === false) {
      return '<img ' . $matches[1] . ' loading="lazy" />';
    }
    return $matches[0];
  }, $html);
});

Bulk Optimizer for Existing Images

Process:

  1. User clicks "Optimize all existing images" in admin
  2. Plugin creates background cron task
  3. Each cron run processes 5 images (configurable batch size)
  4. Progress bar in admin: "Optimized 245 of 2,150 images (11%)"
  5. Takes 5-10 minutes for 2000 images (spread across multiple cron runs)

Implementation:

// On bulk optimizer start:
update_option('h4b_bulk_optimize_status', array(
  'total' => $total_attachments,
  'processed' => 0,
  'started' => current_time('mysql'),
  'batch_size' => 5,
));

// Cron job every 2 minutes:
wp_schedule_event(current_time('timestamp'), '2min', 'h4b_bulk_optimize_cron');

add_action('h4b_bulk_optimize_cron', function() {
  $status = get_option('h4b_bulk_optimize_status');
  if (!$status || $status['processed'] >= $status['total']) {
    wp_clear_scheduled_hook('h4b_bulk_optimize_cron');
    return;
  }
  
  // Get next batch
  $batch = new WP_Query(array(
    'post_type' => 'attachment',
    'post_mime_type' => 'image',
    'posts_per_page' => $status['batch_size'],
    'offset' => $status['processed'],
  ));
  
  foreach ($batch->posts as $attachment) {
    compress_image($attachment->ID);
    generate_webp($attachment->ID);
  }
  
  $status['processed'] += count($batch->posts);
  update_option('h4b_bulk_optimize_status', $status);
});

Fallback & Error Handling

If ImageMagick is not available:

On activation, check:

register_activation_hook(__FILE__, function() {
  $has_convert = shell_exec('which convert 2>/dev/null');
  $has_magick = shell_exec('which magick 2>/dev/null');
  $has_cwebp = shell_exec('which cwebp 2>/dev/null');
  
  if (!$has_convert && !$has_magick) {
    // ImageMagick not installed
    update_option('h4b_imagemagick_available', false);
    add_action('admin_notices', function() {
      echo '<div class="notice notice-error">';
      echo '<p><strong>Image Optimizer:</strong> ImageMagick not found. Install via: apt install imagemagick</p>';
      echo '</div>';
    });
  }
});

Graceful fallback: If compression fails, upload the original image uncompressed. Log the error, but don't break the upload.


Testing Strategy

Plugin 1: Cache

  • HTTP headers are set correctly on requests for .css, .js, .jpg
  • Elementor CSS cache is cleared when post is saved
  • Database cleanup runs on schedule and removes revisions
  • Orphaned postmeta is cleaned up
  • Cache clear button works

Plugin 2: Image Optimizer

  • JPEG images are compressed to target quality
  • WebP is generated and is smaller than JPEG
  • Lazy load tag is injected
  • Elementor image URLs are rewritten
  • Bulk optimizer progresses correctly
  • If ImageMagick unavailable, original image is uploaded (fallback)
  • Featured image srcset is generated correctly

Integration Testing

  • Disable Hummingbird, enable cache plugin → no speed regression
  • Disable Smush, enable image optimizer → speed improvement or equal
  • All Elementor pages still display correctly
  • All image galleries still work

Deployment Checklist

  • Code reviewed
  • All tests passing
  • Ruff linter clean
  • Backup of production database taken
  • Staging environment mirrors production
  • ImageMagick + cwebp verified installed
  • Hummingbird deactivated, cache plugin activated (1 test site)
  • Monitor for 72 hours
  • Smush deactivated, image optimizer activated (1 test site)
  • Monitor for 72 hours
  • Roll out to remaining sites
  • WPMU Dev subscription cancelled