17 KiB
Technical Specification: Custom Caching & Image Optimization Plugins
Document Purpose
This spec defines the exact architecture, API, and integration points for the two replacement plugins:
help4bis-performance-cache— HTTP caching + database cleanuphelp4bis-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:
- User uploads image via WordPress media uploader
- WordPress saves the file (e.g.,
/uploads/2026/05/image.jpg) - 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; }); - Compression + WebP generation (1-5 seconds per image)
- 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:
- User clicks "Optimize all existing images" in admin
- Plugin creates background cron task
- Each cron run processes 5 images (configurable batch size)
- Progress bar in admin: "Optimized 245 of 2,150 images (11%)"
- 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