Redirect Maps

How to configure redirect maps in front of a Waku server.

  • Experimental

External Redirect Maps

This guide is for redirect maps that sit in front of the Waku server, such as middleware, reverse proxies, CDNs, or hosting-platform redirect rules.

When a Waku app uses the client router, a navigation usually fetches both:

  • the browser route, such as /old
  • the RSC payload for the same route, at a separate url

Both have to be redirected. If you redirect only /old, a direct visit works while client-side navigation still lands on the old route.

Inside the app, unstable_parseRouterRequest reads either url as the route it addresses, and unstable_formatRouterRequest builds the matching url for the destination, so middleware never has to spell the RSC url out. Outside the app — a CDN or a hosting platform's redirect rules — there is no way to call into Waku, so those rules do have to name the url. With Waku's default config:

  • basePath is /
  • rscBase is RSC
  • the route /old maps to the RSC path /RSC/R/old.txt
Note

The /RSC/ file naming convention is subject to change in future versions of Waku. Prefer the middleware approach below, which does not depend on it.

If you customize basePath or rscBase, adjust any hand-written rules accordingly.

For application-level redirects implemented inside Waku routes or server actions, see Redirects and Not Found instead. That is a separate use case from the redirect-map approach described here.

Redirect via middleware

Create a new middleware file in src/middleware/. Waku automatically discovers and loads middleware from this directory.

// ./src/middleware/redirects.ts
import type { MiddlewareHandler } from 'hono';
import {
  unstable_formatRouterRequest as formatRouterRequest,
  unstable_parseRouterRequest as parseRouterRequest,
} from 'waku/router/server';

const redirects: Record<string, string> = {
  '/old': '/new',
  // ... add more redirects here
};

const redirectsMiddleware = (): MiddlewareHandler => async (c, next) => {
  // Reads the browser route and the RSC payload request the same way.
  const parsed = parseRouterRequest(c.req.raw);
  const destination =
    parsed?.type === 'route' ? redirects[parsed.path] : undefined;
  // Rebuilds the same kind of url, so client-side navigation gets a payload
  // and a direct visit gets the page. `null` means this request cannot be
  // rewritten without losing state, so leave it alone.
  const location = destination
    ? formatRouterRequest(c.req.raw, destination)
    : null;
  if (location) {
    c.res = new Response(null, {
      status: 302,
      headers: { location: location.toString() },
    });
    return;
  }

  return await next();
};

export default redirectsMiddleware;

Matching on parsed.path covers both requests with one entry per route, and keeps the redirect map readable as a list of routes.

Note

In a production build the adapter serves statically generated files before middleware runs, so a middleware redirect does not fire for a route rendered with render: 'static'. Redirect those from the hosting environment instead, or make the route dynamic.

Note the type check: an action dispatched from the client is addressed by function id and carries no route, so a path rule can never match it. A progressively enhanced form submitted without JavaScript is the other way around — it posts to the route's own url, so it parses as 'route' and a rule for that path does apply to it. Either way, a path rule is a redirect, not an authorization boundary; see Authentication for where to put checks that must not be bypassed.

Redirect via hosting environment

A redirect map outside the app cannot call into Waku, so these rules name both urls explicitly and depend on the /RSC/ convention noted above.

This very much depends on the hosting environment you are using. For example, on Vercel you can use the vercel.json file to define redirects.

{
  "redirects": [
    { "source": "/old", "destination": "/new", "permanent": true },
    {
      "source": "/RSC/R/old.txt",
      "destination": "/RSC/R/new.txt",
      "permanent": true
    }
  ]
}

Netlify and Cloudflare Pages will respect a _redirects file that you can place in the public folder:

/old /new 301
/RSC/R/old.txt /RSC/R/new.txt 301

For app-level redirects inside page code or server actions, use Redirects and Not Found instead of external redirect maps.

designed bycandycode alternative graphic design web development agency San Diego