Canonical urls and Open Graph images from nested cms pages
Titles, descriptions, OG images, and canonical URLs generated from the CMS content model, so metadata is always accurate, even on nested ...
The Blog
Performance isn't "turn on next/image and you're done." We generate purposeful image sizes at upload, serve with correct `sizes` attributes, and cache-bust when editors replace media. Here's the full pipeline from CMS upload to browser render.
The hero image on your homepage is probably the single biggest factor in your Largest Contentful Paint score. And LCP is one of the three Core Web Vitals that directly affect your search rankings.
Most teams handle images one of two ways. Either they upload whatever the designer exported and hope the framework sorts it out, or they manually create multiple sizes and formats for every image. The first approach produces slow pages. The second doesn't scale.
We built an image pipeline that handles this at the CMS level, purposeful sizes generated on upload, WebP format conversion, dedicated OG crops for social sharing, blur placeholders for perceived performance, and cache-busting when editors replace media. The frontend consumes what the CMS produces without manual intervention.
The pipeline
Every image uploaded to the CMS goes through a defined pipeline before it's available to the frontend. This isn't optional or configurable per-upload, it's the system default.
Payload generates eight responsive sizes from every upload: thumbnail (300w), square (500×500), small (600w), medium (900w), large (1200w), xlarge (1680w), xxlarge (1920w), and a dedicated OG size (1200×630, cropped centre). Focal point is enabled so editors can control where the crop centres on each image.
Every generated size is converted to WebP at quality 90. This typically reduces file size by 25–35% compared to JPEG at equivalent visual quality. The original format is preserved as a fallback, but the frontend serves WebP by default.
The 1200×630 OG size is generated as a separate crop specifically for social sharing. When a page's Open Graph image is requested, it pulls this dedicated asset rather than squashing a hero image into the wrong aspect ratio. This connects to the metadata architecture we covered recently, social previews should be intentional, not accidental.
Every image served through Next.js Image uses a shared blur placeholder (`placeholder="blur"`). This gives visitors a low-resolution preview while the full image loads, which significantly improves perceived loading speed, even when the actual load time is unchanged.
The frontend
On the frontend, images are rendered through Next.js `Image` with specific configuration that works with what the CMS produces.
The `sizes` attribute is set per-component based on the layout context, a full-width hero gets different `sizes` than a card thumbnail. This matters more than most teams realise: without correct `sizes`, the browser downloads the largest available image regardless of viewport width. A 1920px image on a 375px phone is wasted bandwidth and a slower LCP.
Images above the fold get `priority` and `fetchPriority="high"` to tell the browser to load them first. Everything else lazy-loads by default. This is a simple toggle but the impact on LCP is significant, the hero image starts downloading immediately instead of waiting for the JavaScript bundle to execute.
Quality is set to 100 at the component level because the CMS has already handled compression during WebP conversion. Double-compressing (CMS compression + Next.js compression) produces visible artifacts on some images.
The cache
Here's a detail most image pipelines miss: what happens when an editor replaces an image?
If the URL doesn't change, the CDN and browser cache serve the old image until the cache expires. On sites with aggressive caching (which you want for performance), this can mean the old image persists for days or weeks.
We append a cache-busting parameter derived from the media's updatedAt timestamp. When the image is replaced, the timestamp changes, the URL changes, and every cache layer fetches the new version. The cache TTL stays aggressive (one year) because the URL itself changes on update.
This keeps performance and freshness aligned instead of trading one for the other.
The outcome
The pipeline produces measurably better web performance scores without any per-image manual work.
LCP improves because above-the-fold images are priority-loaded, correctly sized for the viewport, and served in WebP. CLS (Cumulative Layout Shift) is avoided because Next.js Image reserves the correct aspect ratio space before the image loads. Total page weight drops because responsive sizes mean mobile devices download mobile-sized images, not desktop assets.
For the editors, none of this is visible. They upload an image, optionally set the focal point, and the pipeline handles everything else. That's the point, performance should be a system concern, not a per-upload decision.