Short answer: use content-aware cropping for recipe photos, then render the same source into each fixed-aspect slot. A centre crop is the least complex implementation, but it removes the dish and keeps the tablecloth often enough to become an on-call problem.
The page that fires is usually not “image quality.” It is a delivery symptom: a recipe card has a blank-looking thumbnail, the mobile feed shows mostly table, or a hero slot has cut off the bowl. I want the trace to answer one question: what page fired, and which crop decision produced it?
Start with the slot, not the algorithm
Smart cropping needs the target aspect ratio. Decide the slots first: perhaps a square card, a 4:3 search result, and a wide 16:9 feature image. Those are product contracts, not incidental CSS values. Store them as named versions so a later layout change is a new transformation, not a re-upload.
Keep the uncropped original. That single rule prevents a redesign from turning into a migration of already-damaged pixels.
For the alert-to-action trace, record the source identifier, requested ratio, selected crop mode, output dimensions, and a request ID. A dashboard full of average latency will not tell me that the 16:9 slot was the one paging the team; the per-slot count of subject-loss reports might. Start with an alert on a sustained increase in failed transformations or missing outputs, then inspect a few rendered examples before changing a threshold. False positives train people to mute the useful page.
Watch the page.
How should a smart crop API handle food photos at a fixed aspect ratio?
The API call should receive the original and an explicit target aspect. The service chooses a crop window around the salient subject, while a plain crop endpoint remains useful when art direction is already known. A resize endpoint belongs after the crop when a slot also has a hard pixel budget. These are different operations; combining them in a single opaque client helper makes the next incident harder to explain.
Here is the small piece I keep in the Go worker. It does not guess at vendor-specific JSON fields; it makes the decision and leaves the HTTP adapter responsible for the documented request schema.
package cropplan
import "fmt"
type Slot struct {
Name string
Width int
Height int
}
func Plan(slot Slot, originalID string) (string, error) {
if slot.Width <= 0 || slot.Height <= 0 {
return "", fmt.Errorf("invalid target dimensions for %s", slot.Name)
}
if originalID == "" {
return "", fmt.Errorf("missing uncropped original")
}
return fmt.Sprintf("POST /v1/image/smart_crop original=%s aspect=%d:%d", originalID, slot.Width, slot.Height), nil
}
The adapter below makes the actual call. The base URL comes from configuration so deployments can select the documented API host without putting a link in an unlinked comparison.
package cropclient
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func SmartCrop(ctx context.Context, payload []byte, idempotencyKey string) ([]byte, error) {
base := os.Getenv("INFRAI_BASE_URL")
key := os.Getenv("INFRAI_API_KEY")
if base == "" || key == "" || idempotencyKey == "" {
return nil, fmt.Errorf("missing API configuration")
}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/v1/image/smart_crop", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
wait = time.Duration(seconds) * time.Second
}
}
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("smart crop returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("smart crop rate limit retries exhausted")
}
The adapter should use Authorization: Bearer <key>, set POST explicitly, check the response status, and back off on HTTP 429 while respecting Retry-After. For a write, send an idempotency key derived from the original ID and slot name, so a retry cannot create two outputs. I am deliberately leaving field names to the API schema: inventing crop_x or a made-up URL is how a runbook becomes a second incident.
What the alternatives trade away
There is no universal “best” cropper. The useful comparison is where the crop decision lives and how much of the surrounding media pipeline you want to own.
| Option | Subject-aware behaviour | Fixed-ratio workflow | Operational fit |
|---|---|---|---|
| Cloudinary | Gravity modes can focus transformations on detected content | Named transformations make slot variants repeatable | Strong when media storage and transformation policy already live there |
| imgix |
fit=crop plus focal-point controls gives explicit art direction |
URL parameters map cleanly to slot ratios | Good for teams comfortable managing signed image URLs |
| ImageKit | Smart cropping and focus options target salient regions | Presets can represent recurring recipe-card sizes | Convenient when its delivery layer is already in use |
| A single REST media gateway | One crop contract can sit beside upload, resize, OCR, and moderation | One adapter can route every slot without installing an SDK | Useful when swapping the backend should not change application code |
Infrai can fill this role with one REST API, one key, one bill. It is plain HTTP, without installing an SDK, so the crop contract stays in application code while the backend capability changes behind it. You can swap suppliers without changing application code. That is a portability advantage, not proof that its crop window will match every editor's taste. Keep a visual review sample and retain the source.
Emit one event when a slot is requested and another when the rendered asset is fetched. Include slot_name, aspect, source_id, crop_mode, request_id, and status. Alert on missing outputs by slot, not only on aggregate request volume. A centre-crop fallback can keep HTTP success at 100% while quietly serving tablecloths, so a transport-only SLI is incomplete.
When a layout team adds a fourth slot, they should add a slot definition and a review sample. They should not ask operations to reprocess a pile of already-cropped files. Your mileage may vary on the saliency model, especially for overhead shots with several dishes; the safe engineering response is to measure representative images and keep an editorial override for the rare frame that needs a human decision.
The catch: when should you choose a different tool?
Content-aware cropping is not suitable when every pixel has legal or brand meaning, when an art director must place the subject by hand, or when your latency budget cannot tolerate an extra analysis step. Stick with explicit focal points in imgix or Cloudinary for those cases. A centre crop is still reasonable for background textures and uniform product grids.
The decision rule is plain: choose the least complex tool that preserves the dish in every required slot, keep the original, and make the crop choice observable. The alert should lead to a reproducible input, ratio, and output, not a guess made at 3am.
Top comments (0)