Strapi
Media in Strapi: Image Formats, CDN Strategy, and SEO Alt Text
Strapi doesn't resize, convert, or CDN your images by default — it stores uploads and returns URLs. Here's the actual pipeline we wire in so images are fast, correctly formatted, and accessible, not just "working."

A Strapi project that "just works" on media out of the box is usually one that hasn't been tested with real image traffic yet. Strapi's default local file storage will happily serve a 4MB PNG at its original dimensions to every visitor, on every device, because nothing in the default setup resizes, converts, or CDN-caches it — Strapi's job, by design, is to store the file and return a URL; everything else is a pipeline you build around it. Skipping that pipeline is the single most common reason a Strapi-backed site scores badly on LCP despite an otherwise clean Next.js frontend.
Where media actually lives, and why the default isn't production-ready
Out of the box, Strapi stores uploaded files on the server's local filesystem and serves them from its own URL. This works fine for local development and breaks down in a few predictable ways in production: local disk storage doesn't survive a redeploy on most hosting platforms (Vercel, most container-based hosts), there's no CDN in front of it, and there's no automatic resizing or format conversion.
Use a Strapi upload provider — @strapi/provider-upload-aws-s3, Cloudinary, or Cloudflare R2 are the common choices — so uploads go to durable object storage instead of ephemeral local disk. Configure it via Strapi's config/plugins.js (or .ts), and every new upload routes through it automatically. Existing files uploaded before the switch need a one-time migration script that moves them to the new provider and updates the stored URLs — don't skip this step assuming old content doesn't matter; a blog's older posts are often exactly the pages still pulling organic traffic.
Put a CDN in front regardless of provider — S3 and R2 both need one added explicitly (CloudFront, Cloudflare); Cloudinary includes CDN delivery as part of its service. The CDN is what actually gets images close to visitors geographically; object storage alone just solves durability, not delivery speed.
Formats and resizing: Strapi doesn't do this for you
This is the gap that catches teams most often: Strapi does not resize images or convert them to WebP/AVIF on its own. Two ways to actually get this:
- A provider that transforms on the fly — Cloudinary is the most common choice here, generating resized, format-negotiated variants via URL parameters without any code on your end. This is the lowest-effort option if you're already paying for Cloudinary's storage.
- Let Next.js handle it. If you're serving images through
next/imagewith the Strapi media URL assrc, Next.js's built-in image optimizer resizes and serves the right format per-request — you just need to add your Strapi/CDN domain toremotePatternsinnext.config. This is often the simpler path when your provider is plain object storage (S3, R2) without its own transform layer, since you're not paying for or configuring a second image service.
Whichever path, the rule that matters: never serve a raw, full-resolution upload directly to the browser. A 3000px-wide product photo displayed in a 400px card is wasted bandwidth and a direct LCP hit, and it's the single most common performance issue we find auditing Strapi-backed sites.
<Image
src={getStrapiMedia(image.url)}
alt={image.alternativeText ?? ""}
width={image.width ?? 800}
height={image.height ?? 450}
/>Alt text: store it once, require it, use it everywhere
Strapi's Media library has a built-in alternativeText field on every upload — use it, rather than adding a parallel custom field, so the CMS's native media picker UI (which surfaces this field directly when selecting an image) stays the single place editors manage it. Expose alternativeText in every API response that includes media, and wire it into every <img> and every next/image alt prop on the frontend — no exceptions, no fallback to an empty string as the default behavior for content images.
Make alt text required for anything used in actual content — a rich text embed, a hero image, a card thumbnail — and reserve genuinely optional (empty) alt text for purely decorative images where a screen reader should skip the element. The distinction matters for both accessibility and SEO: decorative images with empty alt="" are correctly ignored by screen readers, while content images with missing alt text are both an accessibility failure and a missed signal for image search.
If you also have a visible caption requirement (a photo credit, a descriptive line under an image), model that as a separate field — don't conflate it with alternativeText, which serves screen readers and crawlers and is often not identical wording to what you'd want visible on the page.
The pipeline, end to end
| Stage | What handles it | What breaks without it |
|---|---|---|
| Storage | Upload provider (S3, Cloudinary, R2) | Files lost on redeploy with local disk |
| Delivery | CDN in front of storage | Slow, non-geo-distributed image loads |
| Resizing + format | Provider transforms or next/image | Full-size uploads shipped to every device, poor LCP |
| Accessibility + SEO | alternativeText field, required and wired to every alt prop | Screen reader gaps, missed image-search signal |
What we set up by default
On every Strapi build, media goes to object storage with a CDN from day one — never local disk in production — and image delivery goes through next/image (or a provider transform layer, if the project's already paying for one) so nothing ships at full resolution to a browser that doesn't need it. Alt text is required in the content model for anything that isn't explicitly decorative, enforced at the schema level, not left to an editor's discretion under deadline pressure.
This is a standard part of every headless CMS implementation we deliver on Strapi — not an optimization pass added after someone notices slow image loads. If your Strapi site is still serving from local disk or shipping full-resolution uploads to the browser, get in touch — this pipeline is usually a day of setup, and it's one of the highest-return performance fixes available on a content-heavy site.