Dynamically updating index.html file in production build of a create react app - reactjs

My client app is built with Create React App, which comes which lots of conveniences built-in. However it's not possible to do server side rendering out of the box. As a result, once the build command is run, all of the output is effectively static.
I have a use case where I do NOT want to switch to complete SSR setup, but would like to dynamically add some data to index.html file so that it is immediately available in javascript when client first loads file.
I worked out the following solution:
React app runs as a docker container, using the serve lib to serve static build content
A separate node service runs in a different docker container and has access to the build content from the react app via a shared volume
The node service runs a function every few minutes that reads the contents of index.html file using fs, inserts some additional data into a script tag (e.g. window.myData={someKey: 'someValue'}), and writes the updated string to index.html.
Locally using docker-compose, this works great. However, I'm wondering about possible ramifications of this approach, especially cases where an incoming request for the react app will fail because of some kind of file lock on index.html as it's being read / written by the node service.
I don't think this would be an issue, but I had enough doubt to post this question. The last thing I can afford are failed requests in my production app because of some unforeseen issue.
Any advice, suggestions, anecdotes, etc. are appreciated!

I've successfully added SSR for CRA without ejecting. If that's your only use case, the SSR setup will be super simple as you don't have to deal with webpack or babel configs. If you are interested, follow 3 steps below:
Step 1: Add a basic Express server in a new folder at your root/ project folder (same level as src/)
Step 2: For main app routes, read file build/index.html and edit it as you want before sending.
Step 3: Other than those routes, serve CRA build/ static files as suggested by CRA SSR documentation
TL;DR
// root/server/index.js (your build is at root/build)
const express = require("express");
const fs = require("fs");
const path = require("path");
const app = express();
// step 2
const renderContent = (req, res) => {
fs.readFile(
path.resolve(__dirname, "../build/index.html"), "utf8", (err, htmlData) => {
if (err) {
return res.sendStatus(500);
}
/* Do something with htmlData here */
return res.send(htmlData);
});
}
app.use("^/$", renderContent); // step 2
app.use(express.static(path.resolve(__dirname, "../build"))); // step 3
app.use("*", renderContent); // step 2
// step 1
app.listen(process.env.PORT, () => {
console.log(`Listening on port ${process.env.PORT}`);
});
Then, you just need to run: node server/index.js, and this server will serve your Create React App normally, except for the part that you edit your HTML above.

I don't know if this will help, but as a suggestion. I faced a similar usecase. I solved it very simply using HtmlWebpackPlugin.
Inside the webpack configuration. I write something like:
new htmlWebpackPlugin({
template: path.join(rootpath, '', 'index.html'),
inject: false //<-- Notice here
})
I set the "inject" to false and using this flag it does not append the script's/bundle on index.html and at the same time it allowed us to use ejs templating to loop over the file's we want to append on index.html.
Like on index.html I wrote:
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.chunks[chunk].css %>">
<script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
My suggestion would be create a separate chunk/file(js) of that data you want to append dynamically on index.html and using EJS templating, you can achieve the desired result.
How to Create Chunks: READ HERE
Thanks, Hope it Help.

Related

How to deploy Next.js app without Node.js server?

I was hoping to deploy a Next.js app with Laravel API. I had developed React apps with CRA and in those I used the API server to serve the index.html of the CRA as the entry point of the app.
But in Next.js, after development I get to know that it needs a Node.js server to serve (which is my bad, didn't notice that). There is an option next export that builds a static representation of the Next.js app and it has an index.html. I am serving the index.html as the entry of the app by my Laravel API. It is serving the page, but just some of the static contents.
What I was hoping to know is it possible to host the aPI and the Next app from a single PHP shared hosting without any node server? If so, how? If not so, what could be the alternatives?
Actually the acepted answer is completly wrong, when you do yarn build and in your package.json is set like "build": "next build && next export", you will get an out folder which all the items in there are used to build without node.js server
Now since you are using laravel, and you use the out folder you will only load half of the page because the routes are not set properly. for that to work you need to edit your next.config.js edit it to
module.exports = {
distDir: '/resources/views',
assetPrefix: '/resources/views',
}
These will set the root directory to the root one in Laravel. now this will work for SPA (single page application) only for the dynamic routes you need to match with a view file for each one that you have in your out folder
For each route that you have you need to create a new "get" route in laravel
Route::get('/', function () {
return require resource_path('views/index.html');
});
Route::get('/contacts', function () {
return require resource_path('views/contacts.html');
});
Route::get('/post/{slug}', function () {
return require resource_path('views/post/[slug].html');
});
Notice that you can pass a wildcard for dynamic routes and they are all gonna work. once you do that and you deploy route out folder inside /resources/views in Laravel it's going to work
Apparently there is no alternative to nodejs server, which is not an option for me currently, so I unfortunately had to abandon next.js and create a CRA app and used as much from the next.js as I could.

How to server a ".html" at certain URL in a Next.js app?

I have an ".html" file that I need to serve in a certain route in my Next.js app, like this ...
/pages/customr-route-name/my-html-file.html
So if I go to my website and type http://example.com/custom-route-name/my-html-file.html I can see it
How can I do that in Next.js?
This one requires an API route and a URL rewrite to get working. And the nice thing is, you'll be able to use this pattern for other things too (like if you want to generate an RSS feed or a sitemap.xml).
NOTE: You will need to be running Next 9.5 for this to work.
0. Move your HTML file (Optional)
Your file doesn't need to be located in the ./pages dir. Let's put it in ./static instead. Just replace your these route-file placeholders with your real filename later: ./static/<route>/<file>.html
1. Create the API route
Next lets you create API routes similar to how you would in an old-style host like Express. Any name is fine as long as it's in ./pages/api. We'll call it ./pages/api/static/<route>/<file>.js
// import
import fs from 'fs';
// vars
const filename = './static/<route>/<file>.html';
// export
export default async function api(req, res) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.write(await fs.readFileSync(filename, 'utf-8'));
res.end();
}
2. Add a rewrite to next.config.js
In Next, rewrites work similar to their apache counterparts. They map a url location to a Next ./page.
// export
module.exports = {
rewrites: async () => [
{source: '/<route>/<file>', destination: './pages/api/static/<route>/<file>'},
],
};
3. Run it!
You should be able to test this with a regular next dev. Of course, changes to next.config.js require you to manually reboot the dev server.
If you look at the Next docs, you'll see you can use wildcards in both the API routes and these redirects, which might help with the direction you're going.
Update for NextJS Version 11
In your public folder you can create a folder structure patterned after the URL you want.
For example:
public/custom-path-folder-name/file.html
will be served at:
http://localhost:3000/custom-path-folder-name/file.html
For more information see the nextjs docs for static file serving

How to set environment variable for a deployed Reactjs app on server?

I think my question is different from previously asked questions about environment variables. I know how to set the environment variables while building the ReactJs app. Once the app is built, it gives me the static files and i put it on server. This app works like a normal webapp.
Question:
If i want to give the client provision of changing one of the environment variables on a deployed build on server without opening the code files, how i can do that?
Note:
As this is not a specific coding question, please move this question to relevant SO forum if you think.
Thanks
It is not possible to change REACT_APP_XXXXX env. vars after compilation. They are getting "baked in" into the app permanently.
I solved this problem by having a dynamic JSON manifest file that is being served from the backend. The app loads the JSON file and reads its values. A variation of this approach is to have a Javascript js file served from the backend. The script executes a function which sets some variables in global window object.
Here is a snippet of a node express app that serves the manifest as JSON and as a JS file.
const express = require('express')
const app = express()
const manifest = {
appTheme: process.env.APP_THEME,
foo: "bar"
};
const cacheTimeoutSec = 600
class ManifestController {
static getJS(req, res) {
/// Generate IIFE function that sets window.serverManifest object
let fileChunks = [
'(function(){',
'var serverManifest=',
JSON.stringify(manifest),
'; window.serverManifest = serverManifest',
'})()',
].join('');
res.set('Cache-Control', `public, max-age=${cacheTimeoutSec}`);
res.setHeader('content-type', 'text/javascript');
res.write(fileChunks);
res.end();
}
static getJSON(req, res) {
res.json(manifest);
}
}
// Serve manifest in JS
app.get('/server-manifest.js', ManifestController.getJS);
// Serve manifest as JSON
app.get('/server-manifest.json', ManifestController.getJSON);
Option 1:
The react app manually fetches the JSON file from your backend (e.g. https://api.mybackend.com/server-manifest.json) and act on the data.
Option 2:
Include <script> tag in the html file header like so
<html lang="en">
<head>
<script src="https://api.mybackend.com/server-manifest.js"></script>
<title>Home</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
If you put it in the <head> the browser will load the script and will execute it. The script sets the manifest in the global window.serverManifest object which the React app can access now at any time.

Serve react project on dev througth proxy

I've split my react application in 3 different projects, using CRA for all of them, auth, X and Y. User is first sent to auth, then I redirect him to either X or Y based on some info.
It works perfectly on PRODUCTION environment (because they run on the same domain), but on dev, X and Y failed to authenticate the user, because they run on different ports (different domains) the data in local storage is not shared between auth, X and Y.
I've tried to find a way to use a reverse proxy (http-proxy) to host the React dev servers on the same domain, but failed too, because the services could not find the assets/static folder, resulting in 404. Also tried http-proxy-middleware, as it is recommended on the CRA docs page, but failed to do so. Is there an easier way that I'm not seeing?
Edit: Found something new, but also failed. Used react-rewired to override CRA scripts, to use PUBLIC_PATH on DEV, but now my bundle.js returns an index.html file.
The following code does redirect to the accordingly react project, but the assets are requested to the wrong path.
const apiProxy = httpProxy.createProxyServer();
app.all("/login/*", function(req, res) {
console.log('redirecting to Login');
apiProxy.web(req, res, {target: servers.login});
});
app.all("/implementacao/*", function(req, res) {
console.log('redirecting to Implementation');
apiProxy.web(req, res, {target: servers.implementation});
});
So I used react-rewired to change the public path
const {
override,
} = require('customize-cra');
module.exports = {
webpack: override(
(config) => {
config.output.publicPath = '/login/';
return config;
},
),
jest: config => {
return config;
},
devServer: configFunction => (proxy, allowedHost) => {
return configFunction(proxy, allowedHost);
},
paths: (paths, env) => {
return paths;
}
};
Now, the assets requests are made correctly to /login/, but nothing the dev server always return an index.html file.
Even with react-app-rewired, to override config, and use publicPath on dev, the assets will not be served from the publicPath.
There is already a pull request on CRA to use PUBLIC_URL in dev mode.
Is there an easier way that I'm not seeing?
Another approach would be to use multiple React Single Page Applications (SPAs) inside one application, see crisp-react. E.g. instead of 3 CRAs in 3 applications/projects have 3 SPAs in one application/project. The backend surely can get data from other backend servers transparently for each SPA.
how do I migrate from a set of existing CRA projects to using crisp-react ?
Background
crisp-react comes with two stock SPAs called ‘First’ and ‘Second’. Both render some explanatory/sample UI.
Migration overview
1.Pick one CRA project and migrate it to the ‘First’ SPA. When finished, you have two CRAs left and two crisp-react SPAs: ‘First’ (renders your UI) and ‘Second’ (still renders the sample UI). Rename the ‘First’ SPA to give it more meaningful name.
2. Pick another CRA and migrate it. When finished, you have one CRA left and two crisp-react SPAs both rendering your UI.
3.Modify crisp-react to add the third SPA and then migrate the remaining CRA to the third SPA.
Migration steps (sketch)
1.1 Follow crisp-react Getting Started.
1.2 The landing page of the First SPA is rendered by crisp-react/client/src/entrypoints/first.tsx
The landing page of the CRA is rendered by src/index.tsx
Replace the content of the former with the latter.
1.3 The first CRA consists of React components: src/App.tsx and others you added. Copy the components to crisp-react/client/src/components/from-first-cra/
1.4 Ensure crisp-react client app compiles: From crisp-react/client/ execute: yarn compile
1.5 Ensure crisp-react client app builds: From crisp-react/client/ execute: yarn build
1.6 Ensure crisp-react client looks ok without backend data: see client Usage Scenarios.
1.7 Get the backend (e.g. Express) involved: see backend Usage Scenarios.
1.8 Milestone reached: browser can be pointed to backend (Express) and get from it html files and bundles - which results in the first SPA rendering initial UI (to the extent possible without data supplied via API enpoints).
1.9 Decide how the the first SPA will get data from API. 3 basic choices here:
- the API endpoints are implemented in Express so you can retire your backend servers
- Express does expose API endpoints but acts as a reverse proxy getting data from your backend servers
- Express knows nothing about API and data supplied by backend servers that are queried directly by the components inside the first SPA.
2.1 Second SRA
as above
...

How to serve ReactJS static files with expressJS?

The Problem
I have successfully served the index.html file of my React app, but the index.js that replaces <root> in the html file
with my first React component is not triggering on ReactDOM.render.
How do I get the index.js file to start? If my understanding of serving a React app is skewed in certain ways, I would greatly
appreciate clarification.
Folder Structure
/ - contains all server-side files, including server.js
/client/ - contains all React files
/client/build/ - contains all production-ready client files
/client/build/index.html
/client/build/static/js/main.[hash].js - seems to be a minified version of index.js that contains the ReactDOM.render for
my React app
Current Deployment
I am using Facebook's create-react-app for the /client/ directory, including npm run build to automatically populate /client/build/
File Snippets
// server.js
let app = express();
app.use(express.static(path.join(__dirname, '../client/public')));
This successfully loads the default index.html provided by
create-react-app
// index.html
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
The above section of code may/may not be useful, but it is the default
html file that comes with create-react-app. Do I need to replace the
noscript tag with a script tag that references the minified index.js
file? I have attempted that, and nothing changed, but perhaps it is
because of incorrect relative path-making.
After trying many different things through trial/error, the solution is quite simple:
Serve the /client/build folder in the static call, like so:
app.use(express.static(path.join(__dirname, '../client/build')));
I had the same problem for a while and I would say that the solution that works is divided into 2 parts to avoid problems with the routers
Server the static from the CRA build (in your case the client/build)
const buildPath = path.normalize(path.join(__dirname, '../client/build'));
app.use(express.static(buildPath));
Set a final route (after all other routers in your server) to use the following:
const rootRouter = express.Router();
/*
* all your other routes go here
*/
rootRouter.get('(/*)?', async (req, res, next) => {
res.sendFile(path.join(buildPath, 'index.html'));
});
app.use(rootRouter);
//on your react app run
npm run build
//The insert the following code on your server
const path = require("path");
app.use(express.static(path.join(__dirname,"nameOfYourReactApp","build")))
//Replace nameOfYourReactApp with the name of your app
my project structure
project
-->client
back end(express)\
after using npn run build i found out that index.html in build was using wrong directory of css files or static instead of using
const path = require('path');
app.use(express.static(path.join(__dirname, '/client/build/')));
i know i also tried ../client...... but not working
so what i did is cut and paste static folder of build in root directory this image can give you the idea, and its working structure

Resources