Reputation: 2382
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
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
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
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