Cloudinary Integration

CMS Assets has first-class support for Cloudinary. It handles both image and video asset URLs and provides a response transformer that rewrites Cloudinary CDN URLs in your API responses.


Supported URL patterns

Cloudinary serves all assets from a single domain using your cloud name and different resource type prefixes:

Images:

https://res.cloudinary.com/{cloudName}/image/upload/v1234567890/folder/photo.jpg

Videos:

https://res.cloudinary.com/{cloudName}/video/upload/v1234567890/folder/clip.mp4

With transformations:

https://res.cloudinary.com/{cloudName}/image/upload/w_800,h_600,c_fill/v1234567890/folder/photo.jpg

Private CDN distribution (Advanced plan and higher):

https://{cloudName}-res.cloudinary.com/image/upload/v1234567890/folder/photo.jpg

All of the above are rewritten to:

https://your-tenant.cmsassets.com/image/upload/v1234567890/folder/photo.jpg

Tenant setup

When creating your tenant, select Cloudinary as the CMS type and provide your cloud name:

FieldValue
CMSCloudinary
Cloud namemy-cloud
Website domainyour-site.com

Two origins are automatically configured:

  • Image origin: https://res.cloudinary.com/{cloudName}
  • Video origin: https://res.cloudinary.com/{cloudName}

Both use the same base URL since Cloudinary serves all asset types from a single domain. CMS Assets routes requests to the correct resource type based on the URL path.


Video support

Cloudinary serves images and videos from the same domain (res.cloudinary.com) using different path prefixes (/image/upload/ vs /video/upload/). CMS Assets handles both automatically.

Range requests for video seeking and streaming are fully supported.


API proxy (read-only)

CMS Assets can proxy read-only Cloudinary Admin API requests through its /~api/ endpoint. This means your API key and secret are stored encrypted on the server and never exposed to the browser.

Configuration

In the tenant settings under API Proxy:

FieldValue
API Origin URLhttps://api.cloudinary.com/v1_1/<cloud_name>
Auth modeBasic auth
Tokenbtoa("api_key:api_secret") — your credentials base64-encoded

The Cloudinary Admin API uses HTTP Basic authentication with your API key and secret concatenated as api_key:api_secret. Encode this string with btoa() before saving it as the token.

Proxied requests

Any GET request to https://your-tenant.cmsassets.com/~api/<path> is forwarded to https://api.cloudinary.com/v1_1/<cloud_name>/<path> with the Authorization: Basic <token> header injected automatically.

Examples:

# List image resources
GET https://your-tenant.cmsassets.com/~api/resources/image

# Search assets
GET https://your-tenant.cmsassets.com/~api/resources/image/upload?prefix=products

Responses are cached (default 60s) and Cloudinary delivery URLs in the JSON body are rewritten to proxy URLs automatically.

Note: Only GET requests are proxied. Upload, rename, destroy, and other write operations are not supported through the CMS Assets API proxy.


Response transformer

Install the response transformer to automatically rewrite Cloudinary asset URLs in your API responses:

npm install @synchronized-studio/response-transformer

Basic usage

import { transformCloudinaryAssetUrls } from "@synchronized-studio/response-transformer"

const response = await fetch(
  `https://api.cloudinary.com/v1_1/${cloudName}/resources/image`,
  {
    headers: {
      Authorization: `Basic ${btoa(apiKey + ":" + apiSecret)}`
    }
  }
)
const data = await response.json()

const transformed = transformCloudinaryAssetUrls(data, {
  cloudName: "my-cloud",
  cmsAssetsUrl: "https://your-tenant.cmsassets.com"
})

What gets transformed

The transformer processes the entire JSON response and rewrites all matching URLs. This includes:

  • Image asset URLs
  • Video asset URLs
  • Derived/transformed asset URLs
  • Any URL matching res.cloudinary.com/{cloudName}

Cloudinary transformation segments in the URL path (like /w_800,h_600,c_fill/) are preserved during rewriting.

Using an environment variable

Set CMS_ASSETS_URL in your environment and omit the cmsAssetsUrl option:

CMS_ASSETS_URL=https://your-tenant.cmsassets.com
const transformed = transformCloudinaryAssetUrls(data, {
  cloudName: "my-cloud"
})

Nuxt / SSR integration

For Nuxt or other SSR frameworks, wrap your Cloudinary API calls:

// composables/useCloudinaryData.ts
import { transformCloudinaryAssetUrls } from "@synchronized-studio/response-transformer"

export async function useCloudinaryAssets() {
  const data = await cloudinaryClient.search
    .expression("folder:products")
    .execute()

  return transformCloudinaryAssetUrls(data, {
    cloudName: "my-cloud",
    cmsAssetsUrl: useRuntimeConfig().public.cmsAssetsUrl
  })
}

Using the generic transformer

You can also use the unified transformCmsAssetUrls function:

import { transformCmsAssetUrls } from "@synchronized-studio/response-transformer"

const transformed = transformCmsAssetUrls(data, {
  cms: "cloudinary",
  cloudName: "my-cloud",
  cmsAssetsUrl: "https://your-tenant.cmsassets.com"
})

Cloudinary Transformations

Cloudinary's transformation URL API embeds transformations directly in the URL path (e.g. /w_800,h_600,c_fill/). When assets are proxied through CMS Assets:

  • The edge cache uses the full URL path (including transformation segments) as the cache key
  • Different transformation variants are cached separately since they have different paths
  • This means Cloudinary transformations generally work as expected through the proxy

However, query parameters appended to Cloudinary URLs are stripped from the cache key for image types.


Advanced options

Custom transformers

Add additional transformers that run after the default Cloudinary ones:

const transformed = transformCloudinaryAssetUrls(data, {
  cloudName: "my-cloud",
  transformers: [
    (jsonStr, { base }) => {
      return jsonStr.replaceAll("old-pattern", "new-pattern")
    }
  ]
})

Post-transform hook

Run a function on the parsed result after all URL replacements:

const transformed = transformCloudinaryAssetUrls(data, {
  cloudName: "my-cloud",
  postTransform: (data) => {
    return data
  }
})

Error handling

By default, transform errors are logged as warnings and the original data is returned unchanged. You can provide a custom error handler:

const transformed = transformCloudinaryAssetUrls(data, {
  cloudName: "my-cloud",
  onError: (error) => {
    Sentry.captureException(error)
  }
})
Need help understanding this?Ask CMS Assets Copilot about features, setup, or integrations.
Ask Copilot →