How to set dynamic meta tags using React and Firebase

Twitter and many other sites are still using crawlers not adapted to single page applications.

That means, if you are changing the meta tags of a single page dynamically, crawlers will only get the meta tags of the first render.

For example, index.html in React looks like:

html
<!DOCTYPE html>
<html lang="en" data-theme='🔵'>
  <head>
    <meta charset="utf-8">
    <meta name="viewport"            content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color"         content="#000000">
    <meta name="description"         content="I am Erik, an engineer from Barcelona. I code and share what I am learning.">
    <meta name="twitter:card"        content="summary_large_image" />
    <meta name="twitter:site"        content="@ErikMarJor" />
    <meta name="twitter:title"       content="TWITTER_DYNAMIC_TITLE" />
    <meta name="twitter:description" content="TWITTER_DYNAMIC_DESC"/>
    <meta name="twitter:image"       content="https://erikmartinjordan.com/TwitterCard.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>Erik Martín Jordán</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
  </body>
</html>

Every time Twitter crawls the site, no matter the route, meta tags will remain constant. Even if you change them dynamically using JavaScript.

Prerender function

Conclusion: meta tags as content="TWITTER_DYNAMIC_TITLE" and content="TWITTER_DYNAMIC_DESC" need to be set before the first render.

To do so, we need to create a prerender function in functions/index.js:

jsx
const functions  = require('firebase-functions');
const admin      = require('firebase-admin');
const fs         = require('fs');

var app = admin.initializeApp();

// Getting and replacing meta tags
exports.preRender = functions.https.onRequest((request, response) => {
    
    // Error 404 is false by default
    let error404 = false;
        
    // Getting the path
    const path = request.path ? request.path.split('/') : request.path;
    // path[0] = erikmartinjordan.com path[1] = kpis
    // path[0] = erikmartinjordan.com path[1] = projects
    // ...
    
    // Getting index.html text
    let index = fs.readFileSync('./web/index.html').toString();
    
    // Changing metas function
    const setMetas = (title, description) => {
        
        index = index.replace('TWITTER_DYNAMIC_TITLE', title);
        index = index.replace('TWITTER_DYNAMIC_DESC', description);
        
    }
    
    // Navigation menu
    if     (path[1] === 'articles')    setMetas('Articles - Erik Martín Jordán', 'Code, web development, tech and off-topic.');
    else if(path[1] === 'projects')    setMetas('Projects - Erik Martín Jordán', 'Websites I have deployed so far.');
    else if(path[1] === 'kpis')        setMetas('Kpis - Erik Martín Jordán', 'Numbers and stats.');
    else if(path[1] === 'timer')       setMetas('Timer - Erik Martín Jordán', 'Tasks I am working on.');
    
    // We need to considerate the routes and a default state to 404 errors as well
    // ...

    
    // Sending index.html    
    error404
    ? response.status(400).send(index)
    : response.status(200).send(index);
    
});

Reading the HTML content

Look at the following line because it's the most important:

jsx
let index = fs.readFileSync('./web/index.html').toString();

Here, we are reading the index.html content to replace the meta tags later on.

As we can't access the files on Firebase Hosting, we need to read the content on Firebase Functions. Therefore, we need to create a functions/web folder and force NPM to build the index.html inside it.

We need to modify the build script on package.json:

terminal
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build && cp build/index.html functions/web/index.html && rm build/index.html",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }

After we run npm run build, a copy of index.html will be stored inside functions/web and removed from build/.

As we are generating the HTML code using the prerender function, we don't need any index.html in Firebase Hosting at all.

Understanding this process will lead you to one of those 'aha' moments that people talk about.

Forcing Firebase to execute the prerender

Finally, to ensure that Firebase executes the prerender function, we need to modify firebase.json:

terminal
{
    "rewrites": [
      {
        "source": "**",
        "function": "preRender"
      }
    ]
}

And that's it. You will notice an increase in the number of function calls in Firebase; prerender function executes every time a user enters on your site.

Hi, I'm Erik, an engineer from Barcelona. If you like the post or have any comments, say hi.