Introduction
When we deploy a React app using create react app or using a custom Webpack configuration. There is at least one issue that everyone faces for sure, that is SEO.
Client-side apps are NOT good for SEO. There are a few reasons for that, two main reasons are:
- Client-side apps are slow as compared to server-side rendered apps.
- It is a single-page application, that means we only have a single
index.html
file. That file can only have one set of meta tags and open graph tags.
In this blog post, we are going to learn how to fix the second issue. After completing this tutorial we will be able to:
- Fix the meta tags and open graph tags issue with client-side rendered React apps.
- Insert dynamic meta and open graph tags in any client-side rendered app in minutes.
To start off you may know that we can use an npm package called React Helmet to fix the issue of meta tags. It is also possible that you have already tried that. BUT, there is an issue with using React Helmet alone. To understand the issue we have to understand how React Helmet works.
Meta tags issue is on two levels:
- To load the correct meta and open graph tags for the users visiting the website.
- To load the correct meta and open graph tags for the crawlers trying to crawl and index the website.
React Helmet alone is able to solve the first problem. But it can't convert our single-page app into a multi-page app. What it does is, on runtime, it inserts meta tags into our page when that page is loaded in the user's browser. It solves one of the problems which is showing the correct meta tags to the users.
It's NOT capable of solving the second issue which is correctly loading the meta tags in the index.html
of our React app. So that the crawlers which are unable to render JavaScript can read the tags properly.
Why is that an issue?
It's an issue because not all crawlers can render JavaScript correctly. Some do and some don't, e.g. Google's crawlers have no problem in rendering JavaScript and reading the meta tags rendered at runtime. While React Helmet works for users, Google and some social media sites, it doesn't work for other services.
If we post our React app's link to these services, it will not load the correct tags that we specified in our React Helmet tag. Instead, it will pick up the default tags from index.html
.
We can fix this problem using the steps below and the solution doesn't require us to convert our application to a server-side rendered app.
Prerequisites
Before following this tutorial make sure you have:
- Familiarity with node and express (or any other framework of your choice, the idea can be implemented using any other framework). Even if you are not comfortable with any of these, you can look at the example app that I have created to get an idea of how all of this works.
- A server where we can run the express application.
Step 1
The solution in this blog post works best with React Helmet. We still need to have React Helmet installed and set up it for all the pages we want the dynamic tags to work.
React helmet will handle all the title changes on route change if you are using something like React Router.
- We need to keep all the meta tags in two places
- One inside the React Helmet tags on the frontend.
- Second on the express server on the backend.
First of all, we need to update our index.html
file to something like the code below. Add as many as meta tags you need. Here we are only going to use the title and the description tag for simplicity.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{title}}</title>
<meta name="description" content="{{description}}" />
</head>
<body>
<div id="app"></div>
<script src="/index_bundle.js"></script>
</body>
</html>
We are doing this because we are not going to serve our React build files to the users directly.
We are going to spin up a node + express server and going to replace these tags in the curly braces dynamically at the runtime.
Step 2
After we have completed the previous step, we have to create a node + express server. I have posted a basic example for this on Github, you can download it, inspect the code and use it directly if you want. Or you can continue following this tutorial.
Create a file called server.js
and copy the React build files in a subfolder called public
.
Our structure should look something like this.
server.js
public/
index.html
index_bundle.js
We will need to initiate a node project in the same folder as server.js
using npm init
and then install express
.
The code below is from the same repository.
In this example we have a React App with three routes:
/
for home/about
for an about me page/contact
for a contact page.
I am not going to put the React code here. You can visit the Github link to inspect the React part of the code.
In the server.js
file below, we have handlers for all these three routes and a 4th route handler. Which will handle any routes that we haven't specified. This handler will replace the title and description with a default value. Think of it as a fallback. In case the route is specified on the frontend and we forget to add it to this file.
const express = require("express");
const path = require("path");
const fs = require("fs");
const app = express();
const port = 3000;
app.get("/", function (req, res) {
const filePath = path.resolve(__dirname, "./public", "index.html");
fs.readFile(filePath, "utf8", function (err, data) {
if (err) {
return console.log(err);
}
data = data.replace(/{{title}}/, "Sachin Verma");
data = data.replace(
/{{description}}/,
"Sachin Verma's personal site and blog"
);
res.send(data);
});
});
app.get("/about", function (req, res) {
const filePath = path.resolve(__dirname, "./public", "index.html");
fs.readFile(filePath, "utf8", function (err, data) {
if (err) {
return console.log(err);
}
data = data.replace(/{{title}}/, "About | Sachin Verma");
data = data.replace(/{{description}}/, "About Sachin Verma");
res.send(data);
});
});
app.get("/contact", function (req, res) {
const filePath = path.resolve(__dirname, "./public", "index.html");
fs.readFile(filePath, "utf8", function (err, data) {
if (err) {
return console.log(err);
}
data = data.replace(/{{title}}/, "Contact | Sachin Verma");
data = data.replace(/{{description}}/, "Contact Sachin Verma");
res.send(data);
});
});
app.use(express.static(path.resolve(__dirname, "./public")));
app.get("*", function (req, res) {
const filePath = path.resolve(__dirname, "./public", "index.html");
fs.readFile(filePath, "utf8", function (err, data) {
if (err) {
return console.log(err);
}
data = data.replace(/{{title}}/, "Sachin Verma");
data = data.replace(
/{{description}}/,
"Sachin Verma's personal site and blog"
);
res.send(data);
});
});
app.listen(port, () => console.log(`Listening on port ${port}`));
To explain the code above briefly. It's listening to the user requests and when a user lands on e.g. /contact
, it replaces {{title}}
and {{description}}
in the index.html
with the corresponding values.
Step 3
Now everything is ready and we can start our express server by running node server.js
. It should start serving at port 3000
, which you can check by going to http://localhost:3000
Go ahead and navigate to different pages and inspect the source of the page and confirm that the dynamic tags are working correctly.
Conclusion
After following this tutorial we will have a working React app with dynamic meta and open graph tags.
We will also be able to add new pages and tags by adding React Helmet tag in the new page and adding more routes in our express server.
Next Steps
There are some possibilities for optimization which are out of the scope of this tutorial. I will leave them for a future tutorial perhaps.
Currently, we have to specify the meta tags directly in the server.js
file and keep it in sync with the frontend to work properly. If you have a fairly large app this method can become complicated real quick.
For the next steps, we can create a routes file, which will contain all the routes and the meta tags. That should make it a little less complicated to update.