Are you looking to make your React.js application more SEO-friendly? If yes, you’ve surely noticed it’s not as easy as it may seem at first. Particularly in case of complex apps with a lot of dynamic content and a huge backend without CSR.
Unfortunately, React.js doesn’t have a native process for dynamically changing title or meta tags according to what the crawler is currently searching. Have a look at your index.html in the public folder (create-react-app) – that’s the only item that crawlers by default can see.
The first thing that pops up in the mind of most programmers is to immediately apply server-side-rendering. However, this was not our case since we needed the app to run locally on the user’s device. Anyway, we already had the frontend completely written so changing it to CSR would have done more harm than good. We had to keep searching.
Inspired by on-line articles and discussions, we decided to try the React-helmet package recommended by React.js itself. All was looking alright in the beginning – title and meta tags were changing successfully both on the localhost and on our staging branch, but nothing worked in production.
After reviewing the documentation, it became clearer why the package wasn’t functional. Our stage and localhost worked differently than our production. Localhost as well as stage didn’t start from the built code. This was easy to see with localhost, but it took us quite a while to figure this out for stage too. Stage was started using a serve package, whereas for production, we used built static files that were subsequently uploaded to AWS.
After that we tried to implement Prerender.io. We still don’t know whether we got everything right, but nothing was really working for us. At one point, we managed to start the home page using Prerender.io, but that was as far as we got. Another important factor was that we wanted to retain as much control as possible over our web and not rely on any third party services.
After a lengthy search for a solution, we finally stumbled upon the React-snap package that was supposed to do exactly what we wanted. That was to build static HTML files with JS. And it really worked! Well… at least to an extent…
Watch your component positions! We encountered the following problem with react-snap: The header elements (title, metas) changed by us were so deep in the components tree that react-snap simply wasn’t able to access these changes.
This can easily go unnoticed first, because title and meta tags do eventually load on localhost, but React-snap doesn’t wait for them during building. As a result, the generated static files contained no header elements. This problem has a relatively straightforward solution.
Move the <Helmet /> component upwards, as much as possible (so it is a top-level component, such as <App />). In theory, React-helmet should work anywhere, but that wasn’t our case.
Cool. We have now built static files with the correct header tags. Let’s proceed to stage, building, SEO check… and boom: No title or meta description tags included. Why? As mentioned earlier, our stage runs with the help of the serve package that doesn’t use built static files but simply serves JS code instead. It’s the same as turning on the website on localhost via Yarn start. This might seem obvious, but it just didn’t occur to us. Luckily, we figured it out after a while!
We know that our code is built into the static HTML files and contains the right header tags. We also know that we cannot try to run the SEO check on the stage branch. For production, we use a relatively simple Gitlab CI Pipeline to build the project, save it as an artifact and upload it to S3. The latest code is pushed to production branch and the pipeline is initialized. Installing node modules, building, building is finished, starting the React-snap and… CORS. GG.
When React-snap builds files, it starts JavaScript. This was the cause of the problem. If the code includes a fetch that hits our production BE, the origin of this request will be the IP address of our CI machine. This is not a problem with localhost or stage, as DEV/STAGE BE doesn’t have such a strict CORS Policy (for testing purposes). Unlike production.
There are several solutions to this problem, the simplest of which is to add the CI IP address to CORS “known hosts”. This solution is not perfect, but it works. After this change, everything should finally work and the build should be functional.
That’s it: Your React.js app is now SEO-friendly and doesn’t need CSR. Moreover, your pages are pre-rendered, which means the app loads much faster.