> For the complete documentation index, see [llms.txt](https://docs.amondo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amondo.com/developer-guides/media-delivery.md).

# Media Delivery

## Overview

Every `CloudinaryMedia` object in the API exposes a `delivery` field with server-built URL templates for sized delivery. Instead of downloading the full-size `publicUrl`, substitute a width into a template and fetch a variant that matches the size you actually render. This gives smaller downloads, automatic format negotiation (WebP/AVIF for images, best codec for video) and automatic quality tuning.

```graphql
media {
  publicUrl
  width
  height
  delivery {
    __typename
    ... on ImageDelivery {
      image { urlTemplate widths }
      avatar { urlTemplate widths }
    }
    ... on VideoDelivery {
      video { urlTemplate widths }
      poster { urlTemplate widths }
    }
  }
}
```

The field is available on both endpoints (`gql.amondo.com` and `pub.amondo.com`) wherever `CloudinaryMedia` appears — tile media, author avatars, form images, backgrounds and so on.

## Template fields

`delivery` is a union: `ImageDelivery` for `IMAGE` media, `VideoDelivery` for `VIDEO` media. Switch on `__typename`.

| Type            | Field    | Use for                                                          |
| --------------- | -------- | ---------------------------------------------------------------- |
| `ImageDelivery` | `image`  | Rendering the image at layout size                               |
| `ImageDelivery` | `avatar` | Small round author avatars (fixed widths `20, 30, 40, 60, 80`)   |
| `VideoDelivery` | `video`  | Video playback at layout size                                    |
| `VideoDelivery` | `poster` | JPG still of the first video frame — placeholder before playback |

Each of these is a `DeliveryTemplate`:

| Field         | Type              | Description                                     |
| ------------- | ----------------- | ----------------------------------------------- |
| `urlTemplate` | `String!`         | Absolute URL containing the literal token `{w}` |
| `widths`      | `[PositiveInt!]!` | Valid substitutions for `{w}`, ascending        |

Example `urlTemplate` values:

```
image:  https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/wpib2xtdf.jpg
avatar: https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:eco,w_{w}/v1718000000/amondo/wpib2xtdf.jpg
video:  https://media.amondo.com/amondo-media/video/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/k2n4fh1q.mp4
poster: https://media.amondo.com/amondo-media/video/upload/c_limit,f_auto,q_auto:low,so_0,w_{w}/v1718000000/amondo/k2n4fh1q.jpg
```

{% hint style="warning" %}
`delivery` is `null` for passthrough media (Instagram CDN, S3, base64, uploads still processing). Always handle this case: render `publicUrl` as-is.
{% endhint %}

## Choosing a width

To build a URL:

1. Take the width the media is rendered at, in layout units (CSS px, iOS points, Android dp).
2. Multiply by the device pixel ratio, **clamped to 2** — beyond 2× the extra pixels are not worth the bytes.
3. Pick the **smallest `widths` entry ≥ that product**, or the largest entry if none is.
4. Substitute it for the `{w}` token in `urlTemplate`.

Only the listed widths are supported — do not substitute arbitrary values.

```js
function deliveryUrl(template, renderedWidth, pixelRatio) {
  const target = renderedWidth * Math.min(pixelRatio, 2)
  const width = template.widths.find(w => w >= target) ?? template.widths.at(-1)
  return template.urlTemplate.replace('{w}', String(width))
}
```

The `widths` ladder runs in 100px steps and is capped at the source media width (rounded up to the next 100) and at 2000 — e.g. a 1080px-wide source yields `[100, 200, …, 1100]`. The ladder is per-media; always read it from the response.

## Scaling and crop rules

Delivery URLs scale, they never crop. Every template uses `c_limit`:

* **Aspect ratio is always preserved.** The returned file is the source scaled down so its width fits `{w}`; height follows the source aspect ratio.
* **Never upscaled.** If `{w}` exceeds the source width you get the source size. Don't assume the returned file is exactly `{w}` pixels wide.
* **Never cropped server-side.** Fitting media into a box of a different aspect ratio (a square grid cell, a 9:16 story) is always done by the renderer.

Tiles tell you which fit the editor chose. `MediaTileAttributes` (and the other tile attribute types) carry two `TileCrop` settings:

* `crop` — how media displays in the grid: `FILL` crops to fill the cell, `FIT` letterboxes.
* `cropFrame` — the same choice for the expanded frame view.

Map `FILL`/`FIT` to your platform's fit modes:

| `TileCrop` | CSS                   | UIKit                                | SwiftUI                     | Android View | Jetpack Compose     |
| ---------- | --------------------- | ------------------------------------ | --------------------------- | ------------ | ------------------- |
| `FILL`     | `object-fit: cover`   | `.scaleAspectFill` + `clipsToBounds` | `.scaledToFill().clipped()` | `centerCrop` | `ContentScale.Crop` |
| `FIT`      | `object-fit: contain` | `.scaleAspectFit`                    | `.scaledToFit()`            | `fitCenter`  | `ContentScale.Fit`  |

Uploaded Cloudinary media is ingested with a maximum 4K resolution preset. Larger source uploads may be normalised at ingest before delivery templates are generated.

### Web — CSS only

No JavaScript needed: expand the ladder into `srcset` and let the browser pick, then crop with `object-fit`.

```html
<img
  src="https://…/c_limit,f_auto,q_auto:good,w_800/…/photo.jpg"
  srcset="https://…/w_400/…/photo.jpg 400w, https://…/w_800/…/photo.jpg 800w, https://…/w_1100/…/photo.jpg 1100w"
  sizes="(max-width: 600px) 100vw, 400px"
  style="width: 100%; height: 100%; object-fit: cover"
  alt=""
/>
```

Generate the `srcset` string directly from `widths`:

```js
const srcset = template.widths.map(w => `${template.urlTemplate.replace('{w}', String(w))} ${w}w`).join(', ')
```

For `background-image`, use `background-size: cover` (FILL) or `contain` (FIT).

{% hint style="info" %}
With `srcset` the browser applies the full device pixel ratio (3× on many phones) and may pick one ladder step higher than the clamp-to-2 rule would. Every listed width is valid, so this is fine. If you want the bandwidth-optimal 2× cap, select the width yourself with the JS helper above.
{% endhint %}

### iOS

Select the width from the view's point size and screen scale, then let the image view crop:

```swift
func deliveryURL(template: DeliveryTemplate, pointWidth: CGFloat, scale: CGFloat) -> URL {
  let target = pointWidth * min(scale, 2)
  let width = template.widths.first { CGFloat($0) >= target } ?? template.widths.last!
  return URL(string: template.urlTemplate.replacingOccurrences(of: "{w}", with: String(width)))!
}
```

UIKit:

```swift
imageView.contentMode = .scaleAspectFill // FILL; use .scaleAspectFit for FIT
imageView.clipsToBounds = true
```

SwiftUI:

```swift
AsyncImage(url: url) { image in
  image.resizable().scaledToFill() // FILL; .scaledToFit() for FIT
} placeholder: {
  Color.gray.opacity(0.2)
}
.frame(width: 160, height: 160)
.clipped()
```

### Android

View widths are already in physical pixels; rescale them so densities above 2× don't fetch oversized files:

```kotlin
fun deliveryUrl(template: DeliveryTemplate, viewWidthPx: Int, density: Float): String {
  val target = viewWidthPx / density * minOf(density, 2f)
  val width = template.widths.firstOrNull { it >= target } ?: template.widths.last()
  return template.urlTemplate.replace("{w}", width.toString())
}
```

Views (with Coil or Glide):

```xml
<!-- FILL; use fitCenter for FIT -->
<ImageView
  android:scaleType="centerCrop"
  ... />
```

Jetpack Compose:

```kotlin
AsyncImage(
  model = url,
  contentDescription = null,
  contentScale = ContentScale.Crop, // FILL; ContentScale.Fit for FIT
  modifier = Modifier.size(160.dp),
)
```

## Videos and posters

For `VideoDelivery`, pick widths for `video` and `poster` with the same algorithm, sized to the player:

* Show the `poster` URL as the placeholder (`<video poster="…">`, `AVPlayerLayer` overlay, Compose placeholder) while the video loads. It is a JPG of the first frame at low quality — cheap and instant.
* Feed the `video` URL to your player. Format and codec are negotiated automatically via `f_auto`.

## Fallback

Code the `delivery == null` path from day one — it is not an edge case. Media served from Instagram's CDN, direct S3 objects and uploads that are still processing all return `null`. In that case render `publicUrl` unchanged (it may be full-size) with the same client-side FILL/FIT treatment.

## Full example

Query a published imprint on `pub.amondo.com` (no auth) and render its media tiles:

```graphql
query {
  imprintPublication(id: "e4f7a1b2-3c4d-5e6f-7a8b-9c0d1e2f3a4b") {
    imprint {
      layoutV2(device: MOBILE) {
        sections {
          elements {
            tile {
              ... on MediaTile {
                mediaTileAttributes {
                  crop
                  cropFrame
                  media {
                    publicUrl
                    width
                    height
                    delivery {
                      __typename
                      ... on ImageDelivery {
                        image {
                          urlTemplate
                          widths
                        }
                        avatar {
                          urlTemplate
                          widths
                        }
                      }
                      ... on VideoDelivery {
                        video {
                          urlTemplate
                          widths
                        }
                        poster {
                          urlTemplate
                          widths
                        }
                      }
                    }
                  }
                  avatar {
                    publicUrl
                    delivery {
                      ... on ImageDelivery {
                        avatar {
                          urlTemplate
                          widths
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

Example `media` payload:

```json
{
  "publicUrl": "https://media.amondo.com/amondo-media/image/upload/v1718000000/amondo/wpib2xtdf.jpg",
  "width": 1080,
  "height": 1350,
  "delivery": {
    "__typename": "ImageDelivery",
    "image": {
      "urlTemplate": "https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/wpib2xtdf.jpg",
      "widths": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100]
    },
    "avatar": {
      "urlTemplate": "https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:eco,w_{w}/v1718000000/amondo/wpib2xtdf.jpg",
      "widths": [20, 30, 40, 60, 80]
    }
  }
}
```

Rendering this tile in a 400 CSS px column on a 3× phone: target = 400 × min(3, 2) = 800 → pick `w_800`. The tile's `crop` is `FILL`, so the image gets `object-fit: cover`.
