T
TanStack2mo ago
rival-black

Router Level Redirects

Hey I'm migrating from Next.js and was wondering if it is possible to perform redirects at the router level, in the same way that rewrites can be set up at the createRouter level. https://nextjs.org/docs/app/api-reference/config/next-config-js/redirects Is this something that could be set up on the createRouter?
26 Replies
rival-black
rival-blackOP2mo ago
It would also be useful for both rewrites and redirects to have access to the Headers & Cookies. It would also be extremely cool if we could modify the headers during the rewrite, IDK if that's possible
grumpy-cyan
grumpy-cyan2mo ago
why would a rewrite modify headers ? also keep in mind that the rewrite happens both on server and client
rival-black
rival-blackOP2mo ago
I want to use it to proxy requests to PostGREST and apply a JWT to Bearer Modifying the headers with Next.js wasn't possible during a rewrite or redirect in the config, but reading is The cool thing about the router level rewrites is it doesn't seem to invoke serverless requests for proxies Whereas using middleware I'm pretty sure would have serverless invocations with CPU time. So I was planning to have rewrite and redirect rules at the router level for localized routes to avoid having extra CPU usage. Then for PostGREST I use a rewrite to proxy requests to Neon Data API Anyway if the rewrite could return { url, headers } and pass ({ url, headers }) that would be really powerful. Or even just pass the entire Request object if its available
grumpy-cyan
grumpy-cyan2mo ago
sorry i cannot follow your terminology "serverless requests" ? might just be me not having any clue about nextjs a rewrite on the router is not doing any proxying it is just a rewrite of the url before router parses the url (and then the opposite direction when producing a link) you might get what you need by looking at nitro's route rules, if you are using nitro to deploy
rival-black
rival-blackOP2mo ago
By serverless request I just mean a serverless invocation, like on Cloudflare workers every SSR render or API request counts as a serverless invocation, which shows up in the Observability logs under Invocations with CPU time being clocked So is there no way to have redirects in the same way as rewrites ? Like instead of having /en rewrite to / and show as /en in the browser, actually just have /en become / in the browser
grumpy-cyan
grumpy-cyan2mo ago
you can redirect for sure but thats a totally different thing than a rewrite there are a few places where you can install those redirects the most common one is throwing a redirect() in e.g. beforeLoad
rival-black
rival-blackOP2mo ago
I see so it’s not possible to have a redirect at the router level? Before script execution on a route
grumpy-cyan
grumpy-cyan2mo ago
you can install a middleware to do it that runs before the router
rival-black
rival-blackOP2mo ago
Okay I'll give that a test. I'm looking at cloudflare dashboard and it looks like they have complex options for rewrites and redirects at the domain level. I just want to avoid having server CPU time just for localization. Like every visit from a German speaker with accept-language de to "/" should not trigger any CPU time and should simply redirect to /de and serve a static asset for that page (100% free)
grumpy-cyan
grumpy-cyan2mo ago
you can do this, but if you want to allow a language selection (e.g. via cookie), it gets complicated quickly with this it's all tradeoffs
rival-black
rival-blackOP2mo ago
Yea I just had it fully working on Next is all directly in next.config.json, even with cookie preference
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
import { routing } from "./src/i18n/routing"

const { locales, defaultLocale } = routing

// Create regex pattern to match paths that DON'T start with any locale
// This uses negative lookahead to exclude paths starting with locales
const skipPattern = `/:path((?!(?:${locales.join("|")}|api|trpc|_next|_vercel)(?:/|$)|.*\\..*).*)`

const nextConfig: NextConfig = {
rewrites: async () => {
return [
{
// Match any path that doesn't start with a locale prefix
// e.g., /about, /contact, / but NOT /en/about or /de/contact
source: skipPattern,
destination: `/${defaultLocale}/:path*`
},
{
source: "/api/v1/db/:path*",
destination: `${process.env.NEON_DATA_API_URL}/:path*`
}
]
},
redirects: async () => {
return [
{
source: `/${defaultLocale}`,
destination: `/`,
permanent: false
},
{
source: `/${defaultLocale}/:path*`,
destination: `/:path*`,
permanent: false
},
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => ({
source: skipPattern,
destination: `/${locale}/:path*`,
permanent: false,
has: [
{
type: "cookie" as const,
key: "NEXT_LOCALE",
value: locale
}
]
})),
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => ({
source: skipPattern,
destination: `/${locale}/:path*`,
permanent: false,
has: [
{
type: "header" as const,
key: "accept-language",
value: `${locale}(-[A-Z]{2})?\\b`
}
],
missing: [
{
type: "cookie" as const,
key: "NEXT_LOCALE"
}
]
}))
]
}
}

const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
import { routing } from "./src/i18n/routing"

const { locales, defaultLocale } = routing

// Create regex pattern to match paths that DON'T start with any locale
// This uses negative lookahead to exclude paths starting with locales
const skipPattern = `/:path((?!(?:${locales.join("|")}|api|trpc|_next|_vercel)(?:/|$)|.*\\..*).*)`

const nextConfig: NextConfig = {
rewrites: async () => {
return [
{
// Match any path that doesn't start with a locale prefix
// e.g., /about, /contact, / but NOT /en/about or /de/contact
source: skipPattern,
destination: `/${defaultLocale}/:path*`
},
{
source: "/api/v1/db/:path*",
destination: `${process.env.NEON_DATA_API_URL}/:path*`
}
]
},
redirects: async () => {
return [
{
source: `/${defaultLocale}`,
destination: `/`,
permanent: false
},
{
source: `/${defaultLocale}/:path*`,
destination: `/:path*`,
permanent: false
},
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => ({
source: skipPattern,
destination: `/${locale}/:path*`,
permanent: false,
has: [
{
type: "cookie" as const,
key: "NEXT_LOCALE",
value: locale
}
]
})),
...locales
.filter((locale) => locale !== defaultLocale)
.map((locale) => ({
source: skipPattern,
destination: `/${locale}/:path*`,
permanent: false,
has: [
{
type: "header" as const,
key: "accept-language",
value: `${locale}(-[A-Z]{2})?\\b`
}
],
missing: [
{
type: "cookie" as const,
key: "NEXT_LOCALE"
}
]
}))
]
}
}

const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)
grumpy-cyan
grumpy-cyan2mo ago
but that is using CPU, right?
rival-black
rival-blackOP2mo ago
The rewrites definitely happen before the requests CPU usage shows up, I thought the redirects work in the same way
grumpy-cyan
grumpy-cyan2mo ago
doubt it but maybe this maps to a static config on vercel? who knows
rival-black
rival-blackOP2mo ago
Yea it might be under the hood in Vercel Which would mean to replicate that I would have to use the Cloudflare rules Hey so the main use case for having rewrites able to check the header & cookies is for non-prefix based localization that uses the locale from the cookie to determine which locale path to render for static sites It would be great if we could call something like getRequestHeaders() inside of the rewrite
grumpy-cyan
grumpy-cyan2mo ago
you can. you just need to make rewrite an isomorphic function as this will run on both client and server
rival-black
rival-blackOP2mo ago
Oh that's really cool actually This means I don't need to use the {-$locale} folder at all
grumpy-cyan
grumpy-cyan2mo ago
the start paraglide example also does not use it
rival-black
rival-blackOP2mo ago
Yea I was looking into theirs, I'm building a custom solution for use-intl atm One of the downsides with that setup is you have to do a hard refresh / window navigate when swapping locales
grumpy-cyan
grumpy-cyan2mo ago
why is that ?
rival-black
rival-blackOP2mo ago
I mean it’s not a huge downside since people don’t swap locales very often, but keeping navigations within the SPA is cleaner It’s just the way that setLocale works it does window.location.href = “/de” since the rewrites don’t let you navigate to other locales using Link or useRouter but yea it’s not a huge deal
grumpy-cyan
grumpy-cyan2mo ago
since the rewrites don’t let you navigate to other locales using Link or useRouter
i am not sure i can follow. where do you store the current locale? i assume in some JS state. why cant this be flipped without a hard reload?
rival-black
rival-blackOP2mo ago
It's all under the hood in the codegen for paraglide
grumpy-cyan
grumpy-cyan2mo ago
@Júlio Mühlbauer any idea?
rival-black
rival-blackOP2mo ago
It looks like the setLocale has the option to disable hard reloads but I think in that case you'd need to manually invalidate the route But yea it's not a big deal
fair-rose
fair-rose5w ago
Yes, the default behavior is reloading the window. You can disable it, but you will have to handle yourself stale content/data in the framework

Did you find this page helpful?