@onderwijsin/nuxt-redirects

Provider-agnostic dynamic redirects for Nuxt.
1,205
Install
npm install @onderwijsin/nuxt-redirects
Latest Version:0.2.7
Vue:3
License:MIT
Last Updated:Oct 18, 2026
Links: GitHub  ·  npm
Maintainer: remihuigen

@onderwijsin/nuxt-redirects

Dynamic redirects for Nuxt, independent of where your redirect data comes from.

The module collects redirects from one or more external sources and stores them in Nitro storage for fast request-time lookups. Redirects are resolved locally, so handling a request never requires a call to the original provider.

Features

  • Combine redirects from any number of consumer-defined sources, with deterministic first-source-wins precedence.
  • Keep redirects up to date through background refreshes or individual webhook updates.
  • Use any Nitro storage driver, including Redis.
  • Enable server middleware, the client-side Pinia store, and route middleware independently.
  • Configure Nitro caching separately for both public read endpoints.
  • Exclude namespaces or individual routes using efficient precompiled matchers.
  • Opt in to route-pattern redirects with dynamicMatching and match: "pattern".

Installation

pnpm add @onderwijsin/nuxt-redirects @pinia/nuxt pinia pinia-plugin-persistedstate vue

The application supplies @pinia/nuxt, pinia, pinia-plugin-persistedstate, vue as peer dependencies so module runtime code shares the application's instances. Nuxt module registration remains automatic. Install the Pinia packages for the default client store; they can be omitted when both store and routeMiddleware are false.

The module adds the application’s Pinia package to Nuxt’s transpilation list so its Vue feature flags are compiled for SSR.

export default defineNuxtConfig({
  modules: ["@onderwijsin/nuxt-redirects"]
});

Source registration

Add one source per external system under server/redirects/. A source owns provider-specific authentication, pagination, field mapping, and deciding which records to fetch. The module enforces activeFrom and activeUntil timing metadata at existing lookup points.

// server/redirects/cms.ts
import { defineRedirectSource } from "@onderwijsin/nuxt-redirects/runtime/source";

export default defineRedirectSource(async (event) => {
  const records = await getCmsRedirects(event);
  return records.map((record) => ({
    from: record.origin,
    to: record.destination,
    statusCode: record.permanent ? 301 : 302
  }));
});

Sources are discovered recursively in lexicographic file-path order. The first occurrence of a normalized origin wins, including duplicates within the same source. Later duplicates are logged loudly and ignored; they never fail a complete refresh.

Origins are normalized during ingestion: non-root trailing slashes are removed and query key/value pairs are sorted. An origin with a query matches only the same normalized query; a path-only origin is the fallback for requests with query parameters. Destination query strings are preserved and request query parameters are never appended implicitly. Protocol-less domains such as sub.example.com/new are treated as external HTTPS destinations; explicit URLs and internal paths are otherwise used unchanged. Destinations must be an internal path, protocol-relative URL, absolute HTTP(S) URL, or bare domain; control characters and other schemes such as javascript: and data: are rejected during ingestion. Internal redirects whose destination is equivalent to the request after trailing-slash normalization are ignored with a warning by both server and client middleware.

Refreshing

The module deliberately does not schedule work. It registers discovered sources once during Nitro startup, then exposes a normal task handler:

// server/tasks/redirects/refresh.ts
export { default } from "@onderwijsin/nuxt-redirects/runtime/refresh-task";

The task name follows its file path: server/tasks/redirects/refresh.ts becomes redirects:refresh. Schedule that consumer task through the application's Nitro configuration. refreshRedirects() is also exported from the runtime package for protected deployment endpoints.

Webhooks

Provider webhooks remain consumer routes, because their payload and authentication are provider-specific. Map a verified payload to the public mutation helpers:

import { removeRedirect, upsertRedirect } from "@onderwijsin/nuxt-redirects/runtime";

export default defineEventHandler(async (event) => {
  const change = await verifyAndParseWebhook(event);
  if (change.kind === "delete") return removeRedirect(change.from);
  return upsertRedirect({ from: change.from, to: change.to, statusCode: change.statusCode });
});

Storage drivers

The module stores redirect data and both cached read endpoints in Nitro's configured storage mount. It defaults to redirects; configure any Nitro storage driver under that name when the index and cache must be shared across instances or survive process replacement:

export default defineNuxtConfig({
  nitro: {
    storage: {
      redirects: {
        driver: "redis"
        /* Redis driver connector options */
      }
    }
  }
});

Use a shared driver for horizontally scaled or serverless production deployments. Cache reads, writes, and mutation/refresh invalidation follow the same mount. If your storage configuration uses a different mount name, set the matching module option:

export default defineNuxtConfig({
  redirects: { storageMount: "shared-redirects" },
  nitro: { storage: { "shared-redirects": { driver: "redis" } } }
});

Dynamic Pattern Matching

Dynamic pattern matching is disabled by default. Enable it at the module level, then opt individual redirects in with match: "pattern":

export default defineNuxtConfig({
  redirects: { dynamicMatching: true }
});
export default defineRedirectSource(() => [
  {
    from: "/legacy/:section/:slug",
    to: "/docs/:section/:slug",
    statusCode: 301,
    match: "pattern"
  },
  { from: "/files/*", to: "/downloads/*", match: "pattern" }
]);

These rules use the regexparam route-pattern syntax. :name captures one path segment, :name? makes a segment optional, and * captures a wildcard path. Exact query and path-only redirects are always checked before pattern rules. Pattern rules match the pathname only, so incoming query parameters are not copied to the destination. Timing bounds use an inclusive activation and exclusive expiration window: activeFrom <= now < activeUntil.

For example, /legacy/guides/getting-started becomes /docs/guides/getting-started, and /files/reports/2026.pdf becomes /downloads/reports/2026.pdf. Raw regular expressions and constrained parameter groups such as :id<[0-9]+> are not supported.

Endpoints and client behavior

GET /api/_redirects returns the compact exact index as a { [origin]: Redirect } object. When dynamicMatching is enabled, the response also includes serializable dynamic pattern rules. GET /api/_redirects/:path looks up one encoded path/query origin and is used when client route middleware is on but the store is off. Both are wrapped in defineCachedEventHandler; configure cache.index and cache.lookup separately.

upsertRedirect(), removeRedirect(), and a complete source refresh immediately invalidate affected public cache entries; an upsert also primes its exact lookup response. Pattern mutations rebuild the ordered pattern manifest from registered sources so incremental updates cannot bypass provider precedence. A path-only change or complete refresh clears all lookup entries because path-only redirects are query fallbacks. Use a short cache.index TTL when the Pinia store must converge quickly after webhooks; a webhook-driven setup can safely use a longer cache.lookup TTL because mutations and refreshes invalidate affected entries. Update the provider before invoking a pattern mutation so the source-backed rebuild sees the canonical pattern set.

Lookup cache keys are derived from the complete normalized origin, including its query string, so distinct paths and query variants never share a cache record.

The client does not revalidate cached single-lookup responses before navigation. A response may be stale for a short period after its timing window changes; this is an intentional performance trade-off.

export default defineNuxtConfig({
  redirects: {
    cache: {
      index: { maxAge: 15, staleMaxAge: 60, swr: true },
      lookup: { maxAge: 3600, staleMaxAge: 3600, swr: true }
    }
  }
});

serverMiddleware, store, and routeMiddleware are independent. When routeMiddleware is true and store is false, every client navigation uses the cached single-path endpoint instead of loading the complete index. Enabling store registers the application-installed @pinia/nuxt and pinia-plugin-persistedstate/nuxt as module dependencies. The client store persists its redirect index and last-fetch timestamp in browser localStorage, not cookies.

Configuration

export default defineNuxtConfig({
  redirects: {
    serverMiddleware: true,
    store: true,
    routeMiddleware: true,
    storageMount: "redirects",
    storeRefreshInterval: 3600,
    excludedNamespaces: ["/api", "/_nuxt", "/_payload", "/__"],
    excludedRoutes: ["/"],
    cache: {
      index: { maxAge: 60, staleMaxAge: 300, swr: true },
      lookup: { maxAge: 60, staleMaxAge: 300, swr: true }
    }
  }
});

Compatibility

Developed against Nuxt 4.5.x and Node.js 24. The package requires Node.js 24 or newer.

Discovered server sources support Nitro virtual imports such as #imports in development and production, including when refreshing through a consumer task.

Related Packages

@onderwijsin/nuxt-directus-client

Server-safe Directus REST integration for Nuxt applications.

3,264 0
@onderwijsin/nuxt-turnstile

Nuxt Turnstile integration with client helpers and server-side validation.

2,924 0
@onderwijsin/nuxt-theme-customizer

Nuxt module for runtime theme selection and custom color editing in Nuxt UI.

2,719 0
@onderwijsin/nuxt-module-utils

Shared runtime and Nuxt module utilities for Onderwijs in modules.

2,622 0
@onderwijsin/nuxt-newsletter-signup

Provider-independent newsletter signup endpoint for Nuxt.

2,353 0

Version History

Version Released Vue License
0.2.7 ^3.5.0 MIT
0.2.6 ^3.5.0 MIT
0.2.5 ^3.0 MIT
0.2.4 ^3.0 MIT
0.2.3 ^3.0 MIT