WebP and PNG on the Same Deploy: The P42 Rule
P42: every ABS featured image deploys as both PNG and WebP in the same cycle. CF serves WebP via Accept-header; the PNG is canonical. The script, the 4h cache quirk, and the verify pattern.

What P42 is
P42: every image deployed to /srv/abs-site/public/images/<slug>-<date>.png is also deployed as <slug>-<date>.webp in the same deploy cycle.
The HTML serves <picture> with both formats; CF’s edge serves WebP via Accept-header negotiation. Average savings today: “85-87%” of the original PNG size.
The rule is binary: deploy the PNG without the WebP, and the page regresses. Deploy the WebP without the PNG, and a future cf-cache-status: HIT would break fallback paths. Pair them.
The script that does it
After the openai plugin generates a PNG:
from PIL import Image
import shutil
src = "/tmp/raw/cover.png" # from r['image']
dst_png = "/srv/abs-site/public/images/<slug>-<date>.png"
dst_webp = "/srv/abs-site/public/images/<slug>-<date>.webp"
shutil.copy(src, dst_png)
Image.open(dst_png).save(dst_webp, 'WEBP', quality=82, method=4)
Three params for the WebP conversion:
quality=82— visually lossless for editorial illustrations while halving size.method=4— slower encoding but ~“10%” smaller. Bump to 6 for slightly smaller files at “2×” the CPU cost; we don’t.Image.open(png)re-reads the just-saved file rather than the in-memory PIL source; re-encoding forces a consistent output regardless of the openai plugin’s output format.
The CF cache “4h” warning
CF caches /scripts/* for “4 hours” by default. ABS uses no JS asset at /scripts/*, but the WebP file at /images/<slug>.webp is on /images/* which has a different TTL — that’s fine, but if you ever switch an asset URL with the same path, cache busting is mandatory:
<!-- Old deployed CSS -->
<link rel="stylesheet" href="/styles/abc.css?v=1" />
<!-- New version of the same file -->
<link rel="stylesheet" href="/styles/abc.css?v=2" /> <!-- version bump on cache-bust -->
For images, the path itself includes a date suffix (<slug>-2026-7-14.png), so cache-bust isn’t typically needed — each batch’s images are a new path.
The 85% size savings
For today’s batch (“3 guides × 1 cover each”), measured sizes:
| File | PNG | WebP | Saved |
|---|---|---|---|
| batch8-cover | 3,795,477 | 529,776 | ”86.0%“ |
| batch7-cover | 3,899,334 | 561,736 | ”85.6%“ |
| batch6-cover | 4,005,566 | 551,018 | ”86.2%” |
The percentage is consistent across complex illustrations: simpler covers save slightly more, but the floor is ~“80%” and the ceiling is ~“90%”.
The HTML serving pattern
<picture> with type-based source selectors:
<picture>
<source srcset="/images/<slug>-<date>.webp" type="image/webp" />
<source srcset="/images/<slug>-<date>.png" type="image/png" />
<img src="/images/<slug>-<date>.png" alt="<ALT>" loading="lazy" />
</picture>
CF inspects the Accept: request header. If the browser sends image/webp, the .webp source is selected; otherwise, the .png fallback serves. The visible <img> tag is the ultimate fallback for non-supporting environments.
What P42 means for the deploy chain
Three places in the deploy chain that must include P42:
- Image-gen script (
/tmp/gen_batch*.py). Both PNG and WebP land in/srv/abs-site/public/images/. - rsync step.
/srv/abs-site/dist/includes references to both; the rsync is to webroot, not the public-images folder, so no special handling is needed (the references are in the static HTML). - CF purge. Both the PNG and the WebP are purged together (the
image_gen*.shscript in/opt/data/scripts/handles this).
A PNG-without-WebP regression is caught by the regression suite’s image_dimensions.py test; the test verifies the companion WebP exists.
When P42 fails — three failure shapes
P42 silently breaks in three ways:
- The WebP file was never written. The PNG is on disk; the WebP is missing. The page renders with the PNG fallback. To detect: the regression suite’s
image_dimensions.pytest orlsfor the pair. - The WebP is corrupt (truncated write). The file is small (a few KB) and the browser fails to decode. CF will serve a 502 to subsequent requests. To detect:
file <slug>.webpshould reportRIFF WEBP. - The
<picture>tag is missing the WebP source. The HTML only references the PNG. Even though both files exist, only the PNG is served. To detect:grep -c "image/webp" dist/guides/<slug>/index.htmlshould return 1 or more.
Each of these is recoverable:
- Missing WebP: rerun the gen script in WebP-only mode against the existing PNG.
- Corrupt WebP: delete the WebP, rerun the conversion.
- Missing
<picture>: rebuild vianpm run buildafter the gen script produced the file.
The agent doesn’t ship a batch with any of these three states.
The verify
A one-liner that confirms both files are present and the sizes are sane:
ls -la /srv/abs-site/public/images/<slug>-<date>.{png,webp}
Expected:
- PNG > “1 MB”
- WebP < “700 KB”
- Both present, both the same base filename.
If one is missing, the script didn’t run cleanly. If the WebP is larger than the PNG, the WebP conversion failed silently (and the PNG fallback will serve).
What not to do
- Don’t deploy the PNG-only and “come back later” for the WebP. Pair them at gen time.
- Don’t use
quality=50to save bytes — the editorial illustrations show compression artifacts in the shading. - Don’t skip the WebP because CF can re-encode PNGs at the edge. CF’s auto-WebP doesn’t kick in for the headers ABS sends; the agent-side conversion is the canonical path.
Verification checklist
| # | Check | What it proves |
|---|---|---|
| 1 | PNG file exists | Image generated |
| 2 | WebP file exists | P42 met |
| 3 | WebP size < “700 KB” | Encoding quality sane |
| 4 | HTML references both files | <picture> set up |
| 5 | Live URL returns HTTP 200 for both | CDN serving correctly |
Sources
- Cloudflare cache docs
- ABS companion: Image Gen: OpenAI Backend (Real Key, Not Codex OAuth)
- ABS companion: Static Site Deploy: rsync, CF Purge, CDN Cache
Last verified: 2026-07-14. P42 has held across batches “1-8” of today’s deploy.



Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.