DeCODE logo

SEO optimization for the blog. Practical guide and use case

General aspects

First of all, for each of the page, there was a necessity to introduce the correct keywords/description/title. In order to do that, previously was used React Helmet, but due to the usage of React 18 and streaming, there was a decision to re-organize the idea of metadata.

First of all, index.html for react application was constructed in such a way, that it's possible to substitute the necessary data. For this case, following markup was prepared :

<!DOCTYPE html>
<html lang="{lang}">
  <head>
    <meta charset="utf-8" />
    {react-head}
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">{react-app}</div>
  </body>
</html>

On the backend, modify the router as following for being compatible with React 18:

  import express from 'express';
  import assets from '../build/assets-manifest.json'; // import assets to map the index.js/index.css, which will be served to the end user

  const app = express();
  const indexFile = path.resolve('./build/index.html');
  const data = fs.readFileSync(indexFile, {
    encoding: 'utf-8',
    flag: 'r',
  });
  const [head, tail] = data.split("{react-app}");

  const [preHead, afterHead] = head.split('{react-head}');

  app.get('*', (req, res) => {
    const userBrowserLang = req.acceptsLanguages('en', 'ru', 'ua') || 'en';

    let originalUrl = req.url
      .replace(/^\/(en|ru|ua)/, '')

    if(originalUrl === '/') originalUrl = '';

    const { pipe } = renderToPipeableStream(
      <AppRoutes /> ,
      {
        bootstrapScripts: [`/static/${assets['index.js']}`],
        onShellReady() {
          res.status(context.status || 200); 
          res.write(preHead);
          res.write(afterHead);
          pipe(res);
          res.write(tail);
        },
        onShellError(error) {
          console.log(error);
        },
        onError(error) {
          console.log(error);
        }
      }
    );
  });

Multiple languages for the SEO

Per the Google documentation, Google is able to distinguish the duplicates of your content for different languages. For the case with DeCODE, we decided to use "hreflang" variant, which can work perfectly with React I18Next

First of all, for the configuration of the frontend, react-i18next should check for the subpath in the URL. We have restructured React Router and I18Next as following:

import {
  Routes,
  Route,
} from "react-router-dom";
import i18next from 'i18next';
import BrowserLanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from "react-i18next";

i18next
  .use(BrowserLanguageDetector)
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
      detection: {
        order: ['path', 'cookie', 'header'],
        lookupCookie: 'lang',
        caches: ['cookie'],
      }
  });
const App = ({ context }) => {
  return (
    <Routes>
      <Route path=':lang/landing/spacechains' element={<SpacechainsLanding />} />
      <Route path='/landing/spacechains' element={<SpacechainsLanding />} />
      <Route path=':lang/' element={<StartLanding context={context} />} />
      <Route path='/' element={<StartLanding context={context} />} />
      {/* other routes */}
    </Routes>
  );

Take a note, that we are using :lang here to be able to work with the links, such as /en/landing/spacechains and so on. And for the I18Next to fetch it from the subpath, it uses parameter detection, where you need to specify what should be used to detect the user desired localization. Right now you need to duplicate the routes, if you are using React Router v6, because there is no possibility to mark url path as optional.

For DeCODE, it was also necessary to specify a set of supported languages for i18n. This action was needed for eliminating the difference from the browser/server, which have been serving different language string (en and en-US). You can do it by passing the supported languages in supportedLangs field of i18next:
    
i18next.init({ supportedLangs: ['en', 'ru', 'ua'] })
    
  

On the backend side, place into the response tags, which are specific to your localized pages. Due to the fact, that Google indexes both www and non-www pages, it was necessary to specify the canonical tag. End result of this step looked as following:

  res.status(context.status || 200); 
  res.write(preHead.replace('{lang}', userBrowserLang)); // specify the language which will be served to the user
  // Specify translations for your page using the hreflang
  res.write(`
    <link rel="canonical" href="https://decodeapps.pp.ua${originalUrl}" />
    <link rel="alternate" hreflang="en" href="https://decodeapps.pp.ua/en${originalUrl}" />
    <link rel="alternate" hreflang="ru" href="https://decodeapps.pp.ua/ru${originalUrl}" />
    <link rel="alternate" hreflang="ua" href="https://decodeapps.pp.ua/ua${originalUrl}" />
    <link rel="alternate" hreflang="x-default" href="https://decodeapps.pp.ua${originalUrl}" />
  `);
  res.write(afterHead);

Tags optimization

On the website and blog, DeCODE is using a lot of images, therefore, it was necessary to define, how should they be optimized for the end user and for the search engine. To achieve better performance, all <img /> tags were replaced with <picture />, which are allowing to specify multiple sources for the image.

  const AdaptiveImage = ({ jpg, webp, alt, ...imgProps }) => {
    return (
      <picture>
        {webp && <source srcSet={webp} type="image/webp" />}
        {jpg && <source srcSet={jpg} type='image/jpeg' />}
        <img alt={alt} src={jpg} {...imgProps}/>
      </picture>
    );
  };

  export default AdaptiveImage;