@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
| Kind | Hosts |
|---|---|
| API | cdn.contentful.com, preview.contentful.com |
| Assets | images.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
| Kind | Hosts |
|---|---|
| API | *.cdn.prismic.io (wildcard — matches any repo subdomain) |
| Assets | images.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
Using createGenericProvider (recommended)
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