Reputation: 2243
Context:
I've a NextJS deployment behind Nginx. The idea is to use NextJS to create several websites hosted in different domains. Each domain will have an entry in Nginx and it will be pointing to the specific path pages/cafes/[cafeId]
in NextJS. There will be only one NextJs deployment for all websites and each domain will be routed using static
proxy in nginx.
nginx.conf
server {
listen 80;
server_name www.cafe-one.local;
location = / {
proxy_pass http://localhost:3000/cafes/cafe_id_1;
...
}
location / {
proxy_pass http://localhost:3000/;
...
}
}
server {
listen 80;
server_name www.cafe-two.local;
location = / {
proxy_pass http://localhost:3000/cafes/cafe_id_2;
...
}
location / {
proxy_pass http://localhost:3000/;
...
}
}
pages/[cafeId]/index.js
export const getStaticPaths = async () => {
return {
paths: [], // no website rendered during build time, there are around 1000+ sites
fallback: true
};
};
export const getStaticProps = async context => {
const cafeId = context.params.cafeId;
const cafe = await ... // get data from server
return {
props: {
cafe
},
revalidate: 10 // revalidate every 10 seconds
};
};
export default function CafeWebsite(props) {
const router = useRouter();
// getStaticProps() is not finished
if (router.isFallback) {
return <div>Loading...</div>;
}
return <div>{props.cafe.name}</div>;
}
Issue:
When I access www.cafe-one.local
, i get to the loading screen, but then NextJS throw a client-side error about the The provided as value (/) is incompatible with the href value (/cafes/[cafeId])
. This is understandable because the current URL is not what NextJS is expecting.
Question:
How to fix the issue, such that NextJS could be used in front of Nginx reverse proxy?
Any help is appreciated.
Thanks in advance.
Upvotes: 13
Views: 17768
Reputation: 8741
Edit Oct 2021: Next.js 12 now supports middleware, which can achieve dynamic subdomains without the need for a proxy server. See an official example.
Original answer: Thanks to Ale for the replaceState idea, this is how we handle it now:
/original.host.com/
as the first item in the path:server {
listen 8080;
# redirect HTTP to HTTPS
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
# (not needed if you set assetPrefix in next.config.js to https://myapp.com)
location /_next {
proxy_pass https://myapp.com;
}
# a separate line for root is needed to bypass nginx messing up the request uri
location = / {
proxy_pass https://myapp.com/$host;
}
# this is the primary proxy
location / {
proxy_pass https://myapp.com/$host$request_uri;
}
}
_app.tsx
, we register an effect that runs after Next.js finishes changing the route (this isn't triggered on the first render).useEffect(() => {
const handleRouteChange = (url: string) => {
const paths = url.split('/')
if (paths[1] === location.host) {
// remove /original.host.com/ from the path
// note that passing history.state as the first argument makes back/forward buttons work correctly
history.replaceState(history.state, '', `/${paths.slice(2).join('/')}`)
}
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [])
All domain-specific pages are under pages/[domain]/
, e.g. pages/[domain]/mypage.tsx
.
Finally, we prepend the original hostname to each href
, like
<Link href="/original.host.com/mypage">...</Link>
orRouter.push('/original.host.com/mypage')
.There's no need to use as
anymore.
Next.js will now navigate to https://original.host.com/original.host.com/mypage
for a split second, and then replace it with https://original.host.com/mypage
once the transition is complete.
To make SSR work for each domain-specific page, we add getStaticPaths
/getStaticProps
in each of our pages so that Next.js knows to generate a separate version of the page for each domain (otherwise router.query.domain
will be empty while in SSR and we'd get errors that the paths mismatch). Note that this doesn't negatively affect performance because the page will be cached after the first request.
// In pages/[domain]/mypage.tsx
export default function MyPage(params: MyPageProps) {
const router = useRouter()
// SSR provides domain as a param, client takes it from router.query
const domain = params.domain || router.query.domain
// ... rest of your page
}
export const getStaticPaths: GetStaticPaths = async () => ({
fallback: 'blocking',
paths: [],
})
export const getStaticProps: GetStaticProps<SpaceEditPageProps> = async ({
params,
}) => {
return {
props: {
domain: params?.domain as string,
},
}
}
passHref
but instead set the fake URL as href
, cancel default action in onClickCapture
, and trigger the router in onClick
(you can extract this into a reusable component):<Link href="/original.host.com/mypage">
<a
href="/mypage"
onClick={() => Router.push("/original.host.com/mypage")}
onClickCapture={e => e.preventDefault()}
>
// ...
</a>
</Link>
Upvotes: 5
Reputation: 51
I've managed to handle the same issue by forcing my dynamic pages to use ONLY serverside rendering.
The fact is, Next tries to hydrate route params from the browser page URL which is outside of Next's context due to nginx.
Upvotes: 1
Reputation: 407
I have been dealing with the same issue, but for mapping different subdomains to a dynamic route in the NextJS app.
I haven't been able to find a proper solution to the The provided as value (/) is incompatible with the href value
error, but I found a somewhat hacky workaround.
First, you have to redirect the requests from my-domain.com
to my-domain.com/path-to-dynamic-route
. Then you have to reverse proxy all request from my-domain.com/path-to-dynamic-route
to the same dynamic route in the NextJS app, like localhost:3000/path-to-dynamic-route
.
You can do it manually from NGINX with a combination of return 301
and proxy_pass
, or you can let NextJS do it automatically by passing the dynamic route in the proxy_pass
directive with a trailing slash.
nginx.conf
server {
listen 80;
server_name www.cafe-one.local;
location = / {
# When a url to a route has a trailing slash, NextJS responds with a "308 Permanent redirect" to the path without the slash.
# In this case from /cafes/cafe_id_1/ to /cafes/cafe_id_1
proxy_pass http://localhost:3000/cafes/cafe_id_1/;
# If you also want to be able to pass parameters in the query string, you should use the variable $request_uri instead of "/"
# proxy_pass http://localhost:3000/cafes/cafe_id_1$request_uri;
...
}
location / {
# Any other request to www.cafe-one.local will keep the original route and query string
proxy_pass http://localhost:3000$request_uri;
...
}
}
This should work, but now we have a problem with the url in the address bar. Any user visiting www.cafe-one.local
will be redirected to www.cafe-one.local/cafes/cafe_id_1
and that doesn't look nice.
The only workaround I found to solve this issue was to use javascript to remove the path by rewriting the browsing history with window.history.replaceState()
.
pages/[cafeId]/index.js
...
export default function CafeWebsite(props) {
if (typeof window !== "undefined") {
window.history.replaceState(null, "", "/")
}
...
if you don't want to remove the path for all domains, you can use window.location.hostname
to check the current url.
...
export default function CafeWebsite(props) {
if (typeof window !== "undefined") {
const hostname = window.location.hostname
const regex = /^(www\.my-domain\.|my-domain\.)/
if (!regex.test(hostname)) {
window.history.replaceState(null, "", "/")
}
}
...
Upvotes: 1