Troubleshooting·7 min read·

Why are my images not loading?

R2 bucket CORS config, CDN cache propagation delay, image hotlink protection, and MIME type issues — a diagnostic walkthrough for broken images in VeloCMS.

Broken images are jarring for readers and embarrassing for authors. The good news is that image failures in VeloCMS almost always trace back to one of four root causes, and each has a clear fix. Let's work through them in order of likelihood.

Check the browser console first

Open the page with the broken image, right-click → Inspect, and go to the Console tab. Image failures usually produce one of three error types: a 403 Forbidden (permissions or hotlink protection), a CORS error (cross-origin headers missing on the R2 bucket), or a net::ERR_FAILED (DNS or network issue). The exact error tells you which section below to jump to.

R2 bucket CORS configuration

Cloudflare R2 buckets do not have CORS enabled by default. If you set up your own R2 bucket (self-hosted mode), you need to add a CORS rule that allows GET requests from your blog's domain. Without this, the browser blocks image loads when the page is served from a different origin than the bucket.

[{"AllowedOrigins":["https://yourdomain.com","https://yourblog.velocms.org"],"AllowedMethods":["GET","HEAD"],"AllowedHeaders":["*"],"ExposeHeaders":["ETag"],"MaxAgeSeconds":86400}]

Apply this via the Cloudflare R2 dashboard: go to R2 → your bucket → Settings → CORS Policy → Edit CORS Policy. Paste the JSON above, replacing the domain values with your actual domain. Save and wait 30 seconds for the policy to propagate. Then hard-refresh the page with the broken images (Ctrl+Shift+R on Windows/Linux, Cmd+Shift+R on Mac).

CDN cache propagation delay

After uploading an image via Admin → Media, VeloCMS stores it in R2 and serves it through Cloudflare's CDN. The first time a reader loads that image URL, Cloudflare fetches it from R2 and caches it at the edge. Subsequent loads are fast. But if you uploaded an image and immediately published the post, there's a small window (typically 10–60 seconds) where the CDN edge node closest to your reader hasn't cached the image yet. If a reader reports an image was broken right when the post went live and it's fine now, this propagation delay was likely the cause.

Image hotlink protection

If you enabled Cloudflare's hotlink protection (Cloudflare dashboard → Scrape Shield → Hotlink Protection), Cloudflare will block image requests that originate from other websites. This can also block your own images if your blog domain isn't on the allowlist. Check Cloudflare → Scrape Shield → Hotlink Protection and add your blog domain (and any preview domains like the .velocms.org subdomain) to the allowlist.

MIME type and upload errors

VeloCMS accepts JPEG, PNG, WebP, GIF, AVIF, and SVG uploads. Files with the wrong extension or a corrupted header will appear to upload successfully but the actual binary is invalid and the browser can't render it. In the browser console, this shows as a 200 OK response with a MIME type of application/octet-stream instead of image/jpeg or image/webp.

  • Go to Admin → Media and find the broken image.
  • Click the image entry to open the detail panel.
  • Try downloading the file directly using the URL shown — if the download is 0 bytes or corrupted, the upload failed mid-transfer.
  • Delete the entry and re-upload the original file.
  • If the same file fails again, check your Railway deployment's request body size limit (default is 50MB — large RAW camera files may exceed this).

Images uploaded through the editor's drag-and-drop work differently from the Media Library upload. If drag-and-drop fails but Media Library works (or vice versa), mention this in your support request — it helps narrow down whether the issue is in the editor's file handler or the API route.

Still broken after all of the above?

Open Admin → Settings → Logs and look for entries with 'R2' or 'media' in the message around the time you uploaded. If you see a 'Failed to upload to R2' error, your CLOUDFLARE_R2_ACCESS_KEY_ID, CLOUDFLARE_R2_SECRET_ACCESS_KEY, or CLOUDFLARE_R2_BUCKET_NAME environment variable may be wrong. Go to Railway → VeloCMS service → Variables and double-check all three values against your R2 API token in the Cloudflare dashboard.