Improve your site’s SEO and Google ranking with React SSR
By: Alan Acuña
Tags: Ssr, React, Firebase, SEO, DevOps

Imagine this, you have finished building the website for your business. It took you several weeks to do because you programmed it yourself with that framework that everyone seems to love, called React. You’re super excited to roll out your site, and for good reason! You trust that after a few weeks you will have hundreds of daily visitors. But time passes and… nothing. It even seems that your site is not even on Google.

What happened? Did you do something wrong? Well not really, it’s not that you’ve done anything wrong. It is simply that frameworks such as React or Angular do not get along very well with Google’s algorithm and have a terrible SEO (Search-Engine Optimization). But I have good news for you! There is a technique you can use to improve the ranking of your React websites. This technique is called Server-Side Rendering or SSR and in this post you will learn how to integrate it with React. In addition, you will learn how to automate the deployment process with DevOps.

To follow this post, I recommend that you have basic notions about React and your computer’s terminal. I also recommend that you know about Firebase, since we will use it to deploy our React site, although you can use the hosting that you like the most.

SSR? What’s that about?

The concept of SSR is very simple. Frameworks like React or Angular use JavaScript to render the content of the website on the client’s side, which is known as Client-Side Rendering (🤯) or CSR. This has many advantages, but since all of the website’s content is rendered on the client, Google’s bots do not “see” anything. To solve this you can take the code of your site and run it on a server so that your content is generated before it reaches the client, that’s what SSR is all about.

Keep in mind there are other alternatives to solve this problem for frameworks like React. You can use prerendering to convert your site’s content to static content, but if you constantly generate content this is not the best option.

With that said, let’s get started building our React app.

Creating our basic React application

To create our React application we are going to use create-react-app. Optionally, you can use my sample project to follow this post, which can be found in the following GitHub repo. So we enter our terminal and in the folder of your choosing, run the command:

npx create-react-app react-firebase-ssr

This will generate the basic configuration for our application. Surely, we also need a router for our site, so we are going to install react-router. In our project folder we run the command:

npm install react-router

Now, you can add whatever content you want to your React website. BUT, when you configure the react-router, add the component <BrowserRouter> in the root of the application, that is, in the index.js file, like this:

const rootElement = document.getElementById('root'); const app = (

<BrowserRouter>

<App />

</BrowserRouter>

); render(app, rootElement);

Keep this in the back of your head. Also, taking advantage of the fact that we are in the index.js file, we are going to change the last line to:

rootElement.hasChildNodes()

? hydrate(app, rootElement)

: render(app, rootElement);

This line, in simple terms, means that if our application is run on the server, only hooks, listeners, etc. will be added to the already generated HTML. And if it runs on the client, all of the content is rendered. So our index.js file would look like this:

import React from 'react';

import { hydrate, render } from 'react-dom';

import { BrowserRouter } from 'react-router-dom';

import App from './App';

import './index.css'; const rootElement = document.getElementById('root'); const app = (

<BrowserRouter>

<App />

</BrowserRouter>

); rootElement.hasChildNodes()

? hydrate(app, rootElement)

: render(app, rootElement);

Using React Helmet to generate the meta tags for our site

An important step to improve the SEO of any website is to add meta tags to each HTML file on the site. Of course, our React site only has one index.html file, but we can use a library called React Helmet to dynamically generate meta tags. To install it we are going to run the command:

npm install react-helmet

Using this library is as simple as wrapping your meta tags in the <Helmet> component on each page of your site. That is why I recommend you create a special Metatags.js component to contain your meta tags so you can just pass the information you want through props. This component would look something like this:

import React from 'react';

import { Helmet } from 'react-helmet'; const Metatags = ({ title, description }) => {

return (

<Helmet>

<title>{ title }</title>

<meta name="description" content={ description }/>

{/* You can add as many meta tags as you like in this component */}

</Helmet>

)

} export default Metatags;

For further explanation of why meta tags are so important for SEO and to generate your own meta tags I recommend you this site.

Integrating SSR into our app with CRA-Universal

So far our site has been a run-of-the-mill React app, so it’s time to integrate the SSR part. For this we are going to use the cra-universal library. We run the following commands:

npm install -D cra-universal

npm install @cra-express/core

npx cra-universal init

This will install the necessary dependencies and generate a server folder along with the app.js and index.js files. I also recommend adding the following commands in the “scripts” section of your package.json `, which we will use later.

"scripts": {

...

"start:ssr": "cra-universal start",

"build:ssr": "cra-universal build",

}

Now we need to make a couple of changes to the app.js file. Remember the configuration we made of the react-router earlier? The reason we did such configuration was because now we need to wrap our <App/> component with the <StaticRouter component in the handleUniversalRender function, like this:

const handleUniversalRender = (req, res) => {

const context = {};

const el = (

<StaticRouter location={ req.url } context={ context }>

<App />

</StaticRouter>

);

return context.url ? res.redirect(301, context.url) : el;

}

We also need another configuration to render our meta tags server-side. In the arguments of the createReactAppExpress function we are going to add the onFinish method, as shown below:

const app = createReactAppExpress({

clientBuildPath,

universalRender: handleUniversalRender,

onFinish(_, res, html) {

const { title, meta } = Helmet.renderStatic();

const newHtml = html

.replace('</head>', `${ title }</head>`)

.replace('</head>', `${ meta }</head>`);

res.send(newHtml);

}

});

What the onFinish method does is inject into the HTML the meta tags that we define with React Helmet. In the end, the app.js file of the server is as follows:

import { createReactAppExpress } from '@cra-express/core';

import React from 'react';

import { Helmet } from 'react-helmet';

import { StaticRouter } from 'react-router-dom';

import path from 'path';

let App = require('../src/App').default; const clientBuildPath = path.resolve(__dirname, '../client'); const handleUniversalRender = (req, res) => {

const context = {};

const el = (

<StaticRouter location={ req.url } context={ context }>

<App />

</StaticRouter>

);

return context.url ? res.redirect(301, context.url) : el;

} const app = createReactAppExpress({

clientBuildPath,

universalRender: handleUniversalRender,

onFinish(_, res, html) {

const { title, meta } = Helmet.renderStatic();

const newHtml = html

.replace('</head>', `${ title }</head>`)

.replace('</head>', `${ meta }</head>`);

res.send(newHtml);

}

}); if (module.hot) {

module.hot.accept('../src/App', () => {

App = require('../src/App').default;

console.log('✅ Server hot reloaded App');

});

} export default app;

To test that our site works with SSR we need to run the npm start and npm run start:ssr commands in different tabs of our terminal. Once we are satisfied with our site we can run the command npm run build:ssr to generate a production build. At this point you can upload the app to your preferred hosting, but here we are going to use Firebase Hosting and Cloud Functions to deploy it.

Deploying our site to Firebase

If you don’t have the Firebase CLI tools yet you can install them with the command npm install -g firebase-tools . I recommend that you create a new project in the firebase console. Now we are going to run the firebase init command in the root of our application. You will have to select your Firebase project and choose hosting and functions to add to the project. The public folder for hosting will be build. Then it will ask you if you want to write your Cloud Functions with JavaScript or with TypeScript, but since we will only add one function you can choose JavaScript. It will also ask you if you want to configure the project as SPA, answer yes. To finish the process, 2 Firebase files will be generated in the project folder.

Now we need to configure the firebase.json file to redirect our hosting to our Cloud Function. So our firebase.json looks like this:

{

"hosting": {

"public": "build",

"ignore": [

"firebase.json",

"**/.*",

"**/node_modules/**"

],

"rewrites": [{

"source": "**",

"function": "ssr"

}]

},

"functions": {

"predeploy": []

}

}

Notice how I left the predeploy line empty, since otherwise it can give us linting errors when we want to upload our Cloud Function.

Before writing our Cloud Function we have to change the access point of our CRA-Universal app, because we won’t need to listen to the server on any port. So we have to create a crau.config.js file in the root of our project, which will contain the following:

module.exports = {

modifyWebpack: config => {

const newConfig = {

...config,

output: {

...config.output,

libraryTarget: 'commonjs2'

},

entry: './server/app.js' // It was './server/index.js' before

};

return newConfig;

}

};

Note that we are changing the access point from index.js to app.js . Now we need to rebuild the production build with the npm run build:ssr command.

We also have to install our React dependencies in our functions folder, as well as the fs-extra library that we will use later, using the commands:

cd functions

npm install fs-extra @cra-express/core react react-dom react-helmet react-router-dom

cd ../

Finally we can write our Cloud Function, which is not rocket science. In the index.js file of the functions folder we write:

const functions = require('firebase-functions');

const app = require('./dist/server/bundle').default; exports.ssr = functions.https.onRequest(app);

And that’s it for the Cloud Function. However, we have a little problem, we need to copy the dist folder into the functions folder and remove some files. We could do it manually, but… no one has time for that 🤣 so let’s write a script to do it for us. We create the copy-app.js file inside the functions folder and write the following:

(async() => {

const srcPath = '../dist';

const copyPath = './dist';

await fs.remove(copyPath);

await fs.copy(srcPath, copyPath);

await fs.remove(`${ copyPath }/package.json`);

await fs.remove(`${ copyPath }/package-lock.json`);

await fs.remove('../build/index.html');

})();

This will copy the dist folder into functions, removing the original along with the package.json files and the index.html file from our build. Don’t worry, our Cloud Function will be the one that serves our HTML. I also recommend adding the command "copy": “node copy-app" inside the scripts section of the package.json file in the functions folder.

So, the process to deploy our application to Firebase ends up as follows:

We create our production build: npm run build:ssr We move to the functions folder and run our script to copy our build:

cd functions

npm run copy

cd ../

3. We deploy to Firebase: firebase deploy

BONUS: Automating our deploys with Cloud Build

Our React with SSR app is ready at this point. However, the deployment process ended up being a bit tedious and involved. In order to automate our deployments we are going to use GitHub together with Google Cloud Build to do a process of CI/CD (Continuous Integration and Delivery) or DevOps.

First you need to create a new repository on GitHub (it can be public or private) and make an initial commit.

git add .

git commit -m "initial commit" git remote add origin git@github.com:<your-repo>.git

git push -u origin master

Next, make sure that it activates the Cloud Build API for your project in the GCP console. You need to give Cloud Build access to your Firebase project, so you need to go to the IAM menu and give Firebase Admin permission to the account ending with @cloudbuild.gserviceaccount.com .

Setting up Firebase IAM permissions to Cloud Build

Firebase is not available in the default NPM image on GCP, so we need a community builder, which is nothing more than a Docker container with the firebase-tools installed. First you need to install the Google Cloud SDK and then clone the community builders repository and upload it to Google Cloud, like this:



cd cloud-builders-community/firebase git clone https://github.com/GoogleCloudPlatform/cloud-builders-community cd cloud-builders-community/firebase gcloud builds submit --config cloudbuild.yaml . cd ../..

rm -rf cloud-builders-community

Watch out if you are on Windows, because you need to change the CRLF characters to LF characters in the firebase.bash file of cloud-builders-community/firebase in order to upload it to Google Cloud. More information about this here.

Once you upload the builder to Google Cloud you will be able to see it in the container registry page.

Container Registry page with Firebase container

Now we need to define the steps that Cloud Build will have to follow to build and deploy our project. For that we create the file cloudbuild.yaml in the root of the project.

steps:

# Install dependencies

- name: 'gcr.io/cloud-builders/npm'

args: ['install']

# Build

- name: 'gcr.io/cloud-builders/npm'

args: ['run', 'build:ssr']

# Install cloud functions dependencies

- name: 'gcr.io/cloud-builders/npm'

dir: 'functions'

args: ['install']

# Copy to cloud functions

- name: 'gcr.io/cloud-builders/npm'

dir: 'functions'

args: ['run', 'copy']

# Deploy

- name: 'gcr.io/[TU-PROYECTO]/firebase'

args: ['deploy']

Take into account that in this file you can also define many additional options, for example, to run your unitary tests if you so wish.

Almost done! We just need to connect our GitHub repository with Cloud Build by registering the build trigger. Make sure to point the trigger to the cloudbuild.yaml file.

Creating the build trigger in Cloud Build

Now we just need to create a new commit and push it to GitHub. Cloud Build will begin building the application and you can see the progress in the GCP console. That’s it! If you need more information to work with Cloud Build, I recommend this article.