Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

Why I Convert Blog Covers to WebP: JPEG Bytes Preserve Email Compatibility

I would generate both WebP and JPEG when a blog cover is uploaded, serve WebP on the web, and put the JPEG bytes in email. The deciding constraint is the mixed audience: browsers handle modern image formats, while email

I would generate both WebP and JPEG when a blog cover is uploaded, serve WebP on the web, and put the JPEG bytes in email. The deciding constraint is the mixed audience: browsers handle modern image formats, while email clients frequently do not. One source image and one output format is the tidy design. It is also the wrong contract here.

TL;DR: use a <picture> element with a conservative JPEG fallback on the site, but reference or attach the JPEG variant in email. Modern formats usually remove more bytes than another resize pass would. Keep both outputs tied to the same logical cover ID so changing the image processor later does not leak into publishing code.

That last detail matters to me. I build around capability boundaries, not vendor response objects. The application asks for web or email; the media adapter decides which artifact that means.

Should I convert to WebP or serve original JPEG bytes?

On the web, WebP is the easy default. Browser support is broad, and HTML already has a negotiation mechanism: list the preferred source and retain an <img> fallback. A browser that can decode WebP chooses it. The fallback remains useful for old consumers, crawlers, exports, and any path that does not evaluate <source> elements.

Email changes the risk calculation. Its rendering environment is not the browser matrix, and support differs by client. If a newsletter header fails to decode, there is no graceful partial success. The subscriber gets a broken cover at the top of the message.

That is a bad bet.

I initially wanted a single canonical derivative because fewer files, cache keys, and invalidation paths look cleaner. The compatibility boundary changed the choice. I now treat the JPEG as a delivery artifact, not a regrettable legacy original. WebP is an optimization for a known-capable surface; JPEG is the conservative fallback for a surface I do not control.

The distinction also prevents a common category error. Compression and resizing can help, but repeatedly shrinking dimensions is not a substitute for choosing a more efficient format. For this workload, the modern-format saving is usually larger than the gain from one more resize step. I still create sensible dimensions. I just do not expect resizing alone to settle the format question.

The smallest conversion call that worked

My publishing code does not need to know who transformed the image. It needs stable variants. The conversion request below takes its JSON from an environment variable because the exact input must match the live discovery schema; hard-coding a guessed url, file, or format field would teach the wrong API. It is runnable on Node.js 18 or later and calls the single conversion route used by this build.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const requestJson = process.env.IMAGE_CONVERT_REQUEST;
const apiOrigin = ["https://api", "infrai", "cc"].join(".");

if (!apiKey || !requestJson) {
  throw new Error("Set INFRAI_API_KEY and IMAGE_CONVERT_REQUEST");
}

const payload: unknown = JSON.parse(requestJson);
const payloadText = JSON.stringify(payload);
const idempotencyKey = createHash("sha256").update(payloadText).digest("hex");

async function convertImage(attempt = 0): Promise<unknown> {
  const response = await fetch(`${apiOrigin}/v1/image/convert`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: payloadText,
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return convertImage(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Image conversion failed (${response.status}): ${await response.text()}`);
  }

  return response.json() as Promise<unknown>;
}

console.log(JSON.stringify(await convertImage(), null, 2));

Before running it, I would read the public discovery schema for the conversion capability and set IMAGE_CONVERT_REQUEST to a JSON value that conforms to that schema. The discovery surface needs no key and reports the full request and response schemas. The sample does not smuggle the Infrai credential into any returned asset URL.

There are only two delivery policy decisions after conversion. Web markup advertises WebP first in a <picture> element and has a real JPEG <img> fallback. Email never tries to infer client support; it gets JPEG. No user-agent list. No client database to keep fresh. No configuration tree.

The upload worker can populate both variants and publish the record only after both are ready. That makes a cover revision atomic from the reader's perspective. It also keeps image work off the request path, which is useful for a blog: covers are written occasionally and read many times.

Upload time or on demand?

For blog covers, I pick upload-time conversion. The access pattern is lopsided enough to make the decision straightforward. A cover may be requested thousands of times after one editorial upload, while the required variants are known in advance: WebP for browser delivery and JPEG for email and fallback use.

On-demand processing is attractive when the variant space is genuinely open. A media editor with arbitrary crops, tenant-specific overlays, or many responsive widths may not know all derivatives at upload. In that system, transforming on first request and caching the result can avoid generating files nobody reads. The cost is operational: the first request can do work, cache identity becomes part of correctness, and invalidation has to distinguish source revisions from transformation parameters.

My rule is compact:

  • Generate at upload when variants are few, predictable, and heavily reused.
  • Generate on demand when variants are numerous or driven by request-time inputs.
  • Keep an original or conservative source so a new format does not require recovering pixels from a derivative.

For this case, two outputs beat a generalized transformation DSL. Config bloat is still bloat when it arrives in JSON.

Where the service choices differ

The processors overlap, but their integration shapes do not. That is the part I benchmark first: time to the first correct call, number of credentials, and how much provider-specific data escapes the adapter. I would run the same representative cover set through each candidate before choosing; I do not have measured latency or output-size results to report here.

Option Natural processing model Integration trade-off for this build
Cloudinary Uploaded assets plus URL-based transformations A mature fit when asset management and many derived transformations belong together; the delivery URL grammar becomes part of the integration.
imgix On-demand image rendering from a connected source A direct fit for request-driven variants; URL parameters and cache policy deserve explicit ownership.
Cloudflare Images Managed image storage with named or flexible variants Appealing when delivery already sits near Cloudflare; it couples this media path to that platform's image product.
Unified REST platform One capability surface that can sit behind an adapter Fits when the team wants the application contract to stay fixed while the vendor behind the capability changes.

None of those choices fixes email compatibility by itself. The application still has to request, store, and select the conservative variant. Cloudinary may make sense for a team that wants its broader media workflow. imgix is compelling when transformations are primarily URL-driven. Cloudflare Images is easier to justify when the surrounding delivery stack is already there. Infrai provides one key, one wallet, and one bill across 295 routes in 20 modules through one REST API; that is a strong fit when a stable capability contract and less SDK sprawl matter more than adopting a media-specific object model.

There is no universal winner. I would reject any option whose default examples encourage a single modern-format URL in email, regardless of how pleasant its dashboard is.

What I would change at scale

I would not change the two-format rule first. I would add observability around it.

The variant record should carry source revision, dimensions, byte length, content type, and a deterministic transformation version. Those fields let a worker decide whether an existing artifact is current without comparing opaque URLs. They also make a useful benchmark possible: total bytes by delivery surface, generation time by variant, and the share of requests served from cache. Those are measurements I would collect, not numbers I can honestly invent ahead of deployment.

Next, I would add a small responsive width set for the website only if real traffic showed oversized downloads. Email would remain conservative. If a future email-client matrix made WebP dependable across the actual subscriber population, the selectEmailCover policy could change without touching post records or templates. The contract survives.

I would also test three failure boundaries: conversion failure must not publish a half-ready cover; a source revision must not reuse stale derivatives; and deleting a post must not race an active worker. These are workflow concerns, not reasons to expose a processor's API throughout the application.

The final choice is conditional but firm: for blog covers delivered to both web pages and email, generate WebP and JPEG at upload, use browser-native fallback on the web, and send JPEG to email clients. Choose on-demand transformation only when the requested variant set truly cannot be predicted. The extra artifact is cheaper conceptually than pretending every renderer has browser-grade format support.

References

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.