vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.69k stars 26.94k forks source link

Multiple Next apps on single domain will collide #257

Closed seldo closed 6 years ago

seldo commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

Configurable prefixes would work perfectly for our use case.

nkzawa commented 7 years ago

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 commented 7 years ago

Another option is a compile-time thing.

import { pathPrefix } from 'next/env'

and we can replace it with webpack

nkzawa commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

ianatha commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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

arunoda commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

I had a PR to fix this on the frontend side as well so you wouldn't need custom server logic for this: https://github.com/zeit/next.js/pull/2002

janbaer commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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

janbaer commented 7 years ago

@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 commented 7 years ago

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.

manolkalinov commented 7 years ago

This is actually a make or break issue if Next.js is to be used in a real production environment. @tscanlin's fix is only two lines long. Is there a way I/we can help get his fix in right now or at least formulate a temporary workaround?

tscanlin commented 7 years ago

@manolkalinov glad to hear my fix is working for you! Could you comment on the PR I opened to help make a case for getting this change landed? I'll add a comment too. https://github.com/zeit/next.js/pull/2002

wesbos commented 7 years ago

Finding issues with my method - the webpack HMR breaks and they live-reload takes 1-2 seconds instead of being instant. I guess because you can't proxy the eventsouce through express.

vanmik commented 7 years ago

I'm also trying to make next.js app work from subfolder. I add appPrefix in config, and now all _next/* links in Githubissues.

  • Githubissues is a development platform for aggregating issues.