Add: detailed technical specification for both plugins
This commit is contained in:
541
02-TECHNICAL-SPECIFICATION.md
Normal file
541
02-TECHNICAL-SPECIFICATION.md
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
# 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)
|
||||||
|
```php
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
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)
|
||||||
|
- [x] Auto-cleanup enabled
|
||||||
|
- Cleanup frequency (select: weekly / biweekly / monthly)
|
||||||
|
- Cleanup day + time (select day, input time)
|
||||||
|
- [x] Delete post revisions older than X days (input: 30)
|
||||||
|
- [x] Delete expired transients
|
||||||
|
- [x] 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)
|
||||||
|
```php
|
||||||
|
'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:
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:**
|
||||||
|
```html
|
||||||
|
<img src="/uploads/2026/05/image.jpg" alt="..." class="wp-image-123" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML after (our plugin):**
|
||||||
|
```html
|
||||||
|
<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:**
|
||||||
|
```php
|
||||||
|
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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:**
|
||||||
|
```php
|
||||||
|
// 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:
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
Reference in New Issue
Block a user