@wb-at/storyblok-nuxt-redirects

WB Storyblok Nuxt Redirects Module
773
Install
npm install @wb-at/storyblok-nuxt-redirects
Latest Version:1.3.1
Vue:3
License:MIT
Last Updated:Sep 15, 2026
Links: npm
Maintainer: markus-gx

WB Storyblok Nuxt Redirects

A Nuxt module that handles URL redirects using Storyblok datasources. This module fetches redirect rules from a Storyblok datasource and applies them either at build time or at runtime. This module is part of a Storyblok App Extension which manages auto-deployment/reload and the datasource in Storyblok.

Features

  • πŸš€ Build-time redirects – generate static route rules at build time for optimal performance
  • ⚑ Runtime redirects – resolved in Nitro server middleware, so they also cover non-page URLs
  • ♻️ Self-healing cache – redirects revalidate in the background; a Storyblok outage never empties the list
  • πŸ”„ Instant refresh – POST /api/_storyblok-redirects/refresh reloads without a restart
  • πŸ”— Query forwarding – utm_*, gclid, fbclid … survive the redirect
  • 🧹 Self-healing data – chains are flattened to one hop, loops and duplicates are dropped
  • πŸ“¦ Storyblok integration – manage redirects directly in Storyblok's datasource editor

Installation

# Using npm
npm install @wb-at/storyblok-nuxt-redirects
# Using yarn
yarn add @wb-at/storyblok-nuxt-redirects
# Using pnpm
pnpm add @wb-at/storyblok-nuxt-redirects

Configuration

Add the module to your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@wb-at/storyblok-nuxt-redirects'],

  storyblokNuxtRedirects: {
    // Your Storyblok Content Delivery API token (required)
    storyblokAccessToken: 'your-storyblok-access-token',

    // Slug of the Storyblok datasource containing redirects
    datasourceSlug: 'sb-wb-redirects',

    // If true, redirects are baked into static route rules at build time
    buildTimeOnly: false,

    // Seconds until a loaded redirect set is refreshed in the background (0 disables)
    revalidateInterval: 300,

    // Append the incoming query string to the destination
    forwardQuery: true,

    // Token protecting the refresh endpoint
    refreshToken: 'your-secret-refresh-token',

    // Enable debug logging
    debug: false,
  },
})

Configuration Options

Option Type Default Description
storyblokAccessToken string – Your Storyblok API access token (required)
datasourceSlug string 'sb-wb-redirects' The slug of the Storyblok datasource containing redirects
buildTimeOnly boolean false Fetch at build time and emit static route rules. Build-time switch – see below
revalidateInterval number 300 Seconds before the cached set is refreshed in the background. 0 disables revalidation
forwardQuery boolean true Append the incoming query string to the destination
refreshToken string – Bearer token for the refresh endpoint. Without it the endpoint returns 404
debug boolean false Enable debug logging

Environment Variables

Environment Variable Overrides
NUXT_PUBLIC_STORYBLOK_ACCESS_TOKEN the access token
NUXT_PUBLIC_STORYBLOK_NUXT_REDIRECTS_STORYBLOK_ACCESS_TOKEN the access token (takes precedence)
NUXT_PUBLIC_STORYBLOK_NUXT_REDIRECTS_DATASOURCE_SLUG datasourceSlug
NUXT_PUBLIC_STORYBLOK_NUXT_REDIRECTS_REVALIDATE_INTERVAL revalidateInterval
NUXT_PUBLIC_STORYBLOK_NUXT_REDIRECTS_FORWARD_QUERY forwardQuery
NUXT_PUBLIC_STORYBLOK_NUXT_REDIRECTS_DEBUG debug
NUXT_STORYBLOK_NUXT_REDIRECTS_REFRESH_TOKEN refreshToken (private)

buildTimeOnly deliberately has no environment variable. It decides at build time whether route rules are generated at all, so flipping it at runtime could not work.

Usage Modes

Runtime Mode (Default)

Redirects are resolved in Nitro server middleware. That covers every request, not only Nuxt pages – legacy .html/.pdf URLs and files under public/ are matched too. The set is loaded while the server starts, kept in memory, and refreshed in the background once it is older than revalidateInterval.

Nothing is shipped to the browser: the redirect table never reaches the client bundle.

Build-time Mode

storyblokNuxtRedirects: {
  buildTimeOnly: true,
}

Redirects are fetched during nuxt build and added as static route rules. Best performance, but a rebuild is needed to pick up changes.

If the redirects cannot be loaded, the build fails. In this mode there is no runtime retry, so a green build that silently contains zero redirects would be worse than a red one.

Refreshing Redirects at Runtime

curl -X POST https://your-site.com/api/_storyblok-redirects/refresh \
  -H "Authorization: Bearer your-secret-refresh-token"
{ "ok": true, "redirects": 278, "loadedAt": "2026-01-01T12:00:00.000Z" }

Set this URL as the Deploy Webhook in the Storyblok extension, and the extension's auth token to the same value as refreshToken.

For backwards compatibility any request carrying Authorization: Bearer <refreshToken> also triggers a reload, which is how older setups pointed the webhook at their home page. Prefer the endpoint: it is explicit, it reports the result, and it does not add a header check to every request.

Horizontally scaled deployments

A refresh only reaches the instance that served it. With several replicas or on serverless, the others keep serving their own copy until they revalidate. That is what revalidateInterval is for β€” it is the mechanism that actually converges a cluster, and the webhook is only a way to make one instance fast:

Deployment Recommendation
Single instance / single container Webhook + default revalidateInterval: 300
Several replicas behind a load balancer Rely on revalidateInterval (60–300); treat the webhook as best-effort
Serverless (Vercel, Netlify Functions, Lambda) Rely on revalidateInterval; instances are short-lived and load on cold start anyway
Redirects must apply instantly and identically everywhere Use buildTimeOnly: true and trigger a real deploy from the extension

Revalidation is stale-while-revalidate: a request never waits for Storyblok, it is served from the current set while the refresh happens in the background. Lowering the interval costs one extra Storyblok request per instance per interval, nothing more.

Behaviour

Matching happens on the pathname only, after normalisation: trailing slashes, repeated slashes, percent-encoding (/%C3%BCber-uns = /ΓΌber-uns), surrounding whitespace and β€” as a fallback β€” letter case are all ignored. Query string and hash never take part in matching.

On load the datasource is cleaned up: chains (A β†’ B β†’ C) are flattened to a single hop, redirects that would loop are dropped, duplicate sources keep the oldest entry, and entries whose value is not usable JSON are skipped with a warning instead of taking the whole list down.

On failure the previously loaded set is kept and an error is logged. An empty redirect list is only ever the result of an empty datasource.

Upgrading from 1.x

Your Storyblok data needs no migration. The datasource format is unchanged, and existing entries β€” including those stored with a trailing slash, with mixed case, or with an external destination β€” keep resolving. Configuration is additive: an existing nuxt.config.ts keeps working, and the old "any request with Authorization: Bearer <refreshToken> reloads" webhook still does.

Four things do change behaviour. None of them need action, but you should know about them:

Change Effect
Client-side navigation no longer redirects Redirects are resolved on the server only. An in-app <NuxtLink to="/old-url"> now renders a 404 instead of redirecting. Fix the internal link β€” external traffic, which is what redirects are for, is unaffected and now covered more completely than before.
Query strings reach the destination forwardQuery defaults to true, so /old?utm_source=x now redirects to /new?utm_source=x. Set forwardQuery: false for the previous behaviour.
Background revalidation revalidateInterval defaults to 300, so each instance makes one extra Storyblok request every 5 minutes. Set revalidateInterval: 0 to refresh only via the webhook, as before.
buildTimeOnly builds fail loudly A build that cannot reach Storyblok now fails instead of silently shipping zero redirects. A flaky network can turn a previously green build red.

Data that was already broken is now handled rather than passed through: chains (A β†’ B β†’ C) resolve in one hop, redirects that would loop are dropped, duplicate sources keep the oldest entry, and a source that is an absolute URL is discarded β€” it could never match a request path.

One case to check if it applies to you: a source that contains a query string (e.g. /blog?page=2). Matching now happens on the path, so such an entry matches /blog for every query. Search your datasource for ? in a source before upgrading; if you have any, they were almost certainly not working before either.

Removed

  • the route middleware global-storyblok-nuxt-redirects (replaced by Nitro server middleware)
  • deleteInstance() on the instance β€” use StoryblokNuxtRedirects.reset() if you need it in tests

Development

# Install dependencies
yarn install

# Prepare development environment
yarn dev:prepare

# Start development server with playground
# (the playground reads NUXT_PUBLIC_STORYBLOK_ACCESS_TOKEN from the environment)
NUXT_PUBLIC_STORYBLOK_ACCESS_TOKEN=… yarn dev

# Run tests
yarn test

# Build the module
yarn prepack

Version History

Version Released Vue License
1.3.1 ^3.0 MIT
1.1.0 ^3.0 MIT
1.0.6 ^3.0 MIT
1.0.5 ^3.0 MIT
1.0.4 ^3.0 MIT