Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Next apps on single domain will collide #257

Closed
seldo opened this issue Nov 15, 2016 · 79 comments
Closed

Multiple Next apps on single domain will collide #257

seldo opened this issue Nov 15, 2016 · 79 comments

Comments

@seldo
Copy link
Contributor

seldo commented Nov 15, 2016

Next uses routes like /_next and /static by default. If you wanted to run multiple different Next apps on the same domain, they would collide with each other. Can these paths be made configurable?

@rauchg
Copy link
Member

rauchg commented Nov 16, 2016

I believe I opened an issue for mounting, or I should have otherwise. The idea is that you'll be able to supply a prefix in the next config (like /some/sub/app), and then you can merge next apps at the proxy level.

@rauchg
Copy link
Member

rauchg commented Nov 16, 2016

There are a few caveats however. For example, <img src="/static/logo.png"> won't work. Maybe we should introduce this.context.next.prefix or something like that.

cc @nkzawa

@seldo
Copy link
Contributor Author

seldo commented Nov 16, 2016

Configurable prefixes would work perfectly for our use case.

@nkzawa
Copy link
Contributor

nkzawa commented Nov 17, 2016

The problem of this.context.next.prefix is the value goes wrong when you load components from other app using <Link/>.

For example, when there are two apps which mounted to /a and /b.
<img src={this.context.next.prefix + '/static/logo.png'} /> becomes <img src="/a/static/logo.png" /> or <img src="/b/static/logo.png" /> depends on which mounted app the component is loaded.

@rauchg
Copy link
Member

rauchg commented Nov 17, 2016

Another option is a compile-time thing.

import { pathPrefix } from 'next/env'

and we can replace it with webpack

@nkzawa
Copy link
Contributor

nkzawa commented Nov 17, 2016

Another options is to fetch the value on .json request.

/b/page.json?pathPrefix=1

// response
{
  "component": "...",
  "pathPrefix": "/b"
}

Maybe we can detect if it's an external component by comparing to own pathPrefix and automatically add the query parameter to the request.

@cncolder
Copy link
Contributor

cncolder commented Jan 5, 2017

Cannot waiting this option.

Now I extends next/dist/server and override defineRoutes().
extends NextScript in next/document and override render().

Client side I monkey path XMLHttpRequest.prototype.open

@rauchg
Copy link
Member

rauchg commented Jan 8, 2017

We won't be implementing it in the short term. If anyone wants to work on it, we could certainly provide feedback on suggested designs

@albinekb
Copy link
Contributor

albinekb commented Jan 16, 2017

@rauchg

There are a few caveats however. For example, won't work. Maybe we should introduce this.context.next.prefix or something like that.

Maybe by using the <base>tag? 🤔
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

@JohnLindahlTech
Copy link

@cncolder Do you have a gist or something illustrating your overrides?
We need this functionality at our site as well.

@ianatha
Copy link

ianatha commented Feb 6, 2017

@JohnPhoto, these are the monkey-patches we've done to support this. It assumes that _document only gets rendered on the server.

In _document.js:

import Document, { Head, Main, NextScript } from 'next/document'
import htmlescape from 'htmlescape'
import Router from 'next/router'

class PrefixedNextScript extends NextScript {
  render () {
    const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
    let { buildId } = __NEXT_DATA__

    return <div>
      {staticMarkup ? null : <script dangerouslySetInnerHTML={{
        __html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
      }} />}
    <script dangerouslySetInnerHTML={{
            __html:
`
__NEXT_DATA__.prefix = "` + this.props.prefix + `";
var open = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function() {
  if (arguments[1].indexOf("/_next/") == 0 || arguments[1].indexOf("/__webpack_hmr") == 0) {
    arguments[1] = __NEXT_DATA__.prefix + arguments[1];
  }
  open.apply(this, arguments);
};
` }} />
      { staticMarkup ? null : <script type='text/javascript' src={this.props.prefix + `/_next/${buildId}/commons.js`} /> }
      { staticMarkup ? null : <script type='text/javascript' src={this.props.prefix + `/_next/${buildId}/main.js`} /> }
    </div>
  }
}

const ROOT_URL = (process.env.ROOT_URL ? process.env.ROOT_URL : "");

export default class MyDocument extends Document {
  render () {
    return (
      <html>
        <Head>
          <title>Title Here</title>
          <link rel="stylesheet" type="text/css" href={ROOT_URL + "/static/main.css"} />
        </Head>
        <body>
          <Main />
          <PrefixedNextScript prefix={ROOT_URL} />
        </body>
      </html>
    )
  }
}

Router.ready(() => {
  if (!Router.router.change_monkeypatched) {
    let router_change = Router.router.change;
    Router.router.change = function(a, b, c) {
      arguments[1] = "" + arguments[1]
      arguments[2] = __NEXT_DATA__.prefix + arguments[2]
      return router_change.apply(this, arguments);
    }
    Router.router.change_monkeypatched = true;
  }
});

Hope this helps.

@davidnguyen11
Copy link
Contributor

davidnguyen11 commented Apr 21, 2017

Above solution is not working in version 2.1.1

Here is the replacement which working in next@2.1.1:

import Document, { Head, Main, NextScript } from 'next/document';

class PrefixedNextScript extends NextScript {
  render() {
    const { __NEXT_DATA__ } = this.context._documentProps;
    const { prefix } = this.props;

    const env = process.env.NODE_ENV;

    // script element contain development js
    let scriptElements = (
      <div>
        <script type="text/javascript" src={`${prefix}/_next/-/manifest.js`} />
        <script type="text/javascript" src={`${prefix}/_next/-/commons.js`} />
        <script type="text/javascript" src={`${prefix}/_next/-/main.js`} />
      </div>
    );

    if (env === 'production') {
      const { buildStats } = __NEXT_DATA__;
      const app = 'app.js';

      // script element contain production js
      scriptElements =
        <script
          type="text/javascript"
          src={`${prefix}/_next/${buildStats[app].hash}/${app}`}
        />
    }

    return (
      <div>
        <script
          dangerouslySetInnerHTML={
            { __html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};` }
          }
        />
        {scriptElements}
      </div>
    );
  }
}

Here is how to use it

<PrefixedNextScript prefix={config.prefixResource} />

this script will replace

<NextScript />

in _document.js

@tpai
Copy link

tpai commented May 11, 2017

This issue should be close, 2.2.0 release assetPrefix config flag.

@arunoda
Copy link
Contributor

arunoda commented May 11, 2017

@tpai does the assetPrefix fix this issue.
I like to learn more about it.
(Anyway, this was not our intention, it this fixes that would be awesome)

@davidnguyen11
Copy link
Contributor

Hi @arunoda.

assetPrefix does not fix the issues. It only give a prefix to resources in static dir. If it can support bundle js. If so is going to be great.

For example, when run command to build in production mode. The assetPrefix does not include the prefix to that script.

@tpai
Copy link

tpai commented May 18, 2017

@nndung179 assetPrefix does support bundle js, you could check this server/document.js:46, and REAMDE also mentioned.

@arunoda Not perfectly fixed, but it still work.

next.config.js

module.exports = {
  assetPrefix: '/prefix/'
};

customServer.js

express.use((req, res) => {
  req.url = req.url.replace('/prefix', '');
  handle(req, res);
});

@tscanlin
Copy link
Contributor

I had a PR to fix this on the frontend side as well so you wouldn't need custom server logic for this:
#2002

@janbaer
Copy link

janbaer commented May 25, 2017

That's indeed a big issue. Since Next.js is provided as a provided as a public framework for hosting server rendered React pages in an easy way, it should be possible also to host the solution on a subdomain. We decided to use Next.js for our next admin-webapp, but this will be hosted under a subdomain like www.some-domain.com/admin and this would actually not possible or am I wrong?

@davidnguyen11
Copy link
Contributor

@janbaer it actually possible. I have met the same problem like you. I used the solution above to solve. But I believe in the future it will be solved

@wesbos
Copy link
Contributor

wesbos commented May 26, 2017

If anyone else is trying to run next on a sub-route with express, I had to use this express route to re-write the HMR pings:

app.use('/_next/*', (req, res) => {
  const newURL = req.originalUrl.replace('_next', 'admin/_next');
  res.redirect(newURL);
});

@timneutkens
Copy link
Member

Thanks for that solution @wesbos 👌 Pretty sure it'll help people 👍

@janbaer
Copy link

janbaer commented May 27, 2017

@nndung179 @wesbos Thanks for your solutions, I'll try it. But I think it should be a feature of the framework, like the feature with for the CDN support.

@janbaer
Copy link

janbaer commented May 29, 2017

I tried to use the server side solutions from @wesbos and @tpai and it worked for me locally somehow. But in case it would run on a server with Nginx as proxy-server which is only the /admin requests proxy to my Next.js page it wouldn't work, since the request to /_next wouldn't arrive in my server.js.

@zenflow
Copy link
Contributor

zenflow commented Feb 8, 2018

The multi-zones branch is 404ing 😞 https://github.com/zeit/next.js/tree/multi-zones

@zenflow
Copy link
Contributor

zenflow commented Feb 8, 2018

@timneutkens It looks like the multi-zones branch was merged, since there's now a Multi Zones section in the README? Is this issue resolved?

/cc @moaxaca

@tomaswitek
Copy link
Contributor

Hi @zenflow, yep it was merged and released in 5.0.0.
So yes, this issue can be probably closed 😏

@timneutkens
Copy link
Member

Yep this should be closed 🎉

@joshuaquek
Copy link

I used:

In my routes.js file:

// ---- API Routes ----
const express = require('express')
const router = express.Router()

// -------- NextJs API Routes --------
router.get(`*`,  (req, res, next) => {
  return app.getRequestHandler()  // where app is nextjs app object
})

module.exports = router

In my server.js file:

const server = require('express')()
let routes = require('routes.js')
server.use('/my_custom_base_url', routes)

server.listen(port, (error) => { // Start server to listen on specified port
  if (error) throw error
  console.log('Server started.')  
})

and in my next.config.js file:

module.exports = {
  assetPrefix: '/my_custom_base_url'
  publicRuntimeConfig: { 
    staticFolder: `/my_custom_base_url/static`
  },
  webpack: (config) => {
    // Fixes npm packages that depend on `fs` module
    config.node = {
      fs: 'empty'
    }
    return config
  }
}

This fixed all of my routing issues and will make the base url of the entire nextjs webapp to have a base url of '/my_custom_base_url'

My version of NextJs is next@7.0.2

Hope this helps!

I have also mentioned this as a solution to Issue #2901

@bgarrett-rt
Copy link

bgarrett-rt commented Dec 9, 2018

For anyone being redirected to this issue ticket trying to just get next mounting to a subdirectory (such as from #2452), here's what finally ended up working for me.

// next.config.js
module.exports = {
   ...
 // With this line, next will try to load routes like `/my_custom_base_url/_next/static/development/pages/_app.js` but they will 404 because next is expecting /_next/ at the root of the url.
  assetPrefix: '/my_custom_base_url',
  ...
}

// We can use middleware to rewrite the url so next handles it as if it wasn't in a subdirectory i.e. /my_custom_base_url/_next/static/development/pages/_app.js => /_next/static/development/pages/_app.js

// server.js - using express.js - app is instance of `next({...})`. Note: Due to scope next() is not the same var nor type as next() in the 3rd argument of the middleware below
app.prepare().then(() => {
  const server = express();
  
  // The rewire middleware - not. req.url is mutable but req.originalUrl isn't, or at least has no effect if mutated
  server.use(function(req, res, next) {
     req.url = req.originalUrl.replace('my_custom_base_url/_next', '_next');
     next(); // be sure to let the next middleware handle the modified request. 
  });

  server.get('*', (req, res) => {
    return handle(req, res);
  });
  ... 

I have not thoroughly tested yet, but this worked when deployed as well. For my use case, I have only one next.js app and it is merely accessible via /my_custom_base_url. Due to service routing, requests above /my_custom_base_url/ (i.e. /_next/) are not intercepted at all by the next service. Hope this helps save someone else an afternoon of head scratching.

@hect1c
Copy link

hect1c commented Dec 29, 2018

Hey,

I'm currently facing the same problem where I have a basePath and need to have the site working on http://domain/basePath

I've tried both solutions above but it doesn't seem to be working. Without the basePath it works fine, but with it I still get a 404 page not found. Any ideas what I could be missing?

Below is my server.js

const next = require('next')
const server = require('express')()

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
    server.use((req,res, next) => {
        req.url = req.originalUrl.replace('teachonline/_next', '_next');
        next();
    });

    server.get('*', (req, res) => {
        return handle(req,res);
    });

    server.listen(port, err => {
        if (err) throw err
        console.log(`>> Ready on http://localhost:${port}`)
    });
});

@unski11ed
Copy link

unski11ed commented Jan 9, 2019

Hey I have a similiar situation with basePath, but can't get out of it. I have tried my own solution at first, but when it didn't work 100% I used @bgarrett-rt 's code for the server.

server.js

const express = require('express');
const compression = require('compression');
const next = require('next');
const { config } = require('./env-config');

const dev = config['NODE_ENV'] !== 'production';
const PORT = config['PORT'] || (dev ? 4100 : 80);
const ROOT_PATH = config['ROOT_PATH'] || '/';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
    const server = express();

    server.use(compression());
    server.use((req, res, next) => {
        req.url = dev ? req.originalUrl :
            req.originalUrl.replace(ROOT_PATH, '');

        next();
    })
    server.get('*', (req, res) =>
        handle(req, res));

    server.listen(PORT, err => {
        if (err) throw err;
        //eslint-disable-next-line
        console.log(`> Ready on http://localhost:${PORT}`);
    });
});

next.config.js

const withPlugins = require('next-compose-plugins');
const sass = require('@zeit/next-sass');
const css = require('@zeit/next-css');
const fonts = require('next-fonts');
const images = require('next-images');
const { config } = require('./env-config');

const dev = config['NODE_ENV'] !== 'production';
const rootPath = dev ? '' : config['ROOT_PATH'];

module.exports = withPlugins([
    [sass], [css], [fonts], [images]
], {
    publicRuntimeConfig: {
        rootPath
    },
    assetPrefix: rootPath
});

The initial load works fine - no 404's whatsoever, but after clicking any link it just loads the page infinitely. While watching the requests, all the needed chunks are loaded succesfully, it just looks like if after loading of the chunks something goes wrong and the page isn't being rendered.

I guess that it is also worth noting that i'm using the rootPath config option in my Link Wrapper which just adds the rootPath to href. They look ok, just as I need: http://domain.com/basePath/pagesDir/pagesDirComponent.

Can someone point me in some kind of direction? I'm banging my head at this for half a day now...

@godmar
Copy link

godmar commented May 14, 2019

First time experience with next.js, was disappointed that building + copying to personal web directory didn't work out of the box. For anyone googling this in the future, I'll leave a note to hopefully save them the hour I spent trying to google this. To get a quick "see what next.js means by deployment" view, you must create a next.config.js file like this as pointed out earlier in this thread:

$ cat next.config.js 
module.exports = {
  distDir: 'build',
  assetPrefix: '/~yourpid/out'
};

then you can make the setup example and run

npx next build && npx next export
/bin/cp -R out ~/public_html/out

which will allow you to see something at shared.institutional.edu/~yourpid/out assuming your sys admins have standard userdir functionality set up in your institution's Apache or nginx.

This doesn't solve the larger issue(s) raised in this thread, but at least will allow a newcomer to see what next.js calls a "production build." Note that unlike CRA next.js requires both a build and an export step; 'build' here doesn't build anything that could be deployed. The main documentation doesn't mention this step directly, but the extra step of setting assetPrefix is hidden in a page titled Deploying a Next.js app into GitHub Pages linked from there for people wanting to deploy on github pages. I would be kind of nice if the default setup didn't assume domain root access and used relative paths only, but perhaps I'm stuck in the 1990s and in any event would apply only to 100% static applications which next.js perhaps doesn't target.

@tanobi92
Copy link

tanobi92 commented Jul 29, 2019

If anyone else is trying to run next on a sub-route with express, I had to use this express route to re-write the HMR pings:

app.use('/_next/*', (req, res) => {
  const newURL = req.originalUrl.replace('_next', 'admin/_next');
  res.redirect(newURL);
});

When I deploy to server, then the states in redux be lost after change page. How to fix it? @wesbos

@alxhghs
Copy link

alxhghs commented Oct 31, 2019

Had a hard time figuring this out initially. Wrote a blog post on the solution https://www.alexhughes.dev/blog/multiple-next-apps/

@kepeterson
Copy link

Hey I have a similiar situation with basePath, but can't get out of it. I have tried my own solution at first, but when it didn't work 100% I used @bgarrett-rt 's code for the server.

server.js

const express = require('express');
const compression = require('compression');
const next = require('next');
const { config } = require('./env-config');

const dev = config['NODE_ENV'] !== 'production';
const PORT = config['PORT'] || (dev ? 4100 : 80);
const ROOT_PATH = config['ROOT_PATH'] || '/';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
    const server = express();

    server.use(compression());
    server.use((req, res, next) => {
        req.url = dev ? req.originalUrl :
            req.originalUrl.replace(ROOT_PATH, '');

        next();
    })
    server.get('*', (req, res) =>
        handle(req, res));

    server.listen(PORT, err => {
        if (err) throw err;
        //eslint-disable-next-line
        console.log(`> Ready on http://localhost:${PORT}`);
    });
});

next.config.js

const withPlugins = require('next-compose-plugins');
const sass = require('@zeit/next-sass');
const css = require('@zeit/next-css');
const fonts = require('next-fonts');
const images = require('next-images');
const { config } = require('./env-config');

const dev = config['NODE_ENV'] !== 'production';
const rootPath = dev ? '' : config['ROOT_PATH'];

module.exports = withPlugins([
    [sass], [css], [fonts], [images]
], {
    publicRuntimeConfig: {
        rootPath
    },
    assetPrefix: rootPath
});

The initial load works fine - no 404's whatsoever, but after clicking any link it just loads the page infinitely. While watching the requests, all the needed chunks are loaded succesfully, it just looks like if after loading of the chunks something goes wrong and the page isn't being rendered.

I guess that it is also worth noting that i'm using the rootPath config option in my Link Wrapper which just adds the rootPath to href. They look ok, just as I need: http://domain.com/basePath/pagesDir/pagesDirComponent.

Can someone point me in some kind of direction? I'm banging my head at this for half a day now...

Were you able to make any progress on this? I seem to be experiencing the exact same issue. I'm re-writing my urls the same way you are so I can use a ROOT_PATH. Nothing 404s on initial load but when I try to use a <Link/> nothing happens. If I navigate to the route from the address bar, everything is fine.

@vinodloha
Copy link

vinodloha commented Jan 14, 2020

Hi all, whoever is facing this problem this below solution has worked absolutely spot-on for me in a non Now environment. This guides you to achieve Zones like capability that Zeit Now provides.

https://levelup.gitconnected.com/deploy-your-nextjs-application-on-a-different-base-path-i-e-not-root-1c4d210cce8a

One improvement i did, was to create a routes.js file which just returns an object of routes, so that i don't have to create wrapper components to prepend base path prefix.

@kepeterson
Copy link

Hi all, whoever is facing this problem this below solution has worked absolutely spot-on for me in a non Now environment. This guides you to achieve Zones like capability that Zeit Now provides.

https://levelup.gitconnected.com/deploy-your-nextjs-application-on-a-different-base-path-i-e-not-root-1c4d210cce8a

One improvement i did, was to create a routes.js file which just returns an object of routes, so that i don't have to create wrapper components to prepend base path prefix.

Does hot-loading work in development mode? And do Link components work?

@vinodloha
Copy link

Hi all, whoever is facing this problem this below solution has worked absolutely spot-on for me in a non Now environment. This guides you to achieve Zones like capability that Zeit Now provides.
https://levelup.gitconnected.com/deploy-your-nextjs-application-on-a-different-base-path-i-e-not-root-1c4d210cce8a
One improvement i did, was to create a routes.js file which just returns an object of routes, so that i don't have to create wrapper components to prepend base path prefix.

Does hot-loading work in development mode? And do Link components work?

Yes, hot reloading works as usual. As far as Link component is concerned, you should write a wrapper that adds base_path to all your links. This blog I referenced has example for it.

@Naxos84
Copy link

Naxos84 commented Jul 27, 2020

Hi all, whoever is facing this problem this below solution has worked absolutely spot-on for me in a non Now environment. This guides you to achieve Zones like capability that Zeit Now provides.

https://levelup.gitconnected.com/deploy-your-nextjs-application-on-a-different-base-path-i-e-not-root-1c4d210cce8a

One improvement i did, was to create a routes.js file which just returns an object of routes, so that i don't have to create wrapper components to prepend base path prefix.

@vinodloha
How does that work with dynamic routes?
like "/posts/1234" as "/posts/[id]"

@Timer
Copy link
Member

Timer commented Jul 27, 2020

Next.js 9.5 was just released with support for a Customizable Base Path, allowing you to mount multiple Next.js apps on one domain, so long as their subpaths are unique!

@marlonmleite
Copy link

@Timer if you want to change only the prefix/path of the static files?
My app run on / and the assets and files come from /any _next for example.
It's possible?

@arihantverma
Copy link

arihantverma commented Jul 13, 2021

@Timer @timneutkens , as the original request says, we have multiple next apps running on the same domain. When we use base path and use proxy passes to handle which request should go to which Next app most of the things work. The one problem that we aren't able to mitigate is to keep links 'clean'. Say our basepath is app1. All our problems with asset prefix, api route path prefix, images path prefix is resolved, except that our links show app1/about instead of /about. Is there a way to leverage as Link prop with basepath so that we can keep our anchor links the same?

The way we tried to solve this was to manually have rewrites for assets, images and api routes, something like this:

{
  assetPrefix: 'app-1',
   async rewrites() {
    return [
      /* for assets */
      {
        source: `/app-1/_next/:path*`,
        destination: '/_next/:path*',
      },
      /* for images */
      {
        source: `/app-1/images/:query*`,
        destination: '/_next/image/:query*',
      },
      {
       /* for api routes */
        source: `/app-1/api/:path*`,
        destination: '/api/:path*',
      },
    ]
  },
}

This way our original links remain as is. But the problem with this is that we have no way to write a re write rule for /_next/data* api calls happening between route transitions.

One solution I'm trying to think of is to catch these api calls in service worker, re route them to /app-1/_next/data* and write a re write rule to serve them from /_next/data*

Last resort is to use custom server, but we are trying to find if there's a workaround or solution without doing that, since some of our pages use getStaticProps.

@arihantverma
Copy link

arihantverma commented Jul 14, 2021

_next/data/<build-id>/<route_slug>.json is how data URLs look like. the <build-id> is a unique build id next.js creates for each build.

We ended up using next-build-id to configure a custom build id where we gave a app-1-<commitSHA> signature and added a re write rule in our load balancer for incoming request host name having app-1, to solve this.

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests