Workaround for _next/data URLs throwing 404, for multiple Next.js Apps Running On Same Domain

ยท

4 min read

Screenshot 2021-07-19 at 6.15.34 PM.png

If you have to use multiple Next.js apps on a single domain, the only straightforward way for you to do that is to have baseUrl option set in next.config.js. But the problem with that is, you'll have different URLs for different apps on your domain:

example.com/app-1/some-route
example.com/app-2/some-other-route

If you want to do have them as so:

example.com/some-route ( from app-1 )
example.com/some-other-route ( from app-2 )

you're a little out of luck. My first hunch was that it'd be possible to use the baseUrl but change the anchor links from

/app-1/some-route-on-some-page
/app-2/some-route-on-some-page

to

some-route-on-some-page
some-route-on-some-page

by using the as property of next/link by masking the URLs that users will see, while still being able to request the correct base-path-added url from next server. As much as I could google, it's not possible. If you've made it work, please tell me on the twitter, I'd be very grateful ๐ŸŒป.

I ended up making it work at my job by doing a bunch of things.

Using asset prefix to make a distinguishing factor between assets of different next.js apps, along with a next.js re write rule.

// next.config.js
module.exports = {
  assetPrefix: BASE_PREFIX_FOR_APP,
  async rewrites(){
    return [
      {
        source: `${BASE_PREFIX_FOR_APP}/_next/:path*`,
        destination: '_next/:path*'
      }
    ]
  }
}

With this, the client will request assets from ${BASE_PREFIX_FOR_APP}/_next/:path*, but it'll reach your app at a path that it serves assets from /_next/:path* ( /_next/static/* to be more precise ).

In a similar fashion, you'd handle images and api request paths

// next.config.js
module.exports = {
  assetPrefix: BASE_PREFIX_FOR_APP,
  async rewrites(){
    return [
      {
        /** ASSET PREFIX */
        source: `${BASE_PREFIX_FOR_APP}/_next/:path*`,
        destination: '/_next/:path*'
      },
      {
        /** IMAGE PREFIX */
        source: `${BASE_PREFIX_FOR_APP}/images/:query*`,
        destination: '/_next/image/:query*'
      },
      /** API PREFIX */
      {
        source: `${BASE_PREFIX_FOR_APP}/api/:path*`,
        destination: '/api/:path*'
      }
    ]
  }
}

For images you'll have to wrap next/image component in your own component, to request your images with the prefix BASE_PREFIX_FOR_APP, using a custom Next.js image loader

// CustomImage.tsx
import Image from 'next/image'

const CustomImage: typeof Image = props => {
  const finalProps = {
    props,
    loader({ src, width, quality } {
      const urlQuery = `?url=/images${src}`
      return `/${BASE_PREFIX_FOR_APP}/images${urlQuery}&w=${width}&q=${quality ?? 75}`
    })
  }

  return <Image {...finalProps} />
}

export default CustomImage;

All was well and good, and the apps were running fine with the requirements that we had: not having to change our links with app specific base path prefix. But there was still a problem.

When you use a next/link to navigate to another route, and that upcoming route has a getServerSideProps method implemented, Next.js will send an API request to the server, which will run getServerSideProps and return a JSON containing the result. You can read about this in Next.js docs here. That resulted JSON fetch request data is used to render the upcoming route. Those data fetch requests have a path that looks like this: _next/data/<build-id>/<route-slug>.json.

The problem with that for our context โ€” to be able to run multiple Next.js apps on same domain without base url prefix โ€” is that Next.js doesn't give us a way to control this path. Which is to say there's no data fetch request path URL prefix which Next.js gives as a configuration option. Because of that we have a hard time finding a distinguishing factor for data URLs for multiple apps.

The fix we ended up using is as follows:

Remember that the data fetch url looks like _next/data/<build-id>/<route_slug>.json. If we could have a way generate unique <build-id> for all our apps, we could use that and write a re write rule at load balancer level to distinguish the request between multiple Next.js apps. By default the <build-id> is a random id generated by Next.js build process for each build. Thankfully they give us a way to control the build id.

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

Hope this was helpful.