@synchronized-studio/cmsassets-core

Universal JavaScript SDK for CMS Assets. Routes CMS API and asset requests through your tenant's proxy — adding edge caching, image optimization, and server-side token injection without changing your application logic.

Works in any JavaScript environment: Node.js 18+, browsers, Cloudflare Workers, Deno.

Built-in providers: Contentful, Prismic — plus any CMS via createGenericProvider


Install

npm install @synchronized-studio/cmsassets-core
pnpm add @synchronized-studio/cmsassets-core

How it works

Instead of calling Contentful or Prismic directly, requests go through your tenant's proxy endpoint:

Your app
  → CMS Assets edge (tenant.cmsassets.com)
    → Contentful / Prismic / ...

@synchronized-studio/cmsassets-core handles the URL rewriting at the transport layer. You configure it once and drop it into your CMS SDK — no other code changes needed.


Quick start

Contentful

import { createCmsAssetsFetch } from '@synchronized-studio/cmsassets-core'
import { createClient } from 'contentful'

const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',          // your CMS Assets tenant slug
  provider: 'contentful',
  providerConfig: {
    spaceId: 'abc123xyz',     // your Contentful space ID
  },
})

const client = createClient({
  space: 'abc123xyz',
  accessToken: '',            // leave empty — injected server-side by the proxy
  adapter: cmsFetch,
})

// All API calls go through the proxy automatically:
// https://cdn.contentful.com/spaces/abc123xyz/entries
// → https://my-site.cmsassets.com/~api/spaces/abc123xyz/entries

Prismic

import { createCmsAssetsFetch } from '@synchronized-studio/cmsassets-core'
import * as prismic from '@prismicio/client'

const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',
  provider: 'prismic',
})

const client = prismic.createClient('my-repo', {
  fetch: cmsFetch,
})

// All API calls go through the proxy automatically:
// https://my-repo.cdn.prismic.io/api/v2/documents/search?ref=...
// → https://my-site.cmsassets.com/~api/api/v2/documents/search?ref=...

Any CMS via generic provider

import { createGenericProvider, createCmsAssetsFetch } from '@synchronized-studio/cmsassets-core'

const storyblok = createGenericProvider({
  id: 'storyblok',
  apiHosts: ['api.storyblok.com'],
  assetHosts: ['a.storyblok.com'],
})

const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',
  provider: storyblok,   // pass ProviderDefinition directly — no registry needed
})

// https://api.storyblok.com/v2/stories → https://my-site.cmsassets.com/~api/v2/stories
// https://a.storyblok.com/f/123/hero.jpg → https://my-site.cmsassets.com/f/123/hero.jpg

URL rewriting without fetch

Use createUrlRewriter when you need to transform URLs without intercepting fetch — useful for server-side response body processing, logging, or building custom adapters.

import { createUrlRewriter } from '@synchronized-studio/cmsassets-core'

const rewrite = createUrlRewriter({
  tenant: 'my-site',
  provider: 'contentful',
  providerConfig: { spaceId: 'abc123xyz' },
})

// API URL
rewrite('https://cdn.contentful.com/spaces/abc123xyz/entries?locale=en')
// → { url: 'https://my-site.cmsassets.com/~api/spaces/abc123xyz/entries?locale=en', kind: 'api', rewritten: true }

// Asset URL — image optimization params are preserved
rewrite('https://images.ctfassets.net/abc123xyz/assetId/token/hero.jpg?w=1200&fm=webp')
// → { url: 'https://my-site.cmsassets.com/assetId/token/hero.jpg?w=1200&fm=webp', kind: 'asset', rewritten: true }

// Non-CMS URL — passes through unchanged
rewrite('https://fonts.googleapis.com/css2?family=Inter')
// → { url: 'https://fonts.googleapis.com/css2?family=Inter', kind: 'unknown', rewritten: false }

API reference

createCmsAssetsFetch(options)

Returns a fetch-compatible function that rewrites CMS URLs through the proxy. Safe to use as a global fetch replacement — non-CMS requests pass through unchanged.

const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',          // required — CMS Assets tenant slug
  provider: 'contentful',     // required — provider id string OR a ProviderDefinition object
  providerConfig: {           // optional — provider-specific config
    spaceId: 'abc123xyz',     //   Contentful: spaceId
  },
  enableApiProxy: true,       // optional — rewrite API requests (default: true)
  enableAssetProxy: true,     // optional — rewrite asset requests (default: true)
  registry: myRegistry,       // optional — custom provider registry (used when provider is a string)
})

Selective proxying:

// Asset proxy only — API calls go directly to Contentful
const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',
  provider: 'contentful',
  enableApiProxy: false,
})

// API proxy only — images are not proxied
const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',
  provider: 'prismic',
  enableAssetProxy: false,
})

createUrlRewriter(options)

Returns a bound (url: string | URL) => RewriteResult function. Same options as createCmsAssetsFetch.

import { createUrlRewriter } from '@synchronized-studio/cmsassets-core'

const rewrite = createUrlRewriter({
  tenant: 'my-site',
  provider: 'prismic',
})

const { url, kind, rewritten } = rewrite('https://images.prismic.io/my-repo/hero.jpg?w=800&fm=webp')
// url:       'https://my-site.cmsassets.com/my-repo/hero.jpg?w=800&fm=webp'
// kind:      'asset'
// rewritten: true

classifyUrl(url, provider)

Classify a URL as 'api', 'asset', or 'unknown' based on its hostname.

import { classifyUrl, contentful, prismic } from '@synchronized-studio/cmsassets-core'

classifyUrl('https://cdn.contentful.com/spaces/abc/entries', contentful)  // 'api'
classifyUrl('https://images.ctfassets.net/abc/assetId/token/img.jpg', contentful)  // 'asset'
classifyUrl('https://my-repo.cdn.prismic.io/api/v2/documents/search', prismic)  // 'api'
classifyUrl('https://images.prismic.io/my-repo/photo.jpg', prismic)  // 'asset'
classifyUrl('https://example.com/logo.svg', contentful)  // 'unknown'

rewriteUrl(url, kind, provider, tenant, config?)

Low-level rewrite — takes a pre-classified URL and returns a RewriteResult.

import { classifyUrl, rewriteUrl, contentful } from '@synchronized-studio/cmsassets-core'

const url = new URL('https://cdn.contentful.com/spaces/abc/entries?locale=en')
const kind = classifyUrl(url, contentful)
const result = rewriteUrl(url, kind, contentful, 'my-site', { spaceId: 'abc' })

// result.url       → 'https://my-site.cmsassets.com/~api/spaces/abc/entries?locale=en'
// result.kind      → 'api'
// result.rewritten → true

ProviderRegistry

Register custom or extended provider definitions.

import { ProviderRegistry, contentful, prismic } from '@synchronized-studio/cmsassets-core'

const registry = new ProviderRegistry()
  .register(contentful)
  .register(prismic)

const cmsFetch = createCmsAssetsFetch({
  tenant: 'my-site',
  provider: 'contentful',
  registry,
})

The defaultRegistry is a shared singleton pre-populated with contentful and prismic. You only need a custom registry if you want to override a built-in provider or register your own.


createGenericProvider(config)

Create a ProviderDefinition from a config object — use any CMS or API origin without writing a custom provider from scratch.

import { createGenericProvider } from '@synchronized-studio/cmsassets-core'

const provider = createGenericProvider({
  id: 'storyblok',                        // unique provider id
  apiHosts: ['api.storyblok.com'],        // API hostnames (exact or wildcard)
  assetHosts: ['a.storyblok.com'],        // asset hostnames

  // Optional — stored for future Worker-side support, not yet processed by proxy
  auth: {
    header: 'Authorization',
    format: 'Bearer {token}',
  },
  previewBypass: {
    queryParams: ['preview', 'draft'],
  },
})

The returned ProviderDefinition plugs directly into createCmsAssetsFetch, createUrlRewriter, classifyUrl, rewriteUrl, and ProviderRegistry.

URL rewriting follows standard CMS Assets conventions:

API:   https://api.storyblok.com/v2/stories → https://{tenant}.cmsassets.com/~api/v2/stories
Asset: https://a.storyblok.com/f/123/img.jpg → https://{tenant}.cmsassets.com/f/123/img.jpg

API_PREFIX

The /~api path prefix used by the CMS Assets API proxy.

import { API_PREFIX } from '@synchronized-studio/cmsassets-core'

console.log(API_PREFIX) // '/~api'

Providers

Contentful

KindHosts
APIcdn.contentful.com, preview.contentful.com
Assetsimages.ctfassets.net, videos.ctfassets.net, assets.ctfassets.net, downloads.ctfassets.net

providerConfig.spaceId — Required for correct asset path stripping. When provided, the space ID segment is removed from the proxy URL path.

// Without spaceId:
// https://images.ctfassets.net/abc/assetId/token/img.jpg
// → https://my-site.cmsassets.com/abc/assetId/token/img.jpg

// With spaceId: 'abc':
// https://images.ctfassets.net/abc/assetId/token/img.jpg
// → https://my-site.cmsassets.com/assetId/token/img.jpg  ← spaceId stripped

Prismic

KindHosts
API*.cdn.prismic.io (wildcard — matches any repo subdomain)
Assetsimages.prismic.io, prismic-io.imgix.net

Prismic image optimization params (w, h, q, fm, auto, fit) are preserved end-to-end.

// Image optimization is preserved:
// https://images.prismic.io/my-repo/hero.jpg?auto=format,compress&w=920&fm=webp&q=80
// → https://my-site.cmsassets.com/my-repo/hero.jpg?auto=format,compress&w=920&fm=webp&q=80

Prismic's ref parameter changes on every content publish, making each published version a distinct cache key — no manual cache invalidation needed.


TypeScript

All types are exported from the package root:

import type {
  RequestKind,           // 'api' | 'asset' | 'unknown'
  ProviderDefinition,    // shape of a provider
  ProviderConfig,        // Record<string, string | undefined>
  RewriteResult,         // { url, kind, rewritten }
  CmsAssetsFetchOptions, // options for createCmsAssetsFetch
  UrlRewriterOptions,    // options for createUrlRewriter
  GenericProviderConfig, // config shape for createGenericProvider
} from '@synchronized-studio/cmsassets-core'

Custom providers

The easiest way to add support for any CMS:

import { createGenericProvider, createCmsAssetsFetch } from '@synchronized-studio/cmsassets-core'

const sanity = createGenericProvider({
  id: 'sanity',
  apiHosts: ['*.api.sanity.io'],
  assetHosts: ['cdn.sanity.io'],
  auth: { header: 'Authorization', format: 'Bearer {token}' },
})

// Pass directly — no registry needed
const cmsFetch = createCmsAssetsFetch({ tenant: 'my-site', provider: sanity })

// Or register and use by id string
import { defaultRegistry } from '@synchronized-studio/cmsassets-core'
defaultRegistry.register(sanity)
const cmsFetch2 = createCmsAssetsFetch({ tenant: 'my-site', provider: 'sanity' })

Using ProviderDefinition directly (advanced)

For full control over rewrite logic, implement the interface manually:

import { ProviderDefinition, defaultRegistry } from '@synchronized-studio/cmsassets-core'

const sanity: ProviderDefinition = {
  id: 'sanity',
  apiHosts: ['*.api.sanity.io'],
  assetHosts: ['cdn.sanity.io'],
  rewriteApiUrl(url, tenant) {
    return `https://${tenant}.cmsassets.com/~api${url.pathname}${url.search}`
  },
  rewriteAssetUrl(url, tenant) {
    return `https://${tenant}.cmsassets.com${url.pathname}${url.search}`
  },
}

defaultRegistry.register(sanity)

Requirements

  • Node.js 18+ (or any runtime with the Fetch API)
  • No dependencies
Need help understanding this?Ask CMS Assets Copilot about features, setup, or integrations.
Ask Copilot →