Docs
Dashboard

Concepts

Media and images

Your bucket, your bytes. Public and private projects, and why a static build must ask for permanent URLs.

ZevContent does not host your images. Media lives in your object storage: ZevCloud Storage, S3, R2, or anything S3-compatible. No byte of it passes through us.

Uploads go browser → your bucket, using a URL we sign. Reads go browser → your bucket, using a URL we resolve. We store a record of the asset, never the file.

What an image looks like in a response

Every image field, gallery entry, featured image, avatar and og image is the same object:

{
  "url": "https://cdn.example.com/media/hero.jpg",
  "alt": "The team at the launch",
  "width": 1600,
  "height": 900,
  "content_type": "image/jpeg"
}

width and height are there so you can reserve space and avoid layout shift without measuring the file. They are null when we could not determine them.

You never receive a raw med_… id in a content response. Resolution happens server-side, in one batched pass per request, so an article with a dozen pictures costs the same round trip as one without.

Public and private projects

Set per project, in the dashboard, and it changes the shape of every image URL.

Public bucket. URLs are permanent. They keep working forever, they cache well, and they are safe to bake into static HTML.

Private bucket. URLs are presigned GETs with an expiry, and the object gains one extra key:

{
  "url": "https://…?X-Amz-Signature=…",
  "url_expires_at": "2026-09-02T11:30:00.000Z",
  "…": "…"
}

The response also carries a short Cache-Control, because caching a URL past its own expiry serves a broken image.

Static builds: ask for permanent URLs

A static build bakes URLs into HTML that outlives the build. A presigned URL dies hours later, and the failure appears long after the deploy that caused it, on a page nobody was looking at.

So say what you need:

curl "https://api.zevcontent.net/v1/blog/articles?media_urls=permanent" \
  -H "Authorization: Bearer $ZEVCONTENT_KEY"

On a public project this changes nothing. On a private project the request is refused with permanent_media_urls_unavailable, rather than handing you links that will break:

This project’s media bucket is private, so every image URL is signed and expires, so there are no permanent URLs to give you. Either make the bucket publicly readable and switch the project to public media access in the dashboard, or fetch this content at request time and re-fetch before url_expires_at, or download each image at build time and serve it yourself.

That is a build failure instead of a silent breakage weeks later, which is the whole reason the parameter exists.

Knowing before you fetch

/v1/introspection reports the project’s media policy once, next to the schema that mentions it:

"media": { "access": "private", "urls": "signed", "url_ttl_seconds": 3600 }

so a client does not have to infer it from the presence of a key inside one field’s shape.

Images inside rich text

The same rules apply to image nodes in a rich text document, with one addition: a node may reference an external URL instead of a library asset. An external image never expires and is never fetched by us. See Rich text.

Updated at, Wednesday, September 2, 2026