KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Serving WebP Without a WordPress Plugin: nginx + CLI

2026/05/20

Step 4: CDN subdomain (if used)

If you serve static assets from a separate CDN subdomain with its own nginx server block, apply the same split there. The CDN block has its own image location stanza — it needs try_files $uri$webp_suffix $uri just like the main domain. Otherwise images served through the CDN domain won't get WebP negotiation, and half your traffic misses the optimization.


Step 5: Future uploads

This setup handles everything already on disk. For new uploads, the conversion needs to happen at upload time so the .webp sibling exists when nginx looks for it. WordPress core does not do this automatically — you'll need one of the following:

Modern Image Formats plugin (free, by the WordPress Performance Team): This is the canonical plugin that was originally proposed for WordPress 6.1 core before being moved to a plugin. It generates .webp and .avif versions of every JPEG and PNG you upload, using the same sub-size naming convention as standard thumbnails (image.jpg → image-150x150.jpg alongside image-150x150.webp). The nginx try_files pattern handles these identically. Install from the plugin directory — no configuration needed beyond activation.

Avada theme users: Avada → Options → Performance → Image Optimization. Enable "Convert new images to WebP." Optionally disable "Keep original" to avoid storing both versions — though keeping the original is safer if you ever need to regenerate thumbnails or switch themes.

Other themes and page builders: Many premium themes include built-in WebP conversion in their performance or image settings. Check your theme's documentation before reaching for a third-party plugin. The nginx setup described here works regardless of how the .webp files were created — all it cares about is that a sibling file with a .webp extension exists on disk.

Manual approach (no plugin): If you prefer to avoid another plugin, run the conversion script from Step 2 on a cron schedule — weekly or monthly — to catch new uploads. This adds a small recurring CPU cost but keeps your plugin count at zero. The script is idempotent; it skips images that already have a .webp sibling, so each run only processes what's new.


Step 6: Orphaned WebP cleanup (optional)

When you delete a JPG from the media library, WordPress removes the original and its thumbnails — but it doesn't know about the .webp siblings you created in Step 2. They sit on disk, orphaned and harmless. If that bothers you, run this periodically:

find /home/user/www -name "*.webp" | while read webp; do
    original="${webp%.webp}"
    if [ ! -f "$original" ]; then
        echo "Orphan: $webp"
        # rm "$webp"  # Uncomment when ready
    fi
done

Orphaned WebP files are small — typically 10–100 KB each — and cause no performance impact. This is hygiene, not urgency. Run it monthly or whenever you remember.


What this doesn't handle

Non-browser clients — RSS readers, some crawlers, email clients — may not advertise WebP support in their Accept header, or may omit the header entirely. nginx falls back to the original JPEG when the Accept header doesn't contain image/webp, so nothing breaks — those clients simply receive the larger file.

Cloudflare's cache partitioning: The Vary: Accept header is critical here. Without it, Cloudflare may cache a single variant of each image and serve it to all visitors regardless of their Accept header. With it, Cloudflare maintains separate cache entries for WebP-capable and non-WebP requests. If you're seeing inconsistent results after deploying — some visitors getting WebP when they shouldn't, or vice versa — verify that Vary: Accept is present in the response headers of your image requests.


Image payload
Full JPG/PNG size → 40–75% smaller for browsers that support WebP
WordPress database
Untouched — URLs still reference .jpg. No search-replace needed, ever.
Plugins
Zero added. No updater, no license key, no compatibility headaches with the next WP release.
Server CPU
One-time batch during initial conversion, then zero ongoing cost.
Browser fallback
Automatic — non-WebP browsers receive the original JPEG with no idea anything was different.
Cloudflare caching
Two cache variants keyed on Accept via Vary. No cache poisoning between WebP and non-WebP visitors.

No plugin dependency. No PHP configuration changes. No database search-replace. WordPress stores and references .jpg exactly as it always has. nginx does the content negotiation at the edge. Browsers that don't support WebP — older Safari, RSS readers, some crawlers — receive the original JPEG with no idea anything was different.

Technical Audit Summary

This guide documents a production WebP deployment using nginx content negotiation and CLI-based conversion — no WordPress plugin involvement.

Last Audit: May 2026
Environment: Debian Trixie (13)
Nginx: 1.30.1
PHP-FPM: 8.5.6

Compatibility: The nginx map $http_accept pattern and try_files fallback chain work on any nginx version that supports the map directive (1.0+). The cwebp encoder is available in all major package managers. For sites behind Cloudflare, the Vary: Accept header ensures correct cache partitioning — without it, a non-WebP browser can receive a cached WebP response.

  • ← Previous
  • 1
  • 2
  • Next →
©2026 KBeezie | Disclaimer | Privacy Notice