vercel / next.js

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

Feature request: Basepath support #4998

Closed tomaswitek closed 4 years ago

tomaswitek commented 6 years ago

Feature request

Is your feature request related to a problem? Please describe.

Multi zones is a great feature which allows to run multiple next.js apps on the same domain, but it doesn't allow to define a basepath which will be accepted by all parts of next.js. Since we are not able to namespace apps right now it is not possible to have the same names for pages in various apps.

Describe the solution you'd like

I want to be able to configure a basepath in the next.config.js file. Thanks to this configuration all parts of next.js (Router, Link, Static assets etc.) will be aware of the basepath and will automatically generate and match to the correct paths.

Describe alternatives you've considered

One alternative is to nest all desired pages into a folder which matches the basepath. This solves just one small issue with routing and is quite ugly because most of the my basepaths are not one level paths. The second alterantive is to configure a proxy in a way where the basepath is automatically removed before the request arrives into a next.js app and also implement a custom Link component which automatically adds basepath to all links. I just don't want to maintain custom fork of next.js. It doesn't make sense in my opinion.

Additional context

The assetPrefix solution allows us to define a different prefix for each app. But as fair as I know it works only with different hosts.

with-zones example

module.exports = {
  assetPrefix: NOW_URL ? `https://${alias}` : 'http://localhost:4000'
}

If I add a basepath to it everything fails

module.exports = {
  assetPrefix: NOW_URL ? `https://${alias}/account` : 'http://localhost:4000/account'
}
screen shot 2018-08-21 at 10 47 08

In my opinion we should split it into 2 variables:

module.exports = {
  assetPrefix: NOW_URL ? `https://${alias}` : 'http://localhost:4000',
  basepath: '/account'
}

Related issues

foxundermoon commented 4 years ago

static export also need basePath 😊

seems work success 👏

{
  experimental:{
    basePath: '/some/dir',
  }
}
jooj123 commented 4 years ago

This is pretty bad limitation for us unfortunately :(

We have all apps behind a reverse proxy so paths need to be prefixed (in the example below this is prefix of /auction-results)

We use assetPrefix prefix already - and this allows apps to run ok for server side requests. Eg: mydomain.com/auction-results/ works fine by using some express routing like such:

router.get(`/${appPrefix}/`, (req, res) => {
  nextApp.render(req, res, '/national', req.params);
});

But when we try and do client side nav via next/link, eg:

Where /auction-results is the application prefix, and /national is the page in ~pages/national

<Link href="/national" as="/auction-results/">
  <a>Goto National Page</a>
</Link>

This does nothing (ghost click)

Having full page refresh links is less than ideal.

If there is any way i can help with this I would love to

aysark commented 4 years ago

Any update on this... last year around this time i ran into this issue. Now a year later, i'm working on a new app and have to do the same workarounds i did last year... kinda alarming for a 'production-ready' react fw. Basepaths should be a vanilla feature.

timneutkens commented 4 years ago

Any update on this... last year around this time i ran into this issue. Now a year later, i'm working on a new app and have to do the same workarounds i did last year... kinda alarming for a 'production-ready' react fw. Basepaths should be a vanilla feature.

I'm not sure what you're expecting by posting this.

Next.js is being worked on full-time by my team (5 people), and we're working on many features at the same time. In the past year we've worked on these:

Effectively making Next.js applications (new and existing) significantly smaller, faster and more scalable.

If you want to voice your "upvote" for a feature you can. use the 👍 feature on the initial thread.

I definitely agree basePath should be a built-in feature. It's on the roadmap already and I even wrote an initial PR, which you could have seen by reading back on the thread.

Here's the PR: https://github.com/zeit/next.js/pull/9872

Feel free to reach out to enterprise@vercel.com if you want to financially contribute to making this feature happen.

Sletheren commented 4 years ago

What is the Status on this? we are really depending on this :/

martpie commented 4 years ago

@Sletheren basePath support is experimental right now, use at your owm risks.

cf. https://github.com/zeit/next.js/pull/9872

Sletheren commented 4 years ago

@Sletheren basePath support is experimental right now, use at your own risks.

cf. #9872

@martpie I already saw it, but for. my case basePath is not just one, it can be multiple basePath, since we serve our app through different "URLs" and setting up basePath during build time is not an option (even though it has to support an array of paths rather than a single string)

pe-s commented 4 years ago

@timneutkens Thanks for the update. Would you be so kind to give another update. This is for us a key feature and we need to know...

  1. Will this be an enterprise-only (your reference to contact enterprise sales caused some irritation)?

  2. It seems to be on the roadmap, according to the PR it won't be removed again; can you give some indication if it's safe to build around this feature now without getting any surprises in the next months like a crippled open source version and another one with full support after we negotiated weeks with some random sales guys about arbitrary prices?

I understand that you guys work on many features and everyone has his/her priorities but even smaller setups need to proxy Next, run multiple instances and give it a dedicated basePath per service. Before we now start to build multiple services on Next we need to know how probable and soon this feature is available as full open source. Otherwise it would be just too risky invest further time into Next.

Thanks for your understanding and looking fwd to your feedback.

pe-s commented 4 years ago

FWIW, I got it now working and for others driving by:

Put this in your next.config.js:

module.exports = {
  experimental: {
    basePath: '/custom',
  },
}

Then, I needed to restart the server and to setup my web server middleware properly:

I catch all requests via a custom path, eg. app.use('/custom', (req, res...) => { ... and then (which was important) I need to proxy to the URL of the system where Next is running (so the internal address of your container orchestration and again with the respective path if you use http-proxy => eg. ... target: 'http://next:3000/custom), so not just the host without the custom path. If you use http-proxy-middleware you do not need this.

It feels quite ok, I hope that this feature won't need any EE license. If your team needs any help to get this feature mature, pls let us know, maybe we can help!

Edit: Just tried this also in with Next's production mode and it seems to work as well.

timneutkens commented 4 years ago

@timneutkens Thanks for the update. Would you be so kind to give another update. This is for us a key feature and we need to know...

  1. Will this be an enterprise-only (your reference to contact enterprise sales caused some irritation)?
  2. It seems to be on the roadmap, according to the PR it won't be removed again; can you give some indication if it's safe to build around this feature now without getting any surprises in the next months like a crippled open source version and another one with full support after we negotiated weeks with some random sales guys about arbitrary prices?

I understand that you guys work on many features and everyone has his/her priorities but even smaller setups need to proxy Next, run multiple instances and give it a dedicated basePath per service. Before we now start to build multiple services on Next we need to know how probable and soon this feature is available as full open source. Otherwise it would be just too risky invest further time into Next.

Thanks for your understanding and looking fwd to your feedback.

@pe-s I think you're misunderstanding my post.

There is no "enterprise Next.js version" as of now. I was referring to the numerous occasions where external companies reached out to pay for consulting to build out features like this one in a shorter timespan. E.g. zones support was built in collaboration with Trulia.

This feature is being worked on still and is on the roadmap. All features being worked on are open-source, like I said there's no enterprise version of Next.js. We have multiple priorities of high-impact work on the roadmap though hence why I referred to contacting enterprise@vercel.com if you need this feature as soon as possible / to discuss enterprise support for Next.js.

pe-s commented 4 years ago

@timneutkens tx for your quick response and great! Then, we can go all in :) Keep up the great work!

timneutkens commented 4 years ago

Basepath support is out on next@canary right now, it's no longer experimental. It will be on the stable channel soon.

mohsen1 commented 4 years ago

I'm pretty late to this but did you consider using actual HTML <base> instead of manually handling this?

peetjvv commented 4 years ago

Basepath support is out on next@canary right now, it's no longer experimental. It will be on the stable channel soon.

@timneutkens, thank you for this addition. Do you know when the non-experimental basePath support will be officially released?

Also, when I set the basePath the assets (located in the public folder) gets served to the appropriate url as expected. But, when I reference them in my code then I have to add the base path to the src manually, because otherwise they will still be referenced from the normal path. Is this the expected use of basePath? I have also tried using assetPrefix, but it didn't have any effect to my code that I could tell.

Example:

  1. using next v9.4.5-canary.24
  2. basePath set to /alerts in next.config.js:
    const basePath = '/alerts';
    module.exports = {
    basePath: basePath,
    env: {
    BASE_PATH: basePath,
    },
    };
  3. asset located in public/images/example.png
  4. example use of asset in react component:
    const ExampleImage = () => (
    <img src={`${process.env.BASE_PATH}/images/example.png`} />
    );
kmturley commented 4 years ago

In my tests, it's not updating assets urls.

I installed the latest canary: npm install next@9.4.5-canary.31

next.config.js

const isProd = process.env.NODE_ENV === 'production';

module.exports = {
  basePath: isProd ? '/example' : ''
}

All pages and links load correctly: http://localhost:3000/example/posts/pre-rendering http://localhost:3000/example/posts/ssg-ssr http://localhost:3000/example/posts/pre-rendering

But images, favicons etc are not mapped: http://localhost:3000/favicon.ico 404 http://localhost:3000/images/profile.jpg 404

Did anyone test this? I also tried using assetPrefix, but that didn't work either.

In addition i'm confused, why not use the built in browser functionality for this? https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

peetjvv commented 4 years ago

Thank you for looking into this on your end as well @kmturley . Glad to know it's not just me. @timneutkens , should we reopen this issue / create a new issue for this bug?

timneutkens commented 4 years ago

You have to prefix images manually. You can get the basePath using

const {basePath} = useRouter()
github0013 commented 4 years ago

https://nextjs.org/docs/api-reference/next.config.js/cdn-support-with-asset-prefix

Next.js will automatically use your prefix in the scripts it loads, but this has no effect whatsoever on the public folder;

Now, I come to realize there are multiple ways to link to files in /public. e.g. <img/> <link/> ... Is this why we have to manually specify the basePath to the each?

If there was a component like below available, I think it would save time and reduce confusions for a lot of people?

<WithinBasePath>
  {/* automatically fixes the path with basePath */}
  <img src="/logo.png" />
</WithinBasePath>
github0013 commented 4 years ago

I really don't think this is appropriate, but this is what I meant.

// src/components/WithinBasePath/index.tsx

import React from "react"
import path from "path"
import { useRouter } from "next/router"
interface Props {}

const WithinBasePath: React.FC<Props> = (props) => {
  const { basePath } = useRouter()
  const children = [props.children].flatMap((c) => c) as React.ReactElement[]
  return (
    <>
      {children.map((child, key) => {
        let newChild = null

        switch (child.type) {
          case "img":
            newChild = React.createElement(child.type, {
              ...child.props,
              src: path.join(basePath, child.props.src),
              key,
            })
            break
          case "link":
            newChild = React.createElement(child.type, {
              ...child.props,
              href: path.join(basePath, child.props.href),
              key,
            })
            break
          default:
            newChild = React.createElement(child.type, {
              ...child.props,
              key,
            })
        }
        return newChild
      })}
    </>
  )
}
export default WithinBasePath
// pages/test.tsx

import React from "react"
import WithinBasePath from "@src/components/WithinBasePath"
interface Props {}

const test: React.FC<Props> = (props) => {
  return (
    <WithinBasePath>
      <img src="/123.jpg" />
      <link href="/abc.jpg" />
      <div>other element</div>
    </WithinBasePath>
  )
}
export default test
kmturley commented 4 years ago

For those trying use const {basePath} = useRouter() which is a Hook, to work with Classes and Components and getting this error:

Invalid Hook Call Warning

https://reactjs.org/warnings/invalid-hook-call-warning.html

You can get it working using:

import { withRouter, Router } from 'next/router'

class Example extends Component<{router: Router}, {router: Router}> {
  constructor(props) {
    super(props)
    this.state = {
      router: props.router
    }
  }
  render() {
    return (
      <Layout home>
        <Head><title>Example title</title></Head>
        <img src={`${this.state.router.basePath}/images/creators.jpg`} />
      </Layout>
    )
  }
}
export default withRouter(Example)
kmturley commented 4 years ago

If you want to use basePath with markdown, it looks like you need to do a find and replace in the string:

const content = this.state.doc.content.replace('/docs', `${this.state.router.basePath}/docs`);
return (
<Layout>
  <Container docs={this.state.allDocs}>
    <h1>{this.state.doc.title}</h1>
    <div
      className={markdownStyles['markdown']}
      dangerouslySetInnerHTML={{ __html: content }}
    />
  </Container>
</Layout>
)
peetjvv commented 4 years ago

You have to prefix images manually. You can get the basePath using

const {basePath} = useRouter()

This solution doesn't take images imported in a css or scss file into account though. Do you have a solution for how to set the base path when importing an asset from within a css or scss file? With this solution we will have to ensure that all images are imported either through an img tag, inline styling or in the style tag. It's not ideal, because it will split your styles to be implemented in multiple places.

kshaa commented 4 years ago

@peetjvv Here's a suboptimal solution for using assets with prefixed basePaths in CSS. Create, import and add a <CSSVariables> component in _app.tsx, which injects a global inlined <style> element containing CSS variables, which you can then use all throughout your stylesheets.

E.g. at the opening of <body> build and inject variables:

<style>
:root {
      --asset-url: url("${basePath}/img/asset.png");
}
</style>

To get that basePath I use the @kmturley's approach using withRouter. Here's how that component could look like:

import { withRouter, Router } from "next/router";
import { Component } from "react";

export interface IProps {
  router: Router;
}

class CSSVariables extends Component<IProps> {
  render() {
    const basePath = this.props.router.basePath;
    const prefixedPath = (path) => `${basePath}${path}`;
    const cssString = (value) => `\"${value}\"`;
    const cssURL = (value) => `url(${value})`;
    const cssVariable = (key, value) => `--${key}: ${value};`;
    const cssVariables = (variables) => Object.entries(variables)
      .map((entry) => cssVariable(entry[0], entry[1]))
      .join("\n");
    const cssRootVariables = (variables) => `:root {
      ${cssVariables(variables)}
    }`;

    const variables = {
      "asset-url": cssURL(
        cssString(prefixedPath("/img/asset.png"))
      ),
    };

    return (
      <style
        dangerouslySetInnerHTML={{
          __html: cssRootVariables(variables),
        }}
      />
    );
  }
}

export default withRouter(CSSVariables);
balazsorban44 commented 2 years ago

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.