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

@towa-digital/storyblok-nuxt-cv

v0.5.0

Published

Keeps Storyblok published content fresh on long-running Nuxt servers by bridging the live cache version (cv) to the render client — no redeploy after publish.

Readme

@towa-digital/storyblok-nuxt-cv

Keeps published Storyblok content fresh on long-running Nuxt servers — no redeploy after publish. Packages the aichelin-multisite 1.5.5 storage-bridge fix as a drop-in Nuxt module.

Maintained by TOWA. Design rationale and the full bug analysis live in TOWA's internal storyblok-cv-audit playbook repo (docs/package-design.md, docs/problem.md).

The problem (short version)

storyblok-js-client pins the space cache version (cv) in a process-global map for the process lifetime, and Storyblok's CDN serves a frozen snapshot for every URL warmed under that cv (7-day s-maxage). On client v6 nothing ever heals the pin — published content stays stale until redeploy; on v5 and v7 the pin self-heals on the first cache-missing response, which presents as unpredictable per-worker flapping after every publish. In production builds the render client is a separate bundled copy of the client that Nitro code can't reach, so a plain server-side flushCache() webhook does not fix the render path.

What this module does

  • Nitro middleware attaches the stored live cv to every page render's event.context; an app plugin (compiled into the same bundle as the render client) pins it via useStoryblokApi().setCacheVersion(cv) before content is fetched, and forwards it to the browser through the payload.
  • The stored cv is kept fresh by three composable strategies:
    • startup init (on by default) — populates the store at boot,
    • publish webhook (built-in route, or one line in your existing handler),
    • TTL stale-while-revalidate — for serverless/edge and multi-instance.
  • Works with storyblok-js-client v5 → v7 and @storyblok/nuxt 5 → 9: the server side never imports the client (raw cdn/spaces/me fetch), the app side only uses useStoryblokApi/setCacheVersion, which are stable across all those majors. Keeps Storyblok's cv-keyed CDN caching intact (unlike cache: { cv: 'manual' }).

Install

Requires @storyblok/nuxt (any major from 5 to 9) in the host app.

yarn add @towa-digital/storyblok-nuxt-cv
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    ['@storyblok/nuxt', { accessToken: process.env.STORYBLOK_TOKEN }],
    '@towa-digital/storyblok-nuxt-cv',
  ],
})

That's it for a single-process Node server (DigitalOcean preset etc.): token is auto-detected from the @storyblok/nuxt config, startup init and the built-in webhook route are on by default.

Then create a story publish webhook in Storyblok (Settings → Webhooks, story events) pointing at https://<site>/api/storyblok-cv-webhook, set a secret, and provide it as NUXT_STORYBLOK_CV_WEBHOOK_SECRET.

Configuration

export default defineNuxtConfig({
  storyblokCv: {
    token: undefined,          // default: auto-detected from @storyblok/nuxt config; env NUXT_STORYBLOK_CV_TOKEN
    region: 'eu',              // eu | us | ap | ca | cn
    endpoint: undefined,       // full API endpoint incl. /v2 (like @storyblok/nuxt); overrides region — for proxied spaces
    storage: 'storyblok-cv',   // nitro storage mount name
    fsStore: false,            // true → module mounts an fs store at <cwd>/.data/<storage> at RUNTIME (node/PM2; owner-correct, per-deploy). string = custom base.
    statusRoute: '/api/storyblok-cv',     // GET current cv; false to disable
    webhook: true,             // built-in route /api/storyblok-cv-webhook; string = custom route; false = use refreshStoryblokCv() yourself
    ttl: false,                // seconds, e.g. 60 — background refresh when the stored cv is older (serverless/multi-instance)
    onStartup: true,           // populate the store once at boot
  },
})

Deployment-target matrix (pick your refresh strategy)

| Target | Storage | Strategy | |---|---|---| | Single Node process (DO app platform, plain node-server) | default (memory) | webhook (default setup) | | PM2 cluster on one host (instances: 'max') | fsStore: true (or a manual fs mount) | webhook | | Multi-node / containers | mount Redis | webhook | | Cloudflare / Vercel Edge / Netlify (isolates) | default | ttl (seconds, e.g. 60) — a webhook can't reach every isolate |

On a PM2 cluster memory is per-worker, so a webhook only updates one worker. The simplest correct fix is fsStore — the module mounts an fs store at a path resolved at runtime, inside the app's own directory (owned by the runtime user, unique per deployment):

storyblokCv: { fsStore: true }   // → <cwd>/.data/storyblok-cv  (or fsStore: '/abs/path')

Prefer this over hand-configuring nitro.storage — it's owner-correct and per-deploy, so it sidesteps the gotchas below. Node/PM2 only — serverless/edge have no writable fs, use ttl there.

nitro: { storage: { 'storyblok-cv': { driver: 'fs', base: '/absolute/writable/path' } } }

Use an absolute path writable by the runtime user. A relative base is resolved at build time (it bakes the build/CI cwd, not the server's) — which is exactly what fsStore sidesteps by resolving at runtime. Persist failures are logged as [storyblok-cv] … FAILED to persist.

If unsure, setting ttl in addition to the webhook is a safe belt-and-braces default: staleness is then bounded by the TTL even if webhook delivery breaks.

Integrating with an existing webhook

Most TOWA projects already have a publish webhook (update-redirects.post.ts and friends). Set webhook: false and add one line — refreshStoryblokCv is auto-imported in server code:

// server/api/update-redirects.post.ts (existing handler, after signature check)
await refreshStoryblokCv()

Multi-tenant spaces: refresh on every publish (do not tenant-filter)

Refreshing on every content publish in the space is the right behavior even for multi-tenant shared spaces where only one tenant's content changed — so the default needs no configuration. Filtering out sibling tenants' publishes (as aichelin 1.5.5 originally did) looks like it protects your CDN cache, but it only protects URLs that were already warm: cold fetches get 301-corrected to the latest cv and fill the cache under the new generation anyway, while your pinned client keeps paying that miss+301 hop on every fetch of them until your own next publish. It also serves stale resolved content if tenants share any references or datasources. Adopting the sibling's cv instead costs one origin fetch per URL identity, once.

For genuinely special cases (e.g. origin rate-limit pressure from extremely frequent sibling publishes), the storyblok-cv:webhook nitro hook can veto a refresh: handlers receive { payload, refresh } and may set refresh = false. Prefer throttling over hard filtering if you reach for this.

What this module does NOT solve

  • ISR / page-cache staleness: a fresh cv doesn't invalidate HTML already cached by routeRules: { isr: true } or a CDN page cache in front of the app.
  • URL-shape hygiene: inconsistent trailing slashes in cdn/stories/... paths create a second cache identity per story (flaky staleness). Normalize in the app; see the audit playbook's docs/remediation.md.
  • On client ≥ 7.4.0 (check the lockfile — the option doesn't exist before 7.4.0) you can alternatively set cache: { cv: 'manual' } and skip this module — at the cost of losing Storyblok's cv-keyed CDN caching (every SSR fetch hits origin). This module keeps CDN caching, and covers v5/v6/early-v7 where no such option exists.

Verifying an installation

Don't trust a single "looks fresh" check. Use the lab method from the audit playbook (docs/detection.md Step 7): instrumented production build, publish via form mode (not the visual editor), sample repeatedly, test the warmed URL shape. Quick sanity check in production: GET /api/storyblok-cv before and after a publish — the value must change without a restart.

Development

npm install
npm run dev:prepare   # stub build + playground types
npm run dev           # playground on localhost:3000
npm run lint          # eslint (@nuxt/eslint-config, tooling + stylistic)
npm run test          # vitest e2e against a fixture app + mocked Storyblok API
npm run test:types    # vue-tsc, server and app contexts separately
npm run prepack       # build dist/
npm run release       # local: lint + test + build + changelogen + npm publish

Releasing

CI and releases run on Buddy and are managed in Buddy (GUI/API), not from a committed file — same as @towa-digital/storyblok-nuxt-sitemap. Two pipelines: Test (auto-runs on branch pushes) and Release to npm (manual/click-to-run from main, publishes the package.json version to public npm).

Release flow: bump the version + changelog on main (npx changelogen --release or edit package.json), push, then click Run on the Release to npm pipeline in Buddy. Publish auth comes from a Buddy FILE variable named npmrc (npm auth config) on the project or workspace — $npmrc resolves to its path, copied to .npmrc before publish. Publishing requires the version to be new (npm rejects re-publishing an existing version).