| outline | deep |
|---|
A rewrite serves a route at a different URL without changing the browser's address. For example, /docs can serve /en/docs. Use a redirect when the browser should move to the destination instead.
Define rewrites in void.config.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.
- Sources and destinations start with
/. Destinations must be paths within the app. *matches any characters, including/. Use:splatin the destination for the matched portion.- The first matching rule wins. Put specific patterns before catch-all patterns.
- Rewrites run before static assets and application routes.
Use c.rewrite() when the destination depends on the request, such as a locale chosen from a cookie:
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, so skip paths that are already rewritten to avoid a loop. Use c.isRewritten() to skip work that should happen only once:
if (c.isRewritten()) return next();Keep access checks on any destination that needs them.
A destination without ? preserves the incoming query string. c.rewrite('/search') keeps ?q=..., while c.rewrite('/search?q=all') replaces it.
Your editor suggests known routes through the RewriteDestination type. Dynamic strings are also accepted.
c.rewrite() targets application routes. To serve an asset such as /hero.png at another URL, use routing.rewrites or a _redirects rule instead. A middleware rewrite to a recognized asset extension throws VoidAssetRewriteError.
c.originalUrl() returns the URL requested before a static or middleware rewrite, or null when no rewrite occurred. Use it for canonical links or locale detection:
import { defineHandler } from 'void';
export default defineHandler((c) => {
const original = c.originalUrl();
return c.json({ pathname: original?.pathname ?? c.req.path });
});routing.fallbacks uses the same patterns as rewrites, but applies only when no asset or route matches:
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 a Void route remains a 404. For third-party frameworks, fallbacks can also replace the framework's 404 response.
SPA apps already fall back to /index.html. Custom fallback rules run first; unmatched paths still use the SPA shell. You don't need to add '/*': '/index.html' yourself.
You can also place rules in Vite's publicDir, which defaults to public/:
# 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 take precedence over file rules; within each group, the first match wins.
Rewrites run on the server. In Pages mode, a client-side <Link href="/docs"> navigation may fetch loader data for /docs rather than the rewritten route /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.
Purge the destination path, such as /en/docs/foo, to clear rewritten cache entries. See Cache keys and rewrites.
During development, inspect the X-Void-Routing response header in your browser's Network tab. It shows the matching rule and where it was declared:
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