npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cachely-io/sdk

v0.12.4

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.

Readme

@cachely-io/sdk

Universal JavaScript SDK for Cachely.

Pre-release (0.x). The package follows semver, so any minor version bump (0.9.x0.10.0) may include breaking changes. Pin a specific version in your package.json for 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 setup

Detects 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 (or utils/cachely.ts if your project uses utils/)
  • Vite / generic: cachely.ts

Provider-specific onboarding (Prismic)

For a more guided, CMS-specific setup, use the provider commands. All three forms below are equivalent (since 0.12.0):

# Prismic + Nuxt — all three reach the same rich onboarder
npx @cachely-io/sdk@latest setup --provider prismic --tenant my-project
npx @cachely-io/sdk setup prismic --tenant my-project
npx @cachely-io/sdk init prismic --tenant my-project

Earlier versions (< 0.12.0) silently dropped --provider prismic to the generic helper-only flow. That bug is fixed — the three forms now produce byte-identical output.

What Prismic onboarding does

  1. Detects your project — looks for nuxt.config.ts/js/mjs, @prismicio/client or @prismicio/nuxt in deps, and slicemachine.config.json.
  2. Reads repositoryName from slicemachine.config.json.
  3. Generates composables/useCachelyPrismicClient.ts — the primary integration surface:
    import { createCachelyPrismicClient } from "@cachely-io/sdk/prismic"
    import { repositoryName } from "~/slicemachine.config.json"
    
    export function useCachelyPrismicClient() {
      return createCachelyPrismicClient({
        repositoryName,
        tenant: "my-project",
      })
    }
    On Nuxt 4 layouts, writes to app/composables/. Picks .ts vs .js based on existing folder contents.
  4. On greenfield or trivially-patchable projects, also writes app/prismic/client.js:
    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 })
    And patches config/nuxt/prismic.{js,ts} to add client: "~/app/prismic/client", preserving endpoint: config.repositoryName and any existing linkResolver. With both files in place, usePrismic().client is also routed through Cachely.
  5. On complex existing clients (token, routes, custom fetch, etc.), the file is left untouched and a loud warning + manual patch instructions are printed instead. Re-run with --patch-client to force-attempt the canonical write — the safe-patch path still declines if the file shape is not trivially patchable, in which case delete the file first.
  6. Writes/merges cachely.config.json (records integration, transforms, experiments in addition to provider metadata):
    {
      "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",
      "integration": "both",
      "transforms": false,
      "experiments": false
    }
    Tooling manifest only — your Nuxt runtime does not import it.
  7. Installs @cachely-io/sdk if it's not already in package.json.
  8. Does not modify nuxt.config.{ts,js} — prints the snippet to add runtimeConfig.public.cachelyProxyUrl instead. Same for the cachely: { tenant } block when --experiments is enabled.
  9. Does not generate composables/useCachely.ts — that generic helper was the source of confusion in earlier versions and is no longer emitted by the Prismic onboarder. Run plain npx @cachely-io/sdk setup (no provider) if you specifically want the lower-level fetch helper.
  10. No auth token needed — Prismic public repos require no access token.

Useful Prismic flags

| Flag | Default | What it does | |---|---|---| | --composables / --patch-client / --no-patch-client | (inferred via --yes heuristic) | Pick integration mode. Combine first two for 'both'. | | --transforms / --no-transforms | off | Write docs/cachely/transforms.md documenting cachely: { transform }. | | --experiments / --no-experiments | off | Wire useCachelyExperiments() into the composable; print Nuxt-module snippet. | | --custom-domain / --no-custom-domain | off | Emit customDomain: true in the client. |

--patch-client --experiments without --composables is not recommended. The canonical app/prismic/client.js uses createCachelyFetch which doesn't accept an experiments tracker, so per-response assignment-header capture won't work. Use --composables or --composables --patch-client instead.

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 dev

In 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 confirm config/nuxt/prismic.js contains client: "~/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.js with createCachelyFetch.
  • 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-project

Prints 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

  1. Confirms contentful is in your dependencies.
  2. Detects your framework and reads CTF_SPACE_ID (or spaceId / space) from config/contentfulConfig.json or contentfulConfig.json if present.
  3. Scans the project (skipping node_modules, build output, and VCS dirs) for the file that configures your Contentful client — one that imports contentful and calls createClient(...) with space + accessToken.
  4. If exactly one such file is found, it is auto-patched to route through Cachely — preserving every existing key:
    • adds const { createContentfulCachelyAdapter } = require('@cachely-io/sdk/contentful') (or the ESM import) after the last existing import,
    • adds const cachelyProxyUrl = process.env.CACHELY_PROXY_URL,
    • adds a conditional adapter to your createClient config:
      const config = {
        space, accessToken, host,
        ...(cachelyProxyUrl
          ? { adapter: createContentfulCachelyAdapter({ proxyUrl: cachelyProxyUrl }) }
          : {}),
      }
  5. Anything ambiguous (multiple client files), already-customized (an existing adapter: key), or otherwise unsafe (mixed CJS+ESM imports, single-line config) is left untouched — the candidate list or manual instructions are printed instead.
  6. Writes/merges cachely.config.json with provider: "contentful", framework, spaceId, apiOrigin, previewApiOrigin.
  7. Installs @cachely-io/sdk if not already in dependencies.

The wiring is env-gated: requests route through Cachely only when CACHELY_PROXY_URL is set; without it the client talks to Contentful directly (unchanged behavior). Preview vs. production routing is still controlled by Contentful's existing host field (cdn.contentful.com vs preview.contentful.com). The same auto-wiring runs for a plain npx @cachely-io/sdk setup (auto-detected provider) — pass --no-wire to skip it.

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 storyblok

These 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 / createGenericProvider calls. The runtime registry currently resolves only provider: "contentful" and provider: "prismic"; calling createCachelyFetch({ provider: "sanity" }) or "storyblok" would throw Unknown 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, storyblok

Manual

npm install @cachely-io/sdk
# or: pnpm add @cachely-io/sdk
# or: yarn add @cachely-io/sdk
# or: bun add @cachely-io/sdk

Then 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/entries

Prismic

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/client

customDomain — 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 URL | |---|---| | Browser, any hostname | relative /~api/... (resolved by the browser against the current origin) | | SSR with NUXT_PUBLIC_SITE_URL or CACHELY_PUBLIC_SITE_URL set | https://{env}/~api/... | | SSR without either env var | relative /~api/... — Node's fetch raises Invalid URL; set the env var or pass cachelyEndpoint | | customDomain: false (explicit) | https://{tenant}.cachely.io/~api/... |

customDomain: true is opt-in and is the user's signal that their origin already routes /~api/* through the Cachely Worker (custom-domain website-proxy, or the Cachely Nuxt module in dev). The SDK never falls back to https://{tenant}.cachely.io once customDomain: true is set.

SSR: Node's fetch cannot resolve a relative URL. For SSR, set NUXT_PUBLIC_SITE_URL (or CACHELY_PUBLIC_SITE_URL) to the public origin of your deployment, or pass a cachelyEndpoint function that returns the per-request origin (see below).

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 for customDomain: true. New code should use customDomain.

Contentful SDK version support

createContentfulCachelyAdapter (and the lower-level createContentfulAdapter) target the Axios-adapter contract shared by every contentful JS SDK from v7 through v11+ — they never import contentful themselves, so they are version-agnostic, and createContentfulCachelyAdapter is the path npx @cachely-io/sdk setup wires for you. The optional peer range is contentful: ">=7", so projects on Contentful v7 (e.g. [email protected]) install cleanly without --legacy-peer-deps.

// What `setup` wires into your existing Contentful client (env-gated):
const { createContentfulCachelyAdapter } = require('@cachely-io/sdk/contentful')
const cachelyProxyUrl = process.env.CACHELY_PROXY_URL
const config = {
  space, accessToken, host,
  ...(cachelyProxyUrl ? { adapter: createContentfulCachelyAdapter({ proxyUrl: cachelyProxyUrl }) } : {}),
}

The high-level createCachelyContentfulClient helper at @cachely-io/sdk/contentful is typed against the chainable v10+ client API. It still runs on older versions (chain modifiers are duck-typed), but for v7 projects prefer createContentfulCachelyAdapter (or createCachelyFetch) — which is what the CLI generates.

Truly ancient pre-Axios bundles — use createCachelyFetch directly

Only the pre-Axios UMD bundles (e.g. contentful.node.js shipped with Nuxt 2 / Vue 2 vintage projects) lack the adapter hook entirely. If you hit adapter errors there, 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.jpg

URL 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.io

Per-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:

  1. options.proxyUrl (explicit override)
  2. process.env.CACHELY_PROXY_URL
  3. process.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/useCachelyPrismicClient.ts (generated by `setup --provider prismic --experiments`)
import { createCachelyPrismicClient } from '@cachely-io/sdk/prismic'
import { repositoryName } from '~/slicemachine.config.json'

export function useCachelyPrismicClient() {
  // Inside the composable body — never at module scope. The Nuxt plugin
  // attaches a fresh per-request tracker via useNuxtApp().
  const experiments = useCachelyExperiments() // auto-imported by the module
  return createCachelyPrismicClient({
    repositoryName, tenant: 'my-project', experiments,
  })
}
<!-- pages/index.vue — no SSR plumbing in product code -->
<script setup>
const client = useCachelyPrismicClient()
const { data } = await useAsyncData('home', () => client.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: transform must 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: transform enables AI transformations on API responses. preview=1 is a separate parameter that bypasses API caching for draft content previews. The SDK never adds preview=1 automatically.

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: true

classifyUrl(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 → true

ProviderRegistry

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.jpg

API_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 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-project.cachely.io/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 {
  // 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