Solving SEO Challenges in CRA (and Beyond) Without Switching to Next.js

Developers using the Create React App (CRA) or any other basic React framework using Client Side Rendering(CSR) often face a challenge when it comes to implementing dynamic SEO for different pages. The common advice suggests migrating to a Server-Side Rendering (SSR) framework like Next.js. However, for early-stage startups or projects heavily invested in CRA, this switch may not be the most practical solution. In this blog, I'll introduce a straightforward approach to address SEO challenges within the CRA framework and highlight its broader applicability to any Client-Side Rendering (CSR) application.

Understanding React's Client-Side Rendering (CSR)

To comprehend the issue, let's delve into how React, specifically CRA, utilizes Client-Side Rendering (CSR). A crucial file is generated in the build folder upon building a production version. This file contains standard HTML, CSS, and JS links. When a browser runs the web app, it fetches all assets from the server, starting with the index.html. The browser loads this file first, followed by the execution of the linked JS, which generates additional HTML files based on the JSX code. This process, known as Client-Side Rendering (CSR), renders code on the client-side, within the browser.

Basics of React server-side rendering with Express.js - Juhana Jauhiainen

How SEO Works in React Applications

SEO, or Search Engine Optimization, is fundamental for ranking websites and providing rich previews when links are shared on social media platforms. Social media platforms employ bots or web crawlers to scrape metadata from web pages, including titles, descriptions, images, and keywords. This metadata, residing in the head of HTML, dictates how links appear in previews. Achieving effective SEO involves modifying this metadata for different routes on our website.

Meta Tags | Conversion Optimisation | Webtrends Optimize

The SEO Challenge in CSR Applications

In a typical CSR setup, such as React with CRA, the index.html is rendered first by the browser. This renders dynamically changing its content using the rendered JS nearly impossible. This limitation poses a significant challenge when attempting to customize SEO data for different pages or routes on our website.

Illustrative Example:

Imagine you have a basic React website with two pages - one about 'About' and the other about 'Dashboard.' In a regular setup where the website is rendered on the client side, checking the page source for both pages would show the same behind-the-scenes information, like HTML metadata.

This might sound okay at first, but it creates a bit of a problem. When it comes to SEO (making sure your website shows up well on search engines and social media), having identical information for different pages can be tricky. Search engines might not catch the unique details of each page because they see the same thing for both. This can affect how well your pages rank in search results or how they show up when shared on platforms like social media.

So, in a nutshell, without doing something about it, the default setup might not help your website stand out for each specific page, making it tougher for people and search engines to understand and appreciate the different content you have on 'About' versus 'Dashboard.'

SSR: The Optimal Solution

In the pursuit of solving the SEO challenges in Client-Side Rendering (CSR) applications, Server-Side Rendering (SSR) emerges as the optimal approach. SSR involves generating pages on the server and then sending them to the browser, addressing the limitations of client-side rendering.

Possible Ways to Apply SSR

  • Use SSR Framework

    One traditional path is to leverage SSR frameworks like Next.js, Gatsby, or Nuxt.js (for Vue.js). These frameworks seamlessly integrate server-side rendering into your React or Vue.js applications, offering enhanced SEO capabilities. However, adopting a new framework may require code modifications and involve a learning curve.

  • A Creative Approach: Utilizing React on Top of Express

    A more creative approach involves utilizing React on top of an Express server. This allows us to serve the entire Create React App (CRA) production build under an Express server. By strategically serving the build folder, primarily the index.html, on a specific route, we can load the entire website. The crucial step is to dynamically manipulate the index.html file based on routes.

    Implementation Steps:

    • Create an API to provide metadata for various pages/routes.

    • Modify the content, including SEO data, in the served index.html file based on the received metadata.

        // basic index.js file
        const express = require("express");
        const app = express();
        const path = require("path");
      
        app.use(express.static(path.join(__dirname, "build")));
      
        app.get("/", (req, res) => {
          res.sendFile(path.join(__dirname, "build", "index.html"));
        });
      
        app.listen(3000, () => {
          console.log("App runnning on port 3000");
        });
      

      After running the index.js file you will see that your react build would start running on express.

      However, a challenge arises when you navigate to any route other than "/" and then reload the page; you'll encounter an error stating that the page is not found. This issue stems from the fact that we are currently handling GET requests only for the "/" route. To address this, we need to extend our route handling to include all routes, denoted by "*", in Express. Here's how you can do it:

        app.get("*", async (req, res) => {
            res.sendFile(path.join(__dirname, "build", "index.html"));
        })
      

      By adding this route handler for "*", we ensure that Express can appropriately handle requests for all routes, resolving the issue of page not found errors upon reload. This adjustment allows for seamless navigation across different routes while maintaining the dynamic SEO modifications we've implemented.

      Now we could change the metadata content of index.html using APIs and then pass it as the replaced file. Here's how you can do it

    •   const express = require("express");
        const app = express();
        const path = require("path");
        const axios = require("axios");
      
        app.use(express.static(path.join(__dirname, "build")));
      
        app.get("/", (req, res) => {
          res.sendFile(path.join(__dirname, "build", "index.html"));
        });
      
        // Function to generate dynamic SEO tags
        async function generateHTML(route) {
          try {
            // Make an API request to fetch SEO tags based on the route
            const response = await axios.get(
              // api and pass route as query
            );
      
            // Extract SEO tags from the API response
            const {
              title,
              description,
              image,
              canonicalUrl,
              keywords /* Add other SEO tags as needed */,
            } = response.data;
      
            // Generate the HTML content with dynamic SEO tags
            const html = `<!DOCTYPE html>
              <html lang="en">
                <head>
                  <meta charset="utf-8" />
                  <title>${title}</title>
                  <meta
                    name="title"
                    content="${title}"
                  />
                  <meta
                    name="description"
                    content="${description}"
                  />
                  <link rel="canonical" href="${canonicalUrl}" />
                  <meta
                    name="keywords"
                    content="${keywords"}"
                  />
                  <script defer="defer" src="/static/js/main.8cas045cb.js"></script>
                  <link href="/static/css/main.7b2ad5e961.css" rel="stylesheet" />
                </head>
                <body>
                  <noscript>You need to enable JavaScript to run this app.</noscript>
                  <div id="root"></div>
                </body>
              </html>
              `;
      
            return html;
          } catch (error) {
            // Handle errors from the API request
            console.error("Error fetching SEO tags:", error);
            return defaultHTML; // Return a default HTML template in case of an error
          }
        }
      
        // Define your SSR route
        app.get("*", async (req, res) => {
          try {
            const html = await generateHTML(req.url);
            res.setHeader("Content-Type", "text/html; charset=utf-8");
            res.send(html);
          } catch (error) {
            console.error("Error generating HTML:", error);
            res.status(500).send("Internal Server Error");
          }
        });
      
        app.listen(3000, () => {
          console.log("App runnning on port 3000");
        });
      

Note: This approach is not exclusive to CRA but is applicable to any CSR application facing similar SEO challenges.

In conclusion, this innovative approach to SEO challenges provides a flexible solution for Client-Side Rendering applications, extending beyond Create React App (CRA). By leveraging Server-Side Rendering techniques within Express, we seamlessly enhance SEO while preserving the benefits of our chosen framework. Remember, whether you're working with CRA or any other CSR application, adapting and optimizing your SEO strategy is now within reach. Happy coding!