Fixing SEO problems for CRA (Create-react-app)

Stacked Q
4 min readJun 26, 2020

--

Handling page title and description could be something challenging for SPAs, specially if you built your app with create-react-app package.

CRA comes with a massive amount of cool features like handling webpack and babel configurations, enabling using latest ECMAScript syntax, importing tons of static file formats like images/fonts/styles and taking care of development and production environments.

CRA has an “eject” script which will bring you the full access to all of those configurations but if you eject no support is provided after from facebook team.

Sometimes the user wants other features like SSR (server-side rendering) which I personally recommend using Gatsby if the app can be statically rendered, and using Nextjs if it needs to get different data from an API endpoint for different pages.

But if you only need to fix SEO problems of your CRA app, this story is for you.

First things first

You will no longer be able to serve your app as a static file.

You need a server to handle incoming requests, express is a minimal and flexible Node.js web application framework which we could use for this purpose.

Step 1: change public/index.html structure

You need to add some “dummy” strings to public/index.html in order to change them with real values later, currently public/index.html should look like:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body class="has-animations">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Let’s add the dummy values:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<-- NOTE BELOW DUMMY STRINGS --> <title>__SEO_TITLE__</title>
<meta name="description" content="__SEO_DESCRIPTION__" />
</head>
<body class="has-animations">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

So we added __SEO_TITLE__ and __SEO_DESCRIPTION__ dummy strings to the html.

Step 2: Serving build version with express

Open up a terminal, go to root directory of your app and:

npm i express

or if you’re using yarn:

yarn add express

Then create a file named server.js in root directory of your app with the following content:

const express = require("express");
const path = require("path");
const fs = require("fs");
const app = new express();app.use("/static", express.static(
path.join(__dirname, "build/static")
));
app.get("*", (req, res) => {
return res.sendFile(path.join(__dirname, "build", "index.html"));
});
app.listen(3000, () => {
console.log("listened on 3000");
});

This file will handle incoming requests to app on port 3000.

Note that this file is only for production and you won’t need it in development.

Step 3: Adding SEO props on the fly

For SEO props of the static pages like home page, you could add an array containing different objects for pages and SEO props, add a new file named seo.js in the root directory:

module.exports = [
{
path: "/",
title: "TITLE",
description: "Main page description",
},
];

then edit server.js to change the dummy values if a matching SEO object exists:

const express = require("express");
const path = require("path");
const fs = require("fs");
const seo = require("./seo"); // note hereconst app = new express();app.use("/static", express.static(path.join(__dirname, "build/static")));app.get("*", (req, res) => {
let pathname = req.pathname || req.originalUrl;
let page = seo.find((item) => item.path === pathname);
if (page) {
let html = fs.readFileSync(path.join(__dirname, "build", "index.html"));
let htmlWithSeo = html
.toString()
.replace("__SEO_TITLE__", page.title)
.replace("__SEO_DESCRIPTION__", page.description);
return res.send(htmlWithSeo);
}
return res.sendFile(path.join(__dirname, "build", "index.html"));
});
app.listen(3000, () => {
console.log("listened on 3000");
});

These changes will try to locate SEO object in seo.js and if it found a match it will change dummy values into real values.

Let’s see if it works now… build the app:

npm run build 

or

yarn build

and then:

node server.js

open your browser at localhost:3000 and you should see the title and description is changed. even in source page.

Step 4: Getting SEO props from external endpoint

The idea here is kinda the same as previous step, but instead of using seo.js file you must fire an external api endpoint to see if there’s any data available for this purpose and then send the result to client.

Happy coding, tell me if you had any problems throughout this process, and remember to Clap if you were happy with the results.

--

--