@cachely-io/sdk
v0.11.6
Published
Universal JavaScript SDK for Cachely — provider-aware request classification, URL rewriting, and fetch adapter for routing your CMS API and assets through the edge. Includes `npx @cachely-io/sdk setup` for one-command project integration.
Downloads
2,563
Maintainers
Readme
@cachely-io/sdk
Universal JavaScript SDK for Cachely.
Pre-release (0.x). The package follows semver, so any minor version bump (
0.9.x→0.10.0) may include breaking changes. Pin a specific version in yourpackage.jsonfor production, e.g."@cachely-io/sdk": "0.9.0"rather than"^0.9.0". The 1.0 release will commit to API stability — until then, treat this SDK as production-usable but not API-frozen.
Routes CMS API and asset requests through your Cachely tenant — 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
Generic setup (recommended for most projects)
npx @cachely-io/sdk setupDetects your framework and CMS, generates a small helper file, adds CACHELY_PROXY_URL to .env.example, and installs the SDK.
Useful flags:
--yes— non-interactive, accept all detected defaults--dry-run— print every action without writing or installing--tenant <slug>— bake your Cachely tenant slug into the helper--proxy-url <url>— full proxy URL (e.g.https://my-project.cachely.io); also seeds.env.example--provider <id>—prismic·contentful·sanity·storyblok·shopify·generic--framework <id>—next·nuxt·vite·generic--dir <path>— project root (default: cwd)
Generated helper locations:
- Next:
lib/cachely.ts - Nuxt:
composables/useCachely.ts(orutils/cachely.tsif your project usesutils/) - Vite / generic:
cachely.ts
Provider-specific onboarding (Prismic)
For a more guided, CMS-specific setup, use the provider commands. Currently, provider-specific onboarding is available for Prismic only.
# Prismic + Nuxt
npx @cachely-io/sdk@latest init prismic --tenant my-project
# or equivalently:
npx @cachely-io/sdk setup prismic --tenant my-projectImportant distinction:
setup --provider prismic→ generic helper-generation flowsetup prismic/init prismic→ Prismic-specific onboarding (different outputs)
What Prismic onboarding does
- Detects your project — looks for
nuxt.config.ts/js/mjs,@prismicio/clientor@prismicio/nuxtin deps, andslicemachine.config.json. - Reads
repositoryNamefromslicemachine.config.json. - Creates
app/prismic/client.jswired to Cachely:import { createClient } from "@prismicio/client" import { createCachelyFetch } from "@cachely-io/sdk" import { repositoryName } from "~/slicemachine.config.json" const cachelyFetch = createCachelyFetch({ tenant: "my-project", provider: "prismic" }) export default createClient(repositoryName, { fetch: cachelyFetch }) - Patches
config/nuxt/prismic.{js,ts}to addclient: "~/app/prismic/client", preservingendpoint: config.repositoryNameand any existinglinkResolver. - Writes/merges
cachely.config.json:{ "provider": "prismic", "framework": "nuxt", "repositoryName": "your-repo", "apiOrigin": "https://your-repo.cdn.prismic.io", "cachelyEndpoint": "https://my-project.cachely.io/~api/api/v2", "recommendedClient": "~/app/prismic/client" }cachely.config.jsonis a tooling manifest for the CLI and future tooling. Your Nuxt runtime does not import it. - Installs
@cachely-io/sdkif it's not already inpackage.json(app/prismic/client.jsimports from it). - No auth token needed — Prismic public repos require no access token.
Why a client file, not endpoint replacement?
Earlier versions rewrote the Prismic client to a long-form Cachely URL (https://<tenant>.cachely.io/~api/api/v2). That broke @nuxtjs/prismic and @prismicio/client@7+ with "A repository name is required for this method but one could not be inferred from the provided API endpoint."
clientConfig: { fetch: cachelyFetch } placed inline in the Nuxt prismic config also doesn't work: @nuxtjs/prismic reads clientConfig from useRuntimeConfig().public.prismic, and Nuxt strips functions when serializing runtimeConfig for SSR→client hydration. The fetch arrives at the runtime plugin as undefined.
The user-file path (client: "~/app/prismic/client") loads the client from a build-time virtual import, so the fetch function survives. This is the only mechanism that works for usePrismic().client — and it works without editing any pages.
After Prismic onboarding
rm -rf .nuxt
npm run devIn the startup log, look for:
ℹ Using user-defined `client` at `~/app/prismic/client.js`Then in DevTools → Network you should see requests like:
https://<tenant>.cachely.io/~api/api/v2/documents/search?...not https://<repo>.cdn.prismic.io/api/v2/....
Asset URLs inside API JSON should be rewritten from https://images.prismic.io/... to:
https://<tenant>.cachely.io/<repositoryName>/...Troubleshooting Nuxt + Prismic
- Still seeing
cdn.prismic.io: delete.nuxt, restart dev, and confirmconfig/nuxt/prismic.jscontainsclient: "~/app/prismic/client". - Seeing "A repository name is required...": you used endpoint replacement. Keep the Prismic endpoint as the repository name and use
app/prismic/client.jswithcreateCachelyFetch. - Asset URLs still point at
images.prismic.io: first verify API requests go through Cachely, then check the project's API URL rewrite setting and logs.
Dry-run mode
npx @cachely-io/sdk init prismic --dry-run --tenant my-projectPrints every planned file change without writing anything.
Provider-specific onboarding (Contentful)
npx @cachely-io/sdk setup contentful --tenant <slug>
# or equivalently:
npx @cachely-io/sdk init contentful --tenant <slug>What Contentful onboarding does
- Confirms
contentfulis in yourdependencies. - Detects your framework and reads
CTF_SPACE_ID(orspaceId/space) fromconfig/contentfulConfig.jsonorcontentfulConfig.jsonif present. - Locates a Contentful client file under
plugins/,services/,lib/,utils/, orsrc/{lib,services}/. - If the file matches the simple safe shape (a single
createClient(<bare-identifier>)call with no existingadapter:key), it is auto-patched to:- add
const { createContentfulAdapter } = require('@cachely-io/sdk')(or the ESMimportequivalent) after the last existing import, - introduce
const cachelyAdapter = createContentfulAdapter({ tenant, spaceId })above the config object, - add
adapter: cachelyAdapter,inside the config object — preserving every existing key.
- add
- Anything more custom (multiple
createClientcalls, an existingadapter:key, mixed CJS+ESM imports, single-line config objects) is left untouched and a manual-instructions block is printed instead. - Writes/merges
cachely.config.jsonwithprovider: "contentful",framework,spaceId,apiOrigin,previewApiOrigin. - Installs
@cachely-io/sdkif not already independencies.
The generated snippet uses the shipped createContentfulAdapter({ tenant, spaceId }) signature only. Preview vs. production routing is controlled by Contentful's existing host field (cdn.contentful.com vs preview.contentful.com); the adapter does not need a preview flag.
Provider-specific onboarding (Sanity, Storyblok — detect + status-only)
npx @cachely-io/sdk setup sanity # also: init sanity
npx @cachely-io/sdk setup storyblok # also: init storyblokThese two providers are intentionally detect + status-only in this release:
- They detect the relevant dependencies and best-effort extract project metadata into
cachely.config.json. - They do not modify any client file.
- They do not install
@cachely-io/sdk— there is no runtime snippet to import yet. - They do not print any executable copy-paste snippet, including no
createCachelyFetch/createGenericProvidercalls. The runtime registry currently resolves onlyprovider: "contentful"andprovider: "prismic"; callingcreateCachelyFetch({ provider: "sanity" })or"storyblok"would throwUnknown provider. Until first-class Sanity/Storyblok routing ships in a future SDK release, the message points to docs and otherwise stays silent.
Re-running setup sanity / setup storyblok after a future SDK release will pick up runtime support automatically.
Listing available provider commands
npx @cachely-io/sdk init
# Please choose a provider: prismic, contentful, sanity, storyblokManual
npm install @cachely-io/sdk
# or: pnpm add @cachely-io/sdk
# or: yarn add @cachely-io/sdk
# or: bun add @cachely-io/sdkThen write your own helper using createCachelyFetch / createCachelyUrlRewriter — see Quick start below.
How it works
Cachely sits between your app and your CMS. Instead of calling Contentful, Prismic, Sanity, or any other CMS directly, your requests go through your project's edge domain:
Your app
→ Cachely edge (https://my-project.cachely.io)
→ Contentful / Prismic / Sanity / ...@cachely-io/sdk 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 { createCachelyFetch } from '@cachely-io/sdk'
import { createClient } from 'contentful'
const cachelyFetch = createCachelyFetch({
tenant: 'my-project', // your Cachely project 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: cachelyFetch,
})
// All API calls go through the proxy automatically:
// https://cdn.contentful.com/spaces/abc123xyz/entries
// → https://my-project.cachely.io/~api/spaces/abc123xyz/entriesPrismic
import { createCachelyFetch } from '@cachely-io/sdk'
import * as prismic from '@prismicio/client'
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
})
const client = prismic.createClient('my-repo', {
fetch: cachelyFetch,
})
// All API calls go through the proxy automatically:
// https://my-repo.cdn.prismic.io/api/v2/documents/search?ref=...
// → https://my-project.cachely.io/~api/api/v2/documents/search?ref=...Prismic + AI Transform
import { createCachelyFetch } from '@cachely-io/sdk'
import { createClient } from '@prismicio/client'
import { repositoryName } from '~/slicemachine.config.json'
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
transform: 'czech', // activates AI Transform profile "czech"
})
export default createClient(repositoryName, {
fetch: cachelyFetch,
})
// API calls include the transform profile automatically:
// https://my-repo.cdn.prismic.io/api/v2/documents/search?ref=abc
// → https://my-project.cachely.io/~api/api/v2/documents/search?ref=abc&transform=czech
//
// The profile "czech" must match an ACTIVE AI Transform rule in your Cachely dashboard.
// Asset URLs are NOT affected — transform is only applied to /~api proxy requests.Per-request transform with .with()
Need a different transform profile per route? Don't construct multiple clients —
the fetch returned by createCachelyFetch is chainable.
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
})
// Scoped fetches — none mutate the original.
const czechFetch = cachelyFetch.with({ transform: 'czech' })
const serbianFetch = cachelyFetch.with({ transform: 'serbian' })
const noTransform = cachelyFetch.with({ transform: undefined }) // explicit no-op scope
// Pick one per route at request time:
const f = route.query.transform
? cachelyFetch.with({ transform: route.query.transform })
: cachelyFetch.with() always replaces the transform of the current scope. transform: undefined
(or null, false, '') is an explicit "no transform" scope — it does not inherit
the parent scope's value. All other base options (tenant, provider, providerConfig,
enableApiProxy, enableAssetProxy, transformWhen) are preserved verbatim across
.with().
Prismic adapter (per-request transform)
Skip the boilerplate of constructing a custom fetch — the Prismic adapter wraps
@prismicio/client and lets you pick a transform profile per call.
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
const client = createCachelyPrismicClient({
repositoryName: 'my-repo',
tenant: 'my-project',
transform: 'czech', // optional default, overridden per-call below
})
const home = await client.getSingle('page_home', {
fetchLinks: ['page.title', 'page.body'],
cachely: { transform: route.query.transform ?? null },
// ^^^^^ null (or undefined / false)
// disables transform for this call
})The cachely namespace is stripped before the params reach @prismicio/client, so it
never appears in the outbound URL. If cachely is the only key in params, the params
argument is dropped entirely. Underlying Prismic clients are cached per transform
profile, so calling the same profile repeatedly does not recreate the client.
@prismicio/client is an optional peer dependency — install it in your app:
npm install @prismicio/clientcustomDomain — route through your Cachely-connected domain
By default every request goes to https://{tenant}.cachely.io/~api/.... When your
Cachely project has a custom domain with the website proxy enabled, the proxy
already serves your site — set customDomain: true to route requests through the
current origin instead of the tenant subdomain:
const client = createCachelyPrismicClient({
repositoryName: 'my-repo',
tenant: 'my-project',
customDomain: true,
})Resolution (when no cachelyEndpoint is provided):
| Environment | Effective API origin |
|---|---|
| Browser, public hostname (https://my-site.com) | window.location.origin |
| Browser, localhost / 127.x / [::1] / private LAN (10.x, 192.168.x, 172.16-31.x) / *.local | safe fallback: https://{tenant}.cachely.io |
| SSR with NUXT_PUBLIC_SITE_URL or CACHELY_PUBLIC_SITE_URL set | that env value |
| SSR without either env var | safe fallback: https://{tenant}.cachely.io |
| customDomain: false (explicit) | https://{tenant}.cachely.io |
Why localhost falls back: in npm run dev your site is http://localhost:3011,
which is not behind the Cachely Worker. The SDK would otherwise rewrite API requests
to http://localhost:3011/~api/... and 404. The fallback keeps dev runs working with
zero configuration.
Why SSR has its own fallback: Node's fetch rejects relative URLs with
TypeError: Invalid URL. The SDK never produces a relative URL — if no per-request
origin can be resolved, it uses the tenant domain, which always works.
Nuxt SSR: per-request origin via useRequestURL()
If you want SSR-rendered HTML to point at the custom domain (instead of the
tenant fallback) without setting NUXT_PUBLIC_SITE_URL, pass a cachelyEndpoint
resolver that reads useRequestURL(). The wizard generates this automatically
for customDomain: true:
// app/prismic/client.js (generated by `npx @cachely-io/sdk init prismic --custom-domain`)
import { createClient } from "@prismicio/client"
import { createCachelyFetch } from "@cachely-io/sdk"
import { repositoryName } from "~/slicemachine.config.json"
const cachelyFetch = createCachelyFetch({
tenant: "my-project",
provider: "prismic",
customDomain: true,
cachelyEndpoint: () => {
// SSR: per-request origin from Nuxt's auto-imported useRequestURL().
// Returning undefined hands control back to the SDK's customDomain logic.
if (typeof window === "undefined") {
try {
const u = useRequestURL()
return u?.origin
} catch {
return undefined
}
}
return undefined
},
})
export default createClient(repositoryName, { fetch: cachelyFetch })cachelyEndpoint (explicit override)
Pass cachelyEndpoint to bypass everything above. Both forms are accepted —
the SDK normalizes them to the origin, so the path never duplicates:
| Input | Effective base |
|---|---|
| 'https://my-site.com' | https://my-site.com |
| 'https://my-site.com/~api/api/v2' | https://my-site.com (path stripped) |
| () => 'https://my-site.com' | https://my-site.com |
| () => undefined | falls through to customDomain logic |
const client = createCachelyPrismicClient({
repositoryName: 'my-repo',
tenant: 'my-project',
cachelyEndpoint: 'https://my-site.com',
// or function form for per-request resolution:
// cachelyEndpoint: () => useRuntimeConfig().public.siteUrl,
})
delivery: "same-origin"is the legacy form, kept as a backward-compatible alias forcustomDomain: true. New code should usecustomDomain.
Older Contentful SDKs — use createCachelyFetch directly
The packaged createContentfulAdapter is shaped for the modern contentful JS SDK (11.x and similar Axios-adapter contracts). If you're integrating with an older Contentful SDK (e.g. the bundle named contentful.node.js shipped with Nuxt 2 / Vue 2 vintage projects) and run into adapter errors, fall back to passing createCachelyFetch directly:
import * as contentful from 'contentful'
import { createCachelyFetch } from '@cachely-io/sdk'
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'contentful',
providerConfig: { spaceId: 'abc123xyz' },
})
const client = contentful.createClient({
space: 'abc123xyz',
accessToken: 'proxy-injected', // any truthy string — real token is injected server-side
// Older Contentful SDKs accept a custom fetch function instead of an adapter.
// Some versions read fetch off `globalThis` instead — set it before calling createClient:
// globalThis.fetch = cachelyFetch
})createCachelyFetch is a plain Fetch-API function and avoids the Axios/adapter shape entirely, so it dodges adapter contract mismatches in older SDK versions.
Any CMS via the generic provider
import { createGenericProvider, createCachelyFetch } from '@cachely-io/sdk'
const storyblok = createGenericProvider({
id: 'storyblok',
apiHosts: ['api.storyblok.com'],
assetHosts: ['a.storyblok.com'],
})
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: storyblok, // pass ProviderDefinition directly — no registry needed
})
// https://api.storyblok.com/v2/stories → https://my-project.cachely.io/~api/v2/stories
// https://a.storyblok.com/f/123/hero.jpg → https://my-project.cachely.io/f/123/hero.jpgURL rewriting without fetch
Use createCachelyUrlRewriter when you need to transform URLs without intercepting fetch — useful for server-side response body processing, logging, or building custom adapters.
import { createCachelyUrlRewriter } from '@cachely-io/sdk'
const rewrite = createCachelyUrlRewriter({
tenant: 'my-project',
provider: 'contentful',
providerConfig: { spaceId: 'abc123xyz' },
})
// API URL
rewrite('https://cdn.contentful.com/spaces/abc123xyz/entries?locale=en')
// → { url: 'https://my-project.cachely.io/~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-project.cachely.io/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 }Response transformer
When you fetch a CMS response on the server and want to rewrite the asset URLs inside the response body (not just intercept fetch calls), use the response transformer. It walks any JSON-serializable value, replacing CMS asset hosts with your Cachely proxy base.
The transformer is part of @cachely-io/sdk — same one install.
Quick start
import { transformAssetUrls, getCachelyProxyBase } from '@cachely-io/sdk'
// CACHELY_PROXY_URL=https://my-project.cachely.io (in your env)
const data = await fetch('https://cdn.contentful.com/spaces/abc/entries').then(r => r.json())
const transformed = transformAssetUrls(data, {
cms: 'contentful',
spaceId: 'abc',
// proxyUrl read from CACHELY_PROXY_URL env var; pass `proxyUrl` to override
})
// Every Contentful asset URL inside `data` is rewritten to my-project.cachely.ioPer-CMS exports
import {
transformPrismicAssetUrls,
transformContentfulAssetUrls,
transformSanityAssetUrls,
transformShopifyAssetUrls,
transformCloudinaryAssetUrls,
transformImgixAssetUrls,
transformGenericAssetUrls,
} from '@cachely-io/sdk'
// Prismic
const out = transformPrismicAssetUrls(data, {
repository: 'my-repo',
proxyUrl: 'https://my-project.cachely.io',
})
// Contentful
const out2 = transformContentfulAssetUrls(data, {
spaceId: 'abc123xyz',
proxyUrl: 'https://my-project.cachely.io',
})getCachelyProxyBase(options)
Resolve the proxy base URL with the same precedence rules the transformer uses:
options.proxyUrl(explicit override)process.env.CACHELY_PROXY_URLprocess.env.CMS_ASSETS_URL— temporary launch-window fallback for projects migrating from the legacy env var
import { getCachelyProxyBase } from '@cachely-io/sdk'
const base = getCachelyProxyBase()
// 'https://my-project.cachely.io/' (with trailing slash, query/hash stripped)Options reference
Every per-CMS function accepts the common shape plus its required keys:
type CommonOptions = {
proxyUrl?: string // override the env-var resolution
envVarName?: string // default: 'CACHELY_PROXY_URL'
env?: Record<string, string | undefined> // default: process.env
nodeEnv?: string // default: process.env.NODE_ENV
forceHttpsInProduction?: boolean // default: true
validateRequiredOptions?: boolean // default: true
throwOnError?: boolean // default: false
transformers?: StringTransformer[] // extra transformers run after the built-ins
postTransform?: (parsed) => any // optional final pass on the parsed object
onError?: (err) => void // called when an error is swallowed
}Required keys per CMS:
| cms | Required option |
|---|---|
| prismic | repository: string |
| contentful | spaceId: string |
| sanity | projectId: string (dataset?: string, default 'production') |
| shopify | storeDomain: string |
| cloudinary | cloudName: string |
| imgix | imgixDomain: string |
| generic | originUrl: string |
AI Experiments — client and SSR
Cachely's AI Experiments pipeline emits four headers on every proxied response (x-cachely-experiment-id, x-cachely-variant-id, x-cachely-variant-key, x-cachely-experiment-profile). The SDK ships a tracker that captures those headers, persists the assignment, and lets you fire conversion events.
Client-only flow (no SSR)
import {
createCachelyExperimentTracker,
createCachelyPrismicClient,
} from '@cachely-io/sdk/prismic'
export const experiments = createCachelyExperimentTracker({ tenant: 'my-project' })
export const prismic = createCachelyPrismicClient({
repositoryName: 'my-repo',
tenant: 'my-project',
experiments, // wraps the underlying fetch — same response that renders content carries the assignment
})
// Later, in a browser-only place:
experiments.autoTrack() // attaches a delegated click handler for [data-cachely-track]<button data-cachely-track="hero_cta_click" data-cachely-meta-cta="hero_primary">Get started</button>For multi-experiment pages, add data-cachely-experiment-id="N" so the click is attributed to the right experiment.
SSR primitives — the framework-agnostic surface
When the response that carries the assignment is fetched on the server (e.g. inside Nuxt's useAsyncData, a Next.js RSC, or an Astro page), the browser never sees those headers directly. The SDK exposes four primitives that bridge the SSR boundary without making a second "probe" request:
| Primitive | Where it runs | What it does |
|---|---|---|
| tracker.dehydrate() | server | snapshot every captured assignment to a JSON-safe object |
| serializeSnapshotScript(snapshot) | server | render an inert <script type="application/json"> tag for the HTML |
| readSnapshotFromDom() | client | read the same tag back, returning the snapshot |
| tracker.hydrate(snapshot) | client | merge the snapshot into the client tracker |
These four primitives are sufficient to integrate SSR experiments with any framework. Optional framework adapters (such as @cachely-io/sdk/nuxt below) are thin conveniences built on top of them — nothing in the core tracker knows about a specific framework.
Security: serializeSnapshotScript always escapes </ → <\/ and U+2028 / U+2029, and renders the script as type="application/json" (an inert data island that CSP script-src treats as data, not code).
Multi-experiment safety: the snapshot carries the full assignmentsById map. hydrate() is last-write-wins per experimentId and preserves assignments not present in the snapshot.
SSR recipe — generic (Express + template)
// server.ts
import {
createCachelyExperimentTracker,
serializeSnapshotScript,
} from '@cachely-io/sdk'
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
app.get('/', async (req, res) => {
const experiments = createCachelyExperimentTracker({ tenant: 'my-project' })
const prismic = createCachelyPrismicClient({
repositoryName: 'my-repo', tenant: 'my-project', experiments,
})
const home = await prismic.getSingle('page_home')
const { scriptTag } = serializeSnapshotScript(experiments.dehydrate())
res.send(renderTemplate({ home, cachelyScript: scriptTag })) // inject before </body>
})// client.ts (bundled separately)
import { createCachelyExperimentTracker, readSnapshotFromDom } from '@cachely-io/sdk'
const tracker = createCachelyExperimentTracker({ tenant: 'my-project' })
tracker.hydrate(readSnapshotFromDom())
tracker.autoTrack()SSR recipe — Next.js App Router
// app/page.tsx (server component)
import { createCachelyExperimentTracker, serializeSnapshotScript } from '@cachely-io/sdk'
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
import { CachelyClientBoot } from './_components/CachelyClientBoot'
export default async function HomePage() {
const experiments = createCachelyExperimentTracker({ tenant: 'my-project' })
const prismic = createCachelyPrismicClient({
repositoryName: 'my-repo', tenant: 'my-project', experiments,
})
const home = await prismic.getSingle('page_home')
const { json } = serializeSnapshotScript(experiments.dehydrate())
return (
<>
<HomeView data={home} />
<script
type="application/json"
id="cachely-experiments"
dangerouslySetInnerHTML={{ __html: json }}
/>
<CachelyClientBoot />
</>
)
}'use client'
// app/_components/CachelyClientBoot.tsx
import { useEffect } from 'react'
import { createCachelyExperimentTracker, readSnapshotFromDom } from '@cachely-io/sdk'
export function CachelyClientBoot() {
useEffect(() => {
const t = createCachelyExperimentTracker({ tenant: 'my-project' })
t.hydrate(readSnapshotFromDom('cachely-experiments'))
t.autoTrack()
}, [])
return null
}SSR recipe — Astro
---
// src/pages/index.astro
import { createCachelyExperimentTracker, serializeSnapshotScript } from '@cachely-io/sdk'
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
const experiments = createCachelyExperimentTracker({ tenant: 'my-project' })
const prismic = createCachelyPrismicClient({
repositoryName: 'my-repo', tenant: 'my-project', experiments,
})
const home = await prismic.getSingle('page_home')
const { scriptTag } = serializeSnapshotScript(experiments.dehydrate())
---
<Layout>
<HomeView data={home} />
<Fragment set:html={scriptTag} />
<script>
import { createCachelyExperimentTracker, readSnapshotFromDom } from '@cachely-io/sdk'
const t = createCachelyExperimentTracker({ tenant: 'my-project' })
t.hydrate(readSnapshotFromDom('cachely-experiments'))
t.autoTrack()
</script>
</Layout>SSR recipe — Nuxt module (convenience adapter)
For Nuxt projects, install the auto-loading module instead of wiring dehydrate / hydrate by hand. Under the hood it calls the same four primitives.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@cachely-io/sdk/nuxt'],
cachely: { tenant: 'my-project' },
})// composables/usePrismic.ts
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
export function usePrismicClient() {
const experiments = useCachelyExperiments() // auto-imported by the module
return createCachelyPrismicClient({
repositoryName: 'my-repo', tenant: 'my-project', experiments,
})
}<!-- pages/index.vue — no SSR plumbing in product code -->
<script setup>
const prismic = usePrismicClient()
const { data } = await useAsyncData('home', () => prismic.getSingle('page_home'))
</script>
<template>
<button data-cachely-track="hero_cta_click" data-cachely-meta-cta="hero_primary">
Get started
</button>
</template>The module creates one tracker per request, dumps tracker.dehydrate() into useState('cachely:experiments') on app:rendered, and on the client reads the same useState, calls tracker.hydrate(...), then tracker.autoTrack(). No page-level boilerplate, no probe requests, no manual Headers/Response reconstruction. Opt out with cachely: { autoInstallPlugin: false } if you want to wire the primitives yourself.
API reference
createCachelyFetch(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 cachelyFetch = createCachelyFetch({
tenant: 'my-project', // required — Cachely project 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)
transform: 'czech', // optional — AI Transform profile name (see below)
})AI Transform:
// Activate an AI Transform profile — appends ?transform=<profile> to all proxy API requests.
// The profile must match an ACTIVE AI Transform rule in your Cachely dashboard.
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
transform: 'czech', // activates AI Transform profile "czech"
})
// Pass false, null, or omit to disable (current behavior — no transform applied)
const cachelyFetch2 = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
transform: false, // no AI transform
})Note — profile must be active:
transformmust match a profile name with at least one active AI Transform rule in your Cachely dashboard. Requests with an unknown or inactive profile are served without transformation.Note — separate from preview bypass:
transformenables AI transformations on API responses.preview=1is a separate parameter that bypasses API caching for draft content previews. The SDK never addspreview=1automatically.
Per-request control with transformWhen:
// Skip transform for specific routes/queries — runs after the provider's
// own routing decision. Both must return true for transform= to be appended.
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
transform: 'czech',
transformWhen: ({ url, request }) => {
// Example: never transform admin queries
if (url.searchParams.get('admin') === '1') return false
return true
},
})The transformWhen callback receives { url, request } where url is the
original URL (before proxy rewrite) and request is the original
Request object if one was passed (otherwise undefined). Return false
to suppress the transform= parameter for that request.
Provider-specific safety — Prismic:
The Prismic provider knows that the root metadata endpoint (/api/v2) must
never receive transform= — that endpoint returns the refs array used
by @prismicio/client for all subsequent content queries. AI-transforming
that response would break ref discovery and every page would fail with
"refs is not iterable". The SDK applies transform= only to
/documents/search requests for provider: 'prismic'.
This means you can drop the transform option in directly:
import { createClient } from '@prismicio/client'
import { createCachelyFetch } from '@cachely-io/sdk'
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
transform: 'czech',
})
// Use the standard Prismic client — transform is applied transparently
// only to content queries, never to repository metadata or assets.
export default createClient(repositoryName, { fetch: cachelyFetch })@prismicio/client calls (getSingle, getByUID, getAllByType, SliceZone,
etc.) work unchanged. The Prismic predicate q=... is preserved
byte-for-byte through the proxy — the SDK never re-encodes existing query
parameters.
Selective proxying:
// Asset proxy only — API calls go directly to Contentful
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'contentful',
enableApiProxy: false,
})
// API proxy only — images are not proxied
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
provider: 'prismic',
enableAssetProxy: false,
})createCachelyUrlRewriter(options)
Returns a bound (url: string | URL) => RewriteResult function. Same options as createCachelyFetch.
import { createCachelyUrlRewriter } from '@cachely-io/sdk'
const rewrite = createCachelyUrlRewriter({
tenant: 'my-project',
provider: 'prismic',
})
const { url, kind, rewritten } = rewrite('https://images.prismic.io/my-repo/hero.jpg?w=800&fm=webp')
// url: 'https://my-project.cachely.io/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 '@cachely-io/sdk'
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 '@cachely-io/sdk'
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-project', { spaceId: 'abc' })
// result.url → 'https://my-project.cachely.io/~api/spaces/abc/entries?locale=en'
// result.kind → 'api'
// result.rewritten → trueProviderRegistry
Register custom or extended provider definitions.
import { ProviderRegistry, contentful, prismic, createCachelyFetch } from '@cachely-io/sdk'
const registry = new ProviderRegistry()
.register(contentful)
.register(prismic)
const cachelyFetch = createCachelyFetch({
tenant: 'my-project',
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 '@cachely-io/sdk'
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 createCachelyFetch, createCachelyUrlRewriter, classifyUrl, rewriteUrl, and ProviderRegistry.
URL rewriting follows standard Cachely conventions:
API: https://api.storyblok.com/v2/stories → https://{tenant}.cachely.io/~api/v2/stories
Asset: https://a.storyblok.com/f/123/img.jpg → https://{tenant}.cachely.io/f/123/img.jpgAPI_PREFIX
The /~api path prefix used by the Cachely API proxy.
import { API_PREFIX } from '@cachely-io/sdk'
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-project.cachely.io/abc/assetId/token/img.jpg
// With spaceId: 'abc':
// https://images.ctfassets.net/abc/assetId/token/img.jpg
// → https://my-project.cachely.io/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.
// Image optimization is preserved:
// https://images.prismic.io/my-repo/hero.jpg?auto=format,compress&w=920&fm=webp&q=80
// → https://my-project.cachely.io/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 {
// Fetch / rewrite
RequestKind, // 'api' | 'asset' | 'unknown'
ProviderDefinition, // shape of a provider
ProviderConfig, // Record<string, string | undefined>
RewriteResult, // { url, kind, rewritten }
CachelyFetchOptions, // options for createCachelyFetch (includes transform?, transformWhen?)
UrlRewriterOptions, // options for createCachelyUrlRewriter
GenericProviderConfig, // config shape for createGenericProvider
TransformWhenFn, // ({ url, request }) => boolean
// Response transformer
TransformOptions, // shared base options
PrismicTransformOptions,
ContentfulTransformOptions,
SanityTransformOptions,
ShopifyTransformOptions,
CloudinaryTransformOptions,
ImgixTransformOptions,
GenericTransformOptions,
CmsDispatchOptions, // discriminated union for transformAssetUrls
SupportedCms, // 'prismic' | 'contentful' | … | 'generic'
StringTransformer, // (jsonStr, ctx) => jsonStr
TransformerContext, // { base, nodeEnv }
} from '@cachely-io/sdk'Custom providers
Using createGenericProvider (recommended)
The easiest way to add support for any CMS:
import { createGenericProvider, createCachelyFetch } from '@cachely-io/sdk'
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 cachelyFetch = createCachelyFetch({ tenant: 'my-project', provider: sanity })
// Or register and use by id string
import { defaultRegistry } from '@cachely-io/sdk'
defaultRegistry.register(sanity)
const cachelyFetch2 = createCachelyFetch({ tenant: 'my-project', provider: 'sanity' })Using ProviderDefinition directly (advanced)
For full control over rewrite logic, implement the interface manually:
import { ProviderDefinition, defaultRegistry } from '@cachely-io/sdk'
const sanity: ProviderDefinition = {
id: 'sanity',
apiHosts: ['*.api.sanity.io'],
assetHosts: ['cdn.sanity.io'],
rewriteApiUrl(url, tenant) {
return `https://${tenant}.cachely.io/~api${url.pathname}${url.search}`
},
rewriteAssetUrl(url, tenant) {
return `https://${tenant}.cachely.io${url.pathname}${url.search}`
},
}
defaultRegistry.register(sanity)Requirements
- Node.js 18+ (or any runtime with the Fetch API)
- No dependencies
