Index of every page on this site: llms.txt
All skills
harlan-zw avatar

/nuxt-skew-protection

@a0fe1bf
Run it yourself

Keep old Nuxt build chunks available after a deploy and prompt open tabs to reload, with the nuxt-skew-protection module. Use when a task mentions version skew, ChunkLoadError, "Failed to fetch dynamically imported module", stale chunks 404 after deploy, update notifications, SkewNotification, useSkewProtection, isClientOutdated, the __nkpv cookie, /__skew routes, or the skewProtection config key.

  • 1 file
  • 11.3 KB
  • MIT
  • Updated last month
  • GitHub
Use this Skill: https://skilld.dev/gh/harlan-zw/nuxt-skew-protection/nuxt-skew-protection

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

SKILL.md

≈106 tokens for metadata: the name and description. ≈2.8k when used: this file.

Description uses 5.3% of example budget

Before choosing a Skill, your Agent reads its name and description. All available Skills share that space.

  • A shorter description leaves more room for other Skills. This entry exceeds our 1% size suggestion.

In our Claude Code example, all Skill names and descriptions share 8,000 characters. This Skill uses ≈423 characters, or 5.3%.

The 1% threshold is a size suggestion. Longer descriptions can still fit.

Your model, settings, and other Skills decide how much text your Agent can read.

Example settings and source

The example uses a 200k-token context and default Claude Code settings. The count includes the name, description, separators, and when_to_use when present. Codex also counts local file paths.

Skit's source and limits: Codex 0.160.1, Claude Code 2.1.292.

nuxt-skew-protection

Tested against nuxt-skew-protection 1.6.2 on Nuxt 4.5.2. At build time the module stores every build's assets, then copies the chunks of earlier builds back into .output/public. In the browser it detects a new deploy and tells you when the chunks the tab loaded are gone. Docs: https://nuxtseo.com/docs/skew-protection

Setup

Included in @nuxtjs/seo v6. If the app uses @nuxtjs/seo, configure the skewProtection key and do not add the module again. Nuxt SEO overrides two defaults: updateStrategy: 'polling' and cookie: false. Set skewProtection: false to remove the module from Nuxt SEO.

Without Nuxt SEO, run npx nuxt module add nuxt-skew-protection. @nuxtjs/robots v7 and nuxt-site-config v5 are optional peers. Install Nuxt Robots to keep bot traffic off SSE and WebSocket connections.

Build history lives in storage. The default is the fs-lite driver at node_modules/.cache/nuxt-seo/skew-protection, relative to the project root. If CI starts from a clean checkout, every build is the first build: the build logs No previous versions found in storage and no old chunk survives. Persist that directory, or configure a shared driver (redis, upstash, cloudflare-kv-binding). On GitHub Actions, give each deploy a new key so the cache saves:

- uses: actions/cache@v4
  with:
    path: node_modules/.cache/nuxt-seo
    key: nuxt-skew-${{ github.sha }}
    restore-keys: nuxt-skew-

Add a notification. Nothing shows until you render <SkewNotification> or call useSkewProtection().

Automatic behaviour

  • Build: stores the assets, restores old chunks into .output/public, and adds skewProtection.versions to /_nuxt/builds/latest.json. Cleanup keeps 10 versions for 30 days (maxNumberOfVersions, retentionDays).
  • Cookie: __nkpv holds the build id, for 7 days, SameSite=Lax. The server sets it only on requests with sec-fetch-dest: document. With app.baseURL: '/app/' the name becomes __nkpv_app.
  • Server context: every request gets event.context.skewVersion from the cookie.
  • Service worker: /_nuxt-skew-sw.js records which chunks the tab loaded.
  • Update strategy: every platform defaults to native Nuxt polling, once per hour. Explicit sse, ws, or adapters enable push. The client subscribes when a component using the composable mounts. Native polling needs appManifest and checkOutdatedBuildInterval enabled.
  • Endpoints: /__skew/health, /__skew/sse, and /__skew/ws, under basePath. With app.baseURL: '/app/' they sit at /app/__skew/*. A static build has none.
  • Multi tab: BroadcastChannel shares complete manifests within the app/base path. Web Locks select one push owner without heartbeats. Closing, hiding, or navigating the owner releases it; visible or restored pages resubscribe unless explicitly disconnected. Unsupported APIs and connectionTracking keep per-tab transports. Set multiTab: false for independent detection. Visible subscribers share VERSION hints without connection identifiers. The owner receives other transport messages and sends commands. Every tab receives onAppOutdated updates. Disable sharing or enable connectionTracking for independent commands.
  • HTML cache capability: when bundleAssets and storage are set, the module publishes runtimeConfig.htmlCacheCapabilities so sibling modules (such as a Cloudflare CDN module) can read how long a cached document safely outlives its build.

Show an update prompt

<SkewNotification> is headless and renders inside <ClientOnly>. Put it in app.vue or a layout.

<template>
  <SkewNotification v-slot="{ isCurrentChunksOutdated, dismiss, reload, timeAgo }">
    <div v-if="isCurrentChunksOutdated" role="status">
      New version released {{ timeAgo }}.
      <button @click="reload">
        Reload
      </button>
      <button @click="dismiss">
        Not now
      </button>
    </div>
  </SkewNotification>
</template>

Pick the slot prop by intent:

  • isCurrentChunksOutdated: the deploy deleted a chunk this tab loaded. Use it for the prompt.
  • isAppOutdated: any new deploy. On a prerendered page this is always false, because the HTML carries a build id from build time.
  • isOpen: either of the two.

dismiss() hides the prompt until the next deploy. reload() calls reloadNuxtApp({ force: true, persistState: true }). To preview the UI, pass force-open. There is no open prop.

To reload without a prompt, set reloadStrategy:

  • 'immediate': reload when chunks go stale.
  • 'idle': reload after 60 seconds without user input, or as soon as the tab is hidden.
  • false: do nothing. Handle skew:chunks-outdated yourself.

React in code

useSkewProtection() is auto-imported. It returns refs and registers callbacks that the module removes on unmount.

const {
  onCurrentChunksOutdated,
  onAppOutdated,
  isAppOutdated,
  isRollback,
  clientVersion,
} = useSkewProtection()

onCurrentChunksOutdated(({ invalidatedModules, passedReleases }) => {
  // the tab runs deleted code; save state, then reload
})
  • isRollback: true when the server version is older than the client version (a deployment rollback). Requires version timestamps in skewProtection.versions from latest.json.
  • simulateUpdate(): dev-only helper that fires skew:chunks-outdated with empty arrays so you can test the notification UI without deploying.

If the update was already detected, a callback runs at registration. useSkewProtection({ lazy: true }) does not connect on mount. Call connect() yourself.

Reject stale clients on the server

The server helpers are not auto-imported. Import them from #skew-protection/server. The nuxt-skew-protection/server alias also works.

import { isClientOutdated } from '#skew-protection/server'

export default defineEventHandler((event) => {
  if (isClientOutdated(event)) {
    setResponseStatus(event, 409)
    return { error: 'Client outdated', requiresReload: true }
  }
  return { ok: true }
})

isClientOutdated is false when the request has no cookie. The same entry exports getClientVersion, getSkewProtectionCookie, and setSkewProtectionCookie.

Cloudflare

  • cloudflare-module polls by default. Workers hold no SSE stream and no WebSocket.
  • Real time needs nitro.preset: 'cloudflare-durable' and nitro.experimental.websocket: true.
  • For KV storage, set storage: { driver: 'cloudflare-kv-binding' }. The build reads the SKEW_PROTECTION binding id from nitro.cloudflare.wrangler.kv_namespaces or wrangler.json(c)/wrangler.toml. With @nuxthub/core, set storage.namespaceId, or the build throws.
  • On cloudflare-module and cloudflare-durable, the module adds your build asset path to assets.run_worker_first. Those requests count as Worker invocations.

Traps

  • A headless browser test sees no push updates. If @nuxtjs/robots is installed, its bot detection matches HeadlessChrome and skips the SSE or WebSocket connection. Override the user agent in the test.
  • useActiveConnections() exists only with connectionTracking: true. It needs sse or ws, and stats reach only connections that call authorize() in the Nitro hook skew:authorize-stats. See https://nuxtseo.com/docs/skew-protection/guides/live-connections
  • sse or ws on nuxt generate falls back to polling with a warning. Polling uses Nuxt experimental.checkOutdatedBuildInterval, which defaults to one hour.
  • Vercel native skew protection turns off asset storage. When VERCEL_SKEW_PROTECTION_ENABLED=1 and VERCEL_DEPLOYMENT_ID are set, bundleAssets defaults to false.
  • A route rule that caches HTML with max-age and no s-maxage drops the cookie for that route. The build warns. Use s-maxage for a CDN, or private for the browser only.
  • The cookie lasts 7 days. It is not a session cookie. Use that duration in a cookie consent list.
  • isRollback requires version timestamps. It reads skewProtection.versions from latest.json. If storage is not configured or bundleAssets is false, it always returns false.

Version limits

2.x requires Nuxt ^4.6.0 || ^5.0.0 and Node ^22.22.3 || ^24.15.0 || >=26.0.0. These 2.x changes break 1.x setups:

1.x 2.x fix
SSE by default on most server presets Polling on every platform. Set updateStrategy: 'sse', or 'ws' on cloudflare-durable, for push updates and connectionTracking.
experimental.appManifest: false allowed The build throws. Remove the override, or set skewProtection: { enabled: false }.
One push connection per tab Visible tabs share one connection. Set multiTab: false or connectionTracking: true for one connection per tab.
storage.driver default fs The default is fs-lite with the same paths. Set driver: 'fs' to keep the old driver.
Robots and Site Config installed with the module Both are optional peers. Run npx nuxt module add @nuxtjs/robots to keep bot filtering.
DevTools panel built in Install nuxtseo-devtools-host as a dev dependency.

1.x renamed these. The package binary rewrites them in place: pnpm exec nuxt-skew-protection migrate.

0.x 1.x
skew-protection:chunks-outdated hook skew:chunks-outdated
isOutdated isAppOutdated
bundlePreviousDeploymentChunks bundleAssets
/_skew/* routes /__skew/*
import { checkForUpdates } from '#skew-protection' useSkewProtection().checkForUpdates

Config

  • bundleAssets (true): set false when your CDN already keeps old /_nuxt/ files.
  • cookie: set false to drop the cookie. isClientOutdated then always returns false.
  • basePath (/__skew): the full public endpoint prefix, including app.baseURL. Auto-detected; set it only for custom routing.
  • updateStrategy: pass pusherAdapter({ key, cluster, appId, secret }) from nuxt-skew-protection/adapters/pusher, or ablyAdapter({ key, authUrl }) from nuxt-skew-protection/adapters/ably, for a hosted realtime provider. Install pusher-js or ably. The build validates the config and broadcasts each new build id. For another provider, write one with defineAdapter from nuxt-skew-protection/adapters: https://nuxtseo.com/docs/skew-protection/providers/external
  • Other options: https://nuxtseo.com/docs/skew-protection/api/config

Debug

  • GET /__skew/health returns { ok, version, uptime }. Compare version with the client build id.
  • /_nuxt/builds/latest.json lists every stored version under skewProtection.versions. One entry after a second deploy means storage did not persist.
  • debug: true logs detection, service worker, and storage steps.
  • Nuxt DevTools has a Skew Protection tab. The panel needs nuxtseo-devtools-host as a dev dependency.
  • In dev, call useSkewProtection().simulateUpdate() from the browser console to fire skew:chunks-outdated and preview the notification without deploying.

Source: SKILL.md on GitHub

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

No third-party reports yet.

Signed by skilld at a0fe1bf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated last month
Other metadata
compatibility
Requires Node.js ^22.22.3 || ^24.15.0 || >=26.0.0. Requires Nuxt ^4.6.0 || ^5.0.0.

README badge

README badge for harlan-zw/nuxt-skew-protection