guide · computers

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.

July 14, 2026 · By Alastair Fraser

A friendly retro robot standing at a small print shop counter: on the robot's left, a chunky COLOR PRINTER outputs a large, brightly colored PNG poster in red-orange; on the robot's right, a smaller COMPACT PRINTER outputs a dim, modern WebP twin. Both prints hang drying side-by-side from a thin clothesline. A handwritten label on the counter reads P42: PNG+WEBP per image. Pastel blue and mint background, soft studio lighting, mid-century robot, no extra text.

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:

FilePNGWebPSaved
batch8-cover3,795,477529,776”86.0%“
batch7-cover3,899,334561,736”85.6%“
batch6-cover4,005,566551,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:

  1. Image-gen script (/tmp/gen_batch*.py). Both PNG and WebP land in /srv/abs-site/public/images/.
  2. 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).
  3. CF purge. Both the PNG and the WebP are purged together (the image_gen*.sh script 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:

  1. 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.py test or ls for the pair.
  2. 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>.webp should report RIFF WEBP.
  3. 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.html should 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 via npm run build after 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=50 to 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

#CheckWhat it proves
1PNG file existsImage generated
2WebP file existsP42 met
3WebP size < “700 KB”Encoding quality sane
4HTML references both files<picture> set up
5Live URL returns HTTP 200 for bothCDN serving correctly

Sources

Last verified: 2026-07-14. P42 has held across batches “1-8” of today’s deploy.

Sources

#webp#png#p42#deploy#cache

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.

Your email address will not be published. Required fields are marked.