Fix image sizing at the source in Zen Cart admin
Most storage bloat in a Zen Cart 2.x catalog comes from oversized source images plus GD-generated thumbnails that never expire. Start in Admin > Configuration > Images.
Set the small, medium, and large image dimensions to values that match your template's actual display size. If your product listing shows thumbnails at 200px, set SMALL_IMAGE_WIDTH to 200 and leave the height blank so Zen Cart scales proportionally. Oversized thumbnails waste both disk and bandwidth.
Set Image Resizing Enabled to Force Sizes only if your source images are already trimmed. For automatic proportional scaling, use Continue - keep aspect ratio. Confirm Image Quality sits at 80–85. JPEG quality above 85 rarely changes visible output but inflates file size.
Zen Cart writes generated thumbnails to images/<category>/ alongside originals. Confirm PHP has GD with the correct functions:
php -i | grep -i -E 'gd|webp|jpeg|png'On CloudLinux, run this through the site's selected PHP version so the output matches the interpreter LiteSpeed actually serves. Use the alt-php binary or the version selector in DirectAdmin's PHP Version Manager or cPanel's MultiPHP Manager rather than the system default.
Generate WebP copies with a batch job
Zen Cart 2.x core does not write WebP on upload. Generate WebP versions of existing catalog images with a one-off script that walks the images/ tree. GD in PHP 8.2–8.4 supports imagewebp().
<?php
// webp-convert.php - run from the store's public root
$root = __DIR__ . '/images';
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $file) {
$path = $file->getPathname();
$ext = strtolower($file->getExtension());
if (!in_array($ext, ['jpg','jpeg','png'])) continue;
$webp = preg_replace('/\.(jpe?g|png)$/i', '.webp', $path);
if (file_exists($webp)) continue;
$img = $ext === 'png' ? imagecreatefrompng($path) : imagecreatefromjpeg($path);
if (!$img) continue;
if ($ext === 'png') { imagepalettetotruecolor($img); imagealphablending($img, true); imagesavealpha($img, true); }
imagewebp($img, $webp, 82);
imagedestroy($img);
echo "converted: $webp\n";
}
Run it once with the site's PHP binary, then delete the script:
cd ~/domains/example.com/public_html
/opt/alt/php83/usr/bin/php webp-convert.php
rm webp-convert.phpAdjust the binary path to the PHP version assigned to the domain. This keeps a .webp sibling next to every JPEG/PNG, which the rewrite rules below serve automatically.
Serve WebP automatically through LiteSpeed
Rather than editing every template call, let LiteSpeed swap in the WebP file when the browser advertises support. Add this to the store's .htaccess in the public root. LiteSpeed reads Apache-style rewrite directives.
<IfModule LiteSpeed>
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} \.(jpe?g|png)$
RewriteCond %{REQUEST_FILENAME}\.webp -f
RewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.webp [T=image/webp,E=accept:1,L]
</IfModule>
Header append Vary Accept env=REDIRECT_accept
Note the naming: this rule targets photo.jpg.webp. If you kept the photo.webp naming from the script above, change the RewriteCond and RewriteRule to strip the original extension instead:
RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
RewriteRule ^(.+)\.(jpe?g|png)$ $1.webp [T=image/webp,L]Pick one convention and keep the script and rewrite rules aligned. The Vary: Accept header stops shared caches and the LiteSpeed cache from serving a WebP body to a client that cannot decode it.
Verify the swap with curl, forcing the Accept header:
curl -sI -H 'Accept: image/webp' https://example.com/images/widget.jpg | grep -i -E 'content-type|vary'A correct response returns Content-Type: image/webp and Vary: Accept.
Offload the images directory to S3 or a CDN
Once the catalog grows past a few gigabytes of media, keep the account's disk quota for the database, application code, and order data instead. Two approaches work with Zen Cart 2.x without core edits.
Pull-through CDN (simplest)
Point a CDN pull zone (CloudFront, Bunny, Cloudflare) at your origin, then rewrite image URLs to the CDN hostname. Zen Cart builds image tags from DIR_WS_IMAGES, so define the CDN host in the template's HTML head handling or, more cleanly, in includes/extra_configures/:
<?php
// includes/extra_configures/cdn.php
define('HTTP_SERVER', 'https://cdn.example.com');
Only override the asset host, not the storefront host used for checkout and sessions. Mixing hosts on cart or account pages breaks cookies. Scope the CDN to /images/ requests and let the pull zone cache them at the edge. Local disk still holds the originals, but repeat delivery and bandwidth move off the server.
True offload to S3
To remove media from local disk entirely, sync images/ to a bucket and serve from there. Push existing files and set long cache headers:
aws s3 sync ./images s3://example-store-media/images \
--exclude '*.php' \
--cache-control 'public, max-age=31536000, immutable' \
--content-type-detectionServe the bucket behind a CDN and set HTTP_SERVER to that hostname as above. Keep the index.php guard files that Zen Cart places in image folders out of the bucket — they are only there to block local directory listing.
The operational trade-off: uploads through Zen Cart admin still land on local disk, so add a post-upload sync. A cron job under the account keeps the bucket current without touching Zen Cart code:
# crontab -e (run as the site's system user)
*/15 * * * * /usr/local/bin/aws s3 sync /home/user/domains/example.com/public_html/images s3://example-store-media/images --size-only --cache-control 'public, max-age=31536000, immutable' >/dev/null 2>&1Use --size-only so the sync does not re-upload every file on each run. Store AWS credentials in the system user's ~/.aws/credentials with an IAM key scoped to PutObject and ListBucket on that one bucket, never a root account key.
Set cache headers and confirm the result
Local images benefit from the same long-lived caching you set on the bucket. Add expiry rules to .htaccess so LiteSpeed sends far-future headers for static media:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
</IfModule>Slow media delivery compounds into lost conversions, one of the reasons your online store may be losing sales because of hosting. After deploying, re-run the curl check and confirm both the WebP swap and the cache headers, then clear the LiteSpeed cache for the domain in the panel so stale image responses are purged.