← Back to blog
13 June 2026

Serve WebP Images on cPanel With One .htaccess Block

Serving WebP on cPanel shared hosting takes one .htaccess block and a batch conversion script. No server admin access, no CDN, no plugin required.

Why Your Images Are Your Biggest Page Weight Problem

We have audited dozens of South African small business websites over the past few years, and the story is almost always the same. The owner paid someone R2,000 to build a WordPress site on shared cPanel hosting, the site looks fine on the desktop in the office, and nobody has ever opened the Network tab in DevTools. Then we run a PageSpeed test and the first thing we see is a page that downloads 4MB of images to show six product photos.

That 4MB matters more here than it does in Germany or the US. A 2025 DataReportal report on South Africa puts the country at 50.8 million internet users, and the overwhelming majority of them are browsing on mobile, on Vodacom or MTN prepaid data. Prepaid bundles are not cheap: from our own buying, a 500MB bundle runs roughly R50 to R80 depending on the operator and promo. Every megabyte you shave off your page directly reduces what your customers pay to visit it. That is not a theoretical concern. It is a real reason people bounce.

The biggest lever you have on page weight is almost always images. Not JavaScript, not CSS, not fonts. Images. A homepage hero that was exported from Photoshop at "Save for Web" quality still lands at 300 to 600KB if it is a JPEG. Multiply that by five or six product images and you are already at 2MB before the browser has loaded a single line of JavaScript.

The fix that delivers the most improvement for the least ongoing work is switching to WebP. According to Google's WebP documentation, WebP lossy images are 25 to 34% smaller than comparable JPEG images at equivalent visual quality. On a page with 2MB of images, that is 500KB to 680KB gone without touching your layout or your design. For a Vodacom customer on a 1GB bundle, that is a meaningful saving every time they load your page.

The catch is that you cannot just swap out every JPEG wholesale. Some older browsers do not support WebP, and some image workflows make bulk conversion annoying. We will cover both problems below. But the good news is that if you are on cPanel shared hosting (the default for most R100 to R300 per month South African hosting accounts), you can serve WebP automatically to browsers that support it and fall back to JPEG for everything else, using a single .htaccess block. No server admin access required. No plugin. No CDN subscription.

How Browsers Negotiate Image Formats (the Accept Header)

Before we touch .htaccess, you need to understand the mechanism we are exploiting. When a browser requests an image, it sends an HTTP request header called Accept that lists the formats it can handle. A modern Chrome or Firefox request for any image will include something like this:

Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8

That is the browser saying: "I would prefer AVIF, then WebP, then APNG. I will accept any image format, but those are my preferences in order." The MDN documentation on HTTP content negotiation explains this pattern in full. The server can read this header and respond with a different file than the one literally requested.

So when a modern browser asks for /images/product-photo.jpg, the server can check whether the Accept header contains image/webp, and if it does, serve /images/product-photo.webp instead. The browser receives the WebP file, renders it correctly, and the HTML never has to change. Your <img src="product-photo.jpg"> tags stay exactly as they are.

This is content negotiation, and Apache's mod_rewrite module gives us the tools to do it purely in .htaccess. Most cPanel shared hosting plans have mod_rewrite enabled by default because WordPress requires it. The infrastructure is already there.

The key point is that this approach is transparent to your HTML. You do not need to change any image tags, you do not need to use the <picture> element, and you do not need a JavaScript library for format detection. The rewrite happens at the HTTP layer before the browser sees anything.

Convert Your Existing Images to WebP

The rewrite rules only work if you have .webp versions of your images sitting alongside the originals. If Apache rewrites a request for photo.jpg to photo.jpg.webp but that file does not exist, the browser gets a 404. Conversion comes first.

The standard command-line tool is cwebp, part of Google's libwebp package. On Ubuntu or Debian:

sudo apt-get install webp

On macOS with Homebrew:

brew install webp

To convert a single file:

cwebp -q 82 input.jpg -o output.jpg.webp

The -q 82 flag sets quality to 82 out of 100. At 82, most photos are visually indistinguishable from the original JPEG at quality 85, and the file size difference is substantial. You can push to 75 if you are optimising hard, or 90 if you have brand photography where you cannot afford any perceptible degradation. Note the output filename is output.jpg.webp, not output.webp. The rewrite rule we use below appends .webp to the full original filename, so the naming must match.

For a whole folder of images, use this batch script. Save it as convert-to-webp.sh, make it executable with chmod +x convert-to-webp.sh, and run it from the directory containing your images:

#!/bin/bash
# Batch convert all JPEGs and PNGs to WebP in the current directory.
# Originals are kept. WebP files are written alongside them.
# Usage: ./convert-to-webp.sh [quality]
# Default quality: 82

QUALITY=${1:-82}

for img in *.jpg *.jpeg *.png; do
    [ -f "$img" ] || continue
    output="${img}.webp"
    if [ -f "$output" ]; then
        echo "Skipping $img ($output already exists)"
    else
        cwebp -q "$QUALITY" "$img" -o "$output"
        echo "Converted: $img -> $output"
    fi
done

echo "Done."

Run it as ./convert-to-webp.sh for quality 82, or ./convert-to-webp.sh 75 for more aggressive compression. It skips files that already have a .webp counterpart, so you can re-run it safely as you add new images.

If you prefer a browser-based tool, Squoosh (built by Google) runs entirely in your browser, costs nothing, and lets you compare the original against WebP side by side with file size shown for each. For small batches or one-off files it works well. For a site with 80 product photos, the shell script above is the practical choice.

One thing to keep in mind: you need to keep originals around. If you only store WebP and a browser that cannot handle it requests the file, you have nothing to serve as fallback. Keep your JPEGs and PNGs.

The .htaccess Rewrite Block Explained Line by Line

This is the core deliverable. Paste this block into your site's .htaccess file. If WordPress is already using .htaccess, add this block above the # BEGIN WordPress marker.

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Serve WebP if: browser accepts it AND a .webp file exists on disk
    RewriteCond %{HTTP_ACCEPT} image/webp
    RewriteCond %{REQUEST_FILENAME}.webp -f
    RewriteRule \.(?:jpe?g|png)$ %{REQUEST_URI}.webp [T=image/webp,E=accept:1,L]
</IfModule>

# Tell caches this response varies by Accept header (only when we rewrote)
<IfModule mod_headers.c>
    Header append Vary Accept env=REDIRECT_accept
</IfModule>

Here is what each line does.

<IfModule mod_rewrite.c>: Wraps the block in a conditional. If mod_rewrite is not available (rare on cPanel, but possible), Apache skips the block entirely rather than throwing a 500 error.

RewriteEngine On: Activates the rewrite engine for this directory. Required before any RewriteRule will fire.

RewriteCond %{HTTP_ACCEPT} image/webp: The browser check. This condition only passes if the incoming Accept header contains the string image/webp. Browsers that do not support WebP never send this string, so the rule never fires for them. This is the entire fallback mechanism.

RewriteCond %{REQUEST_FILENAME}.webp -f: Before rewriting anything, this checks that a .webp version actually exists on disk. In a per-directory .htaccess context, %{REQUEST_FILENAME} is already the full filesystem path to the requested file, so you append .webp directly. Do not prefix it with %{DOCUMENT_ROOT}: that doubles the path and the file check silently fails, which is the single most common reason these rules "do nothing". The -f flag means "is a regular file". If you request photo.jpg and photo.jpg.webp does not exist, this condition fails and Apache serves the original JPEG. This is your safety net for images you have not converted yet.

RewriteRule \.(?:jpe?g|png)$ %{REQUEST_URI}.webp [T=image/webp,E=accept:1,L]: The actual rewrite. When both conditions above pass, a request for product.jpg is silently served as product.jpg.webp. The T=image/webp flag sets the correct Content-Type header. The E=accept:1 flag sets an environment variable that the Vary header below depends on (without it, the header never fires). The L flag means stop processing rules here.

Header append Vary Accept env=REDIRECT_accept: Do not skip this line. It tells downstream caches (Cloudflare, browser cache, CDN edge nodes) that the response for this URL varies based on the Accept header. Without it, a cache can store the WebP response and then serve it to an older browser that cannot render WebP. The env=REDIRECT_accept condition makes the header fire only when the rewrite actually happened. Apache renames the accept variable we set with E=accept:1 to REDIRECT_accept after the internal rewrite, which is why the names differ. If you drop the E=accept:1 flag above, this header silently never appears, so keep them together.

The Header directive requires mod_headers, so we wrap it in its own <IfModule mod_headers.c> guard. That module is enabled by default on virtually all cPanel hosts, but the guard means the block degrades quietly instead of throwing a 500 if it is missing.

Fallback Strategy: What Older Browsers Get

The fallback is already baked into the logic above. The RewriteCond on %{HTTP_ACCEPT} means the rule only fires for browsers that explicitly declare WebP support. Browsers that do not send image/webp in their Accept header never trigger the rewrite and always receive the original JPEG or PNG.

WebP is supported by around 96% of global browsers as of 2025, including all modern versions of Chrome, Firefox, Edge, and Safari (macOS 11+ and iOS 14+). The browsers that fall through to JPEG are genuinely old: Internet Explorer, very old Safari on iOS 13 and below, some legacy Android WebViews.

Where this approach does not protect you is images served from a CDN or external storage. This .htaccess block only affects files served from your own Apache server. If you push your media to S3, Cloudflare R2, or a WordPress CDN offload plugin, you need a different mechanism. For most cPanel shared hosting sites in South Africa, this is not a concern. Your images live on the same server as your PHP files.

What we give up with this approach: you have to maintain two copies of every image. Add a new product photo and you need to convert it and upload the .webp version too. If you forget, that image will never be served as WebP. A WordPress plugin like Imagify or Smush can automate this on upload, but they add cost. If you are managing images manually, add "convert and upload WebP" to your upload checklist and treat it as a required step.

Testing It Works

Once the .htaccess block is in place and you have WebP files alongside your originals, test before you trust it.

Test with curl from terminal. Replace the URL with a real image on your site:

curl -sI \
  -H "Accept: image/webp,image/*,*/*;q=0.8" \
  https://yourdomain.co.za/images/photo.jpg

In the response headers, look for two things. First: content-type: image/webp. If you see this, Apache rewrote the request and is serving the WebP file. Second: vary: accept. If you see this, the cache header is working correctly.

Now test without the WebP accept header to confirm the fallback:

curl -sI \
  -H "Accept: image/jpeg,image/*;q=0.8" \
  https://yourdomain.co.za/images/photo.jpg

This should return content-type: image/jpeg. If it returns WebP regardless of the header, your RewriteCond on %{HTTP_ACCEPT} is not working. Double-check you copied the block exactly, particularly the string image/webp in the condition (not image\/webp with an escaped slash, which some older tutorials incorrectly show).

Test with Chrome DevTools. Open Chrome, navigate to a page with images on your site, open DevTools (F12), go to the Network tab, and filter by "Img". Click any image request and look at Response Headers. You should see content-type: image/webp even though the URL in the request shows .jpg. That is the rewrite working.

PageSpeed before and after. Run a PageSpeed Insights test on your homepage before enabling WebP, note the "Serve images in next-gen formats" opportunity showing estimated KB savings. Enable WebP, wait a few minutes for any server-side cache to clear, and run again. On a typical South African small business homepage with 8 to 12 unoptimised images, we have seen the image-related opportunity drop from 800KB to under 100KB after this change. Total mobile PageSpeed scores commonly move from the 40s or 50s into the 70s on image-heavy pages where this was the main bottleneck.

A note on LiteSpeed. Plenty of cPanel hosts in South Africa run LiteSpeed rather than Apache now, not the other way around, so do not assume Apache. The good news is LiteSpeed reads .htaccess and implements mod_rewrite and mod_headers compatibility, so this exact block works unchanged in nearly all cases. The one thing to verify on LiteSpeed is the Vary: Accept header: run the curl test above and confirm vary: accept appears. If it does not, your mod_headers compatibility or E=accept:1 handling differs, and that is the line to investigate. To check your server type in cPanel, open "Server Information" and look at the web server field.

What This Does Not Fix

This block has limits. It serves WebP when the file exists and the browser supports it. It does not:

WebP conversion is one tool, not a complete image performance solution. It is the highest-return tool available on a basic shared hosting plan with no additional cost and about 30 minutes of setup work. For most South African small business sites on budget cPanel hosting, image format is the single biggest quick win available.

If You Want Help With This

We do performance audits and image pipeline work for South African small businesses as part of the technical services at nel404labs. If your PageSpeed score is sitting below 60, images are almost certainly a significant part of the problem, and the WebP approach above is usually the first thing we fix. If you would rather have someone audit your site, run the diagnostics, and implement the full image optimisation pipeline rather than doing it yourself, get in touch through the contact page. We work with clients on cPanel shared hosting at the R100 to R300 price point, and we know what is realistically achievable without a server upgrade or a budget CDN subscription.

Need something custom built? We work with founders and agencies who know what they want and need someone who can actually deliver it.

Let's chat