@synchronized-studio/cmsassets-core
v0.4.2
Published
Universal CMS integration foundation for CMS Assets — provider-aware request classification, URL rewriting, and fetch adapter.
Downloads
610
Maintainers
Readme
@synchronized-studio/cmsassets-core
Universal CMS integration layer for CMS Assets.
Routes CMS API and asset requests through the CMS Assets edge 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-corepnpm add @synchronized-studio/cmsassets-coreyarn add @synchronized-studio/cmsassets-coreHow it works
CMS Assets sits between your app and your CMS. 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 { createContentfulAdapter } from '@synchronized-studio/cmsassets-core'
import { createClient } from 'contentful'
const client = createClient({
space: 'abc123xyz',
accessToken: 'proxy-injected', // any truthy string — real token is injected server-side by the proxy
adapter: createContentfulAdapter({
tenant: 'my-site',
provider: 'contentful',
providerConfig: { spaceId: 'abc123xyz' },
}),
})
// All API calls go through the proxy automatically:
// https://cdn.contentful.com/spaces/abc123xyz/entries
// → https://my-site.cmsassets.com/~api/spaces/abc123xyz/entriesWhy
createContentfulAdapterinstead ofcreateCmsAssetsFetch? The Contentful SDK uses Axios internally, which expects an adapter with a different signature thanfetch. It also injectsAuthorizationandX-Contentful-User-Agentheaders that trigger CORS preflight in browsers.createContentfulAdapterhandles both: it bridges Axios to fetch and strips the headers that would cause CORS issues. For direct fetch usage without the SDK, usecreateCmsAssetsFetch.
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.jpgURL 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)
})Using enableApiProxy / enableAssetProxy:
// 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,
})createContentfulAdapter(options)
Returns an Axios-compatible adapter function for the Contentful JS SDK. Internally creates a cmsFetch for URL rewriting and bridges the Axios adapter interface to fetch. Automatically strips Authorization and X-Contentful-User-Agent headers to prevent CORS preflight requests.
import { createContentfulAdapter } from '@synchronized-studio/cmsassets-core'
import { createClient } from 'contentful'
const client = createClient({
space: 'abc123xyz',
accessToken: 'proxy-injected',
adapter: createContentfulAdapter({
tenant: 'my-site',
provider: 'contentful',
providerConfig: { spaceId: 'abc123xyz' },
}),
})Options: Same as createCmsAssetsFetch, plus:
| Option | Type | Description |
|--------|------|-------------|
| wrapFetch | (baseFetch) => fetch | Optional middleware to wrap the internal fetch before use (e.g. cache bypass) |
// With preview cache bypass:
const adapter = createContentfulAdapter({
tenant: 'my-site',
provider: 'contentful',
providerConfig: { spaceId: 'abc123xyz' },
wrapFetch: (baseFetch) => withCacheBypass(baseFetch, () => isPreview),
})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: trueclassifyUrl(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 → trueProviderRegistry
Register custom or extended provider definitions.
import { ProviderRegistry, contentful, prismic } from '@synchronized-studio/cmsassets-core'
// Create a custom registry
const registry = new ProviderRegistry()
.register(contentful)
.register(prismic)
// Use it with the fetch adapter
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',
apiHosts: ['api.storyblok.com'], // exact or wildcard (e.g. '*.api.example.com')
assetHosts: ['a.storyblok.com'],
// Optional — reserved for future Worker-side auth injection
auth: {
header: 'Authorization',
format: 'Bearer {token}',
},
// Optional — reserved for future proxy preview bypass support
previewBypass: {
queryParams: ['preview', 'draft'],
},
})URL rewriting follows standard 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.jpgAPI_PREFIX
The /~api path prefix used by the CMS Assets API proxy. Exported for use in custom integrations.
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 — Contentful space ID. Required for correct asset path stripping. When provided, the space ID segment is removed from the proxy URL path (matching the proxy worker's configured origin).
// 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 strippedPrismic
| 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. The proxy forwards them to images.prismic.io (Imgix CDN) where they are processed.
// 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=80Prismic'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)
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:
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
