se22as
se22as

Reputation: 2382

Specify URL path as a variable for a REACT SSR applications built manually (i.e. NOT using NextJS )

Note: I am not using NextJS for my SSR application. I also did not use create-react-app to create my application.

I have a React Server Side Rendered (SSR) application which was hand built (as apposed to using a tool like create-react-app). I use WebPack to bundle up the server side code and the client side code. I followed the excellent Udemy course https://www.udemy.com/course/server-side-rendering-with-react-and-redux/ to understand how to create a React SSR application

My Application

Application structure

enter image description here

webpack.base.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/i,
        use: 'file-loader',
      },
      {
        test: /\.(js|jsx)$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          cwd: __dirname,
        },
      },
    ],
  },

  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

webpack.client.config.js

const path = require('path');
const { merge } = require('webpack-merge');
const CopyPlugin = require('copy-webpack-plugin');
const baseConfig = require('./webpack.base.config.js');

const config = {

  entry: './src/client/client.jsx',

  output: {
    filename: 'client-bundle.js',
    path: path.resolve(__dirname, 'public'),
  },

  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, 'src', 'styles'),
          to: path.resolve(__dirname, 'public'),
        },
      ],
    }),
  ],
};

module.exports = merge(baseConfig, config);

webpack.server.config.js

const path = require('path');
const { merge } = require('webpack-merge');
const webpackNodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base.config.js');

const config = {
  target: 'node',

  entry: './src/server/server.js',

  output: {
    filename: 'server-bundle.js',
    path: path.resolve(__dirname, 'build'),
  },

  externals: [webpackNodeExternals()],
};

module.exports = merge(baseConfig, config);

Routes

{
    ...Home,
    path: '/',
    exact: true,
},
{
    ...Page1,
    path: '/page1',
    exact: true,
},

Client Side Routing

<BrowserRouter>
  ...
</BrowserRouter>

Server Side Routing

<StaticRouter context={context} location={req.path}>
    ...
</StaticRouter>

Server Side generated HTML template

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="styles.css">
  </head>
  <body>
    <div id="root">${content}</div>
    <script src="client-bundle.js"></script>
  </body>
</html>

package.json scripts

"scripts": {
  "start": "node build/server-bundle.js",
  "build": "npm-run-all --parallel prod:build*",
  "prod:build-server-bundle": "webpack --config webpack.server.config.js",
  "prod:build-client-bundle": "webpack --config webpack.client.config.js",

  "dev": "npm-run-all --parallel dev:*",
  "dev:run-server": "nodemon --watch build --exec node build/server-bundle.js",
  "dev:build-server-bundle": "webpack --config webpack.server.config.js --watch",
  "dev:build-client-bundle": "webpack --config webpack.client.config.js --watch",
  "lint": "eslint ./src --ext .js,.jsx"
},

Running my application

I run the application locally using

npm run dev

My application URLs are therefore

http://localhost:/     
http://localhost:/page1

My Requirements

I would like my application to have a customizable URL path, for example "/a/b" so that my URLs would be

http://localhost:/a/b 
http://localhost:/a/b/page1

or if my path is "xyz" my URLs would be

http://localhost:/xyz 
http://localhost:/xyz/page1

How to i enable a custom base path in my React SSR Application.

What i tried

I hardcoded a path in my application in the HTML, and routers, i..e

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="a/b/styles.css">
  </head>
  <body>
    <div id="root">${content}</div>
    <script src="a/b/client-bundle.js"></script>
  </body>
</html>


<BrowserRouter basename="a/b/">     
  ...
</BrowserRouter>


<StaticRouter context={context} location={req.path} basename="a/b/"> 
   ...
</StaticRouter>

But this does not work, going to either of the following

http://localhost http://localhost/a/b

renders my home page with no stylesheet applied and no client side bundle. This is because neither of the following can be found and return a 404

http://localhost/a/b/styles.css
http://localhost/a/b/client-bundle.js

Furthermore, if i use a link to invoke the router, the URL for the styles and client-bundle has the path twice, i.e.

client side navigation to 
   http://localhost:8080/a/b/contact

means styles and client-bundle request urls are
    http://localhost/a/b/a/b/styles.css
    http://localhost/a/b/a/b/client-bundle.js

Upvotes: 0

Views: 1987

Answers (2)

se22as
se22as

Reputation: 2382

Hassaan Tauqir's post above which I have marked as the answer helped me refine the solution. Thank you to Hassaan.

package.json

Change the scripts for the PRODUCTION environment to have the BASE_URL specified.

"prod:build-server-bundle": "cross-env BASE_URL=/a/b webpack --config webpack.server.config.js",
"prod:build-client-bundle": "cross-env BASE_URL=/a/b webpack --config webpack.client.config.js",

Note: You have to use "cross-env" otherwise this will not work on every operating system, therefore I had to install "cross-env" first

npm install cross-env

I left the DEVELOPMENT scripts unchanged as I do not need a path when testing locally

"dev:build-server-bundle": "webpack --config webpack.server.config.js --watch",
"dev:build-client-bundle": "webpack --config webpack.client.config.js --watch",

webpack.base.config.js

Reading in BASE_URL

The "BASE_URL" is accessible in "webpack.base.config.js". I added code so that I can handle the "BASE_URL" being specified with a trailing slash or not.

// Set up the BASE_URL parameter, ensuring it does not have a trailing slash
let BASE_URL = '';
if (process.env.BASE_URL) {
  BASE_URL = process.env.BASE_URL.toString().trim();
  if (BASE_URL.substr(-1) === '/') {
    BASE_URL = BASE_URL.substr(0, BASE_URL.length - 1);
  }
}

publicPath

In the "module.exports" add the "output" section and add the "publicPath" setting. "publicPath" allows you to specify the base path for all the assets within your app, for example I have images which I reference in my application using the following code.

import myImage from '../images/myImage.png';
....
<img src={myImage } alt="myImage " />
....
 

"publicPath" must end in a trailing slash, therefore if we have a BASE_URL I append a / otherwise I leave it empty.

output: {
    publicPath: (BASE_URL) ? `${BASE_URL}/` : '',
},
  

For more information on "publicPath" see https://webpack.js.org/guides/public-path/

webpack.DefinePlugin

In the "module.exports" add the "webpack.DefinePlugin" setting the environment variable to be passed through to the reset of the application

plugins: [
    new webpack.DefinePlugin({
      'process.env.BASE_URL': JSON.stringify(BASE_URL),
    }),
],    

For more information on "DefaultPlugin" see https://webpack.js.org/plugins/define-plugin/

Server Side Routing

Add the "basename" to the server side router, whose value is the variable specified in the "DefinePlugin" in the webpack config file

<StaticRouter context={context} location={req.path} basename={process.env.BASE_URL}>
    ...
</StaticRouter>

Client Side Routing

Add the "basename" to the client side router, whose value is the variable specified in the "DefinePlugin" in the webpack config file

<BrowserRouter basename={process.env.BASE_URL}>
    ...
</BrowserRouter>

Upvotes: 0

Hassaan Tauqir
Hassaan Tauqir

Reputation: 2742

You can simply add an env variable basePath and then use that to set your routes.

Routes

{
    ...Home,
    path: `${process.env.basePath}/`,
    exact: true,
},
{
    ...Page1,
    path: `${process.env.basePath}/page1`,
    exact: true,
},

Now, if your basePath is '/a/b', your index component will be available on yourdomain/a/b/ and page1 will be available on yourdomain/a/b/page1

Upvotes: 1

Related Questions