Skip to content
Built with

Rewrites ​

Build & Deploy

A rewrite serves a route at a different URL without changing the address bar. For example, /docs can serve /en/docs. Use a redirect when the browser should move to the new URL instead.

On this pageAdd rewrite rules in config · Rewrite from a handler · Avoid surprises with client navigation

This step defines a rewrite in void.config.ts.

ts
import { defineConfig } from 'void/config';

export default defineConfig({
  routing: {
    rewrites: {
      '/': '/en',
      '/docs': '/en/docs',
      '/docs/*': '/en/docs/:splat',
    },
  },
});

A request to /docs/getting-started serves /en/docs/getting-started. Other locales, such as /ja/docs/getting-started, keep their own routes.

Matching rules ​

  • Sources and destinations start with /. Destinations must be paths within the app.
  • * matches any characters, including /. Use :splat in the destination for the matched portion.
  • The first matching rule wins. Put specific patterns before catch-alls.
  • Rewrites run before static assets and application routes.

Programmatic rewrites in middleware ​

This step rewrites from middleware with c.rewrite(). Use it when the destination depends on the request, such as a locale cookie.

ts
import { defineMiddleware } from 'void';

export default defineMiddleware(async (c, next) => {
  if (c.req.path.match(/^\/(en|ja)(\/|$)/)) return next();

  const locale = detectLocale(c.req);
  return c.rewrite(`/${locale}${c.req.path}`);
});

Return the result of c.rewrite(). Middleware runs again at the destination. Skip rewritten paths to avoid a loop. This step uses c.isRewritten() to run work only once.

ts
if (c.isRewritten()) return next();

Keep access checks on any destination that needs them.

A destination without ? keeps the incoming query string. c.rewrite('/search') keeps ?q=.... c.rewrite('/search?q=all') replaces it.

Your editor suggests known routes through the RewriteDestination type. Dynamic strings are also accepted.

Runtime rewrites cannot reach static assets ​

c.rewrite() targets application routes only. To serve an asset such as /hero.png at another URL, use routing.rewrites or a _redirects rule. A middleware rewrite to a recognized asset extension throws VoidAssetRewriteError.

Original URL access ​

c.originalUrl() returns the URL before any rewrite, or null if none happened. This step uses it for a canonical link.

ts
import { defineHandler } from 'void';

export default defineHandler((c) => {
  const original = c.originalUrl();
  return c.json({ pathname: original?.pathname ?? c.req.path });
});

Fallbacks ​

This step adds a fallback. routing.fallbacks uses rewrite patterns, but applies only when no asset or route matches.

ts
import { defineConfig } from 'void/config';

export default defineConfig({
  routing: {
    fallbacks: { '/*': '/index.html' },
  },
});

Use a fallback for a client-side router or a default-locale catch-all. An intentional 404 from an SDK route stays a 404. With third-party frameworks, a fallback can replace the framework's 404.

SPA apps already fall back to /index.html. Custom fallbacks run first. Other paths still get the SPA shell. You do not need to add '/*': '/index.html'.

_redirects file ​

This step puts rules in a file in Vite's publicDir. It defaults to public/.

text
# Fallback: only when no asset or route matches
/*         /index.html     200

# Rewrite: before assets and routes
/docs/*    /en/docs/:splat  200!

The file can include 3xx redirects too. Rules in void.config.ts win over file rules. Within each group, the first match wins.

Client navigation ​

Rewrites run on the server. In Pages mode, a client-side <Link href="/docs"> can fetch loader data for /docs, not for the rewritten /en/docs.

Use <a href="/docs"> to make a full server request when the destination has a different loader. Use a redirect if the URL should change.

ISR cache keys with rewrites ​

Purge the destination path, such as /en/docs/foo, to clear rewritten cache entries. See Cache keys and rewrites.

Debugging with X-Void-Routing ​

This step helps you debug rules in development. Check the X-Void-Routing header in the browser's Network tab. It shows the matching rule and where it is declared.

text
X-Void-Routing: rewrite[/docs/*] -> /en/docs/:splat (void.config.ts#routing.rewrites)
X-Void-Routing: c.rewrite -> /new-path (middleware)
X-Void-Routing: pass-through

Next steps ​

implemented.stream AI