DevAces
Mejora el posicionamiento y SEO de tu sitio en Google con React SSR

Mejora el posicionamiento y SEO de tu sitio en Google con React SSR

Por Alan AcuñaDiciembre 23, 2020

Imagínate esto, has terminado de construir el sitio web para tu negocio. Te tomó varias semanas hacerlo porque lo programaste tú mismo con ese framework que todo el mundo parece amar, llamado React. Estás súper emocionado por desplegar tu sitio, ¡y con razón! Confías en que al cabo de unas semanas vas a tener cientos de visitantes diarios. Pero pasa el tiempo y... nada. Hasta pareciera que tu sitio ni siquiera está en Google.

¿Qué paso? ¿Hiciste algo mal? Pues no, no es que hayas hecho nada mal. Simplemente es que los frameworks como React o Angular no se llevan muy bien con el algoritmo de Google y tienen un pésimo SEO (Search-Engine Optimization). ¡Pero te tengo una buena noticia! Existe una técnica que puedes usar para mejorar la posición de tus sitios web de React. Esta técnica se llama Server-Side Rendering o SSR y en este post vas a aprender a integrarla con React. Además, aprenderás a automatizar el proceso de despliegue con DevOps.

Para seguir este post te recomiendo que sepas nociones básicas sobre React y manejar la terminal de tu equipo. También te recomiendo que sepas acerca de Firebase, ya que lo usaremos para desplegar nuestro sitio de React, aunque puedes usar el hosting que más te agrade.

¿Y eso del SSR qué es?

El concepto de SSR es bien sencillo. Frameworks como React o Angular usan JavaScript para generar el contenido del sitio web del lado del cliente, lo que se conoce como Client-Side Rendering (🤯) o CSR. Esto tiene muchas ventajas, pero como todo el contenido se genera del lado del cliente, los bots de Google no "ven" nada. Para resolver esto puedes tomar el código de tu sitio y correrlo en un servidor para que así se genere tu contenido antes de que llegue al cliente, de eso se trata el SSR.

Existen otras alternativas para solucionar este problema de los frameworks como React. Puedes usar prerendering para convertir el contenido de tu sitio en contenido estático, pero si generas contenido constantemente esto no es la mejor opción.

Dicho esto, empecemos a crear nuestra aplicación de React.

Creando nuestra aplicación básica de React

Para crear nuestra aplicación de React vamos a usar create-react-app. De manera opcional, puedes hacer uso de nuestro proyecto de ejemplo para seguir este post, el cual se encuentra en el siguiente repositorio en GitHub. Ingresamos a nuestra terminal y en la carpeta que elijamos, corremos el comando:

npx create-react-app react-firebase-ssr

Esto va a generar la configuración básica para nuestra aplicación. Seguramente también necesitamos un router para nuestro sitio, así que vamos a instalar el react-router. En la carpeta de nuestro proyecto corremos el comando:

npm install react-router

Ahora puedes agregar el contenido que quieras en tu sitio de React. Pero OJO AQUÍ, cuando configures el react-router, agrega el componente <BrowserRouter> en la raíz de la aplicación, es decir, en el index.js, de esta manera:

const rootElement = document.getElementById('root');
const app = (
    <BrowserRouter>
        <App />
    </BrowserRouter>
);
render(app, rootElement);

Esta configuración nos ayudará más tarde. Y aprovechando que estamos en el index.js, vamos a cambiar la última línea por:

rootElement.hasChildNodes()
    ? hydrate(app, rootElement)
    : render(app, rootElement);

Esta línea, en términos sencillo, quiere decir que si nuestra aplicación se corre en el servidor solo se van a agregar al HTML ya generado hooks, listeners, etc. Y si se corre en el cliente que se genere todo el contenido. Entonces nuestro index.js quedaría así:

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);

Usando React Helmet para generar las etiquetas meta de nuestro sitio

Un paso importante para mejorar el SEO de cualquier sitio web, es añadir etiquetas meta a cada archivo HTML del sitio. Claro, nuestro sitio de React solo tiene un index.html, pero podemos usar una librería llamada React Helmet para generar las etiquetas meta de forma dinámica. Para instalarla vamos a correr el comando:

npm install react-helmet

Usar esta librería es tan sencillo como envolver tus etiquetas meta en el componente <Helmet> en cada una de las páginas de tu sitio. Por eso yo te recomiendo crear un componente especial Metatags.js para contener tus etiquetas meta y nada más le pasas la información que quieras por props. Este componente sería algo así:

import React from 'react';
import { Helmet } from 'react-helmet';

const Metatags = ({ title, description }) => {
    return (
        <Helmet>
            <title>{ title }</title>
            <meta name="description" content={ description }/>
            {/* Puedes añadir tantas etiquetas meta como quieras en este componente */}
        </Helmet>
    )
}

export default Metatags;

Para una mayor explicación del por qué las etiquetas meta son tan importantes para el SEO y generar tus propias etiquetas meta te recomiendo este sitio.

Integrando SSR en nuestra aplicación con CRA-Universal

Hasta ahora nuestra aplicación ha sido una aplicación común y corriente de React, así que es momento de integrar la parte de SSR. Para esto vamos a usar la librería cra-universal. Usamos los siguientes comandos:

npm install -D cra-universal
npm install @cra-express/core
npx cra-universal init

Esto va a instalar las dependencias necesarias y generar una carpeta server con los archivos app.js e index.js. También te recomiendo agregar los siguientes comandos en el apartado de "scripts" de tu package.json, los cuales los usaremos más adelante.

"scripts": {
    ...
    "start:ssr": "cra-universal start",
    "build:ssr": "cra-universal build",
}

Ahora necesitamos hacer un par de cambios en app.js. ¿Recuerdas la configuración que hicimos del react-router anteriormente? La razón por la que hicimos dicha configuración fue porque ahora necesitamos envolver nuestro componente <App/> con el componente <StaticRouter> en la función handleUniversalRender, de esta manera:

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;
}

También necesitamos otra configuración para hacer que nuestras etiquetas meta se generen del lado del servidor. En los argumentos de la función createReactAppExpress vamos a agregar el método onFinish, como se muestra a continuación:

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);
    }
});

Lo que hace el método onFinish es inyectar en el HTML las etiquetas meta que definimos con React Helmet. Al final el app.js del servidor queda de la siguiente forma:

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;

Para probar que nuestra aplicación funciona con SSR necesitamos correr los comandos npm start y npm run start:ssr en diferentes pestañas de nuestra terminal. Una vez que estamos satisfechos con nuestra aplicación podemos correr el comando npm run build:ssr para generar el build de producción de la aplicación. En este punto puedes subir la aplicación a tu hosting de preferencia, pero aquí vamos a usar Firebase Hosting y Cloud Functions para desplegar nuestra aplicación.

Desplegando nuestra aplicación en Firebase

Si aún no tienes la herramienta CLI de Firebase puedes instalarla con el comando npm install -g firebase-tools. Te recomiendo que crees un nuevo proyecto en la consola de firebase. Ahora vamos a correr el comando firebase init en la raíz de nuestra aplicación. Tendrás que seleccionar tu proyecto de Firebase y elegir hosting y functions para agregar al proyecto. La carpeta pública para el hosting será build. Te preguntará si quieres escribir tus Cloud Functions con JavaScript o con TypeScript, y como nada más agregaremos una función puedes elegir JavaScript. También te preguntará si quieres configurar el proyecto como SPA, contesta que si. Para terminar el proceso se generarán 2 archivos de Firebase en la carpeta de la aplicación.

Ahora necesitamos configurar el archivo firebase.json para redireccionar nuestro hosting a nuestra Cloud Function. Entonces nuestro firebase.json queda así:

{
    "hosting": {
        "public": "build",
        "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
        ],
        "rewrites": [
            {
                "source": "**",
                "function": "ssr"
            }
        ]
    },
    "functions": {
        "predeploy": []
    }
}

Ojo que dejé vacía la línea de predeploy, ya que de otra forma nos puede dar errores de linting cuando queramos subir nuestra Cloud Function.

Antes de escribir nuestra Cloud Function tenemos que cambiar el punto de acceso de nuestra aplicación de CRA-Universal, porque no necesitamos escuchar la aplicación en ningún puerto. Así que tenemos que crear el archivo crau.config.js en la raíz de nuestro proyecto, el cual contendrá lo siguiente:

module.exports = {
    modifyWebpack: config => {
      const newConfig = {
        ...config,
        output: {
          ...config.output,
          libraryTarget: 'commonjs2'
        },
        entry: './server/app.js' // Antes era './server/index.js'
      };
      return newConfig;
    }
};

Nota que estamos cambiando el punto de acceso de index.js a app.js. Ahora necesitamos volver a generar el build de producción con el comando npm run build:ssr.

También tenemos que instalar nuestras dependencias de React en nuestra carpeta de functions, así como la librería fs-extra que usaremos más adelante, utilizando los comandos:

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

Ahora sí, podemos escribir nuestra Cloud Function, que no tiene gran ciencia. En el index.js de la carpeta functions escribimos:

const functions = require('firebase-functions');
const app = require('./dist/server/bundle').default;

exports.ssr = functions.https.onRequest(app);

Y esa es toda la Cloud Function. Sin embargo, tenemos un pequeño problema, necesitamos copiar la carpeta dist dentro de la carpeta functions y remover algunos archivos. Lo podríamos hacer manualmente, pero... nadie tiene tiempo para eso 🤣 así que vamos a escribir un script que lo haga por nosotros. Creamos el archivo copy-app.js dentro de la carpeta functions y escribimos lo siguiente:

const fs = require('fs-extra');

(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');
})();

Esto va a copiar la carpeta dist dentro de functions, removiendo el original junto con los archivos package*.json y el index.html de nuestro build. No te preocupes, nuestra Cloud Function será la que sirva nuestro HTML. También te recomiendo agregar el comando "copy": "node copy-app" dentro de la sección de scripts del package.json de la carpeta functions.

Entonces, el proceso para desplegar nuestra aplicación a Firebase es el siguiente:

  1. Generamos nuestro build de producción: npm run build:ssr
  2. Nos movemos a la carpeta functions y corremos nuestro script para copiar nuestro build:
cd functions
npm run copy
cd ../
  1. Desplegamos a Firebase: firebase deploy

BONUS: Automatizando nuestros despliegues con Cloud Build

Nuestra aplicación de React con SSR esta lista en este punto. Sin embargo, el proceso de despliegue terminó siendo un poco tedioso e involucrado. Para poder automatizar nuestros despliegues vamos a utilizar GitHub junto con la herramienta Cloud Build de Google para hacer un proceso de CI/CD (Continuous Integration and Delivery) o DevOps.

Primero necesitas crear un nuevo repositorio en GitHub (puede ser público o privado) y hacer un commit inicial.

git add .
git commit -m "initial commit"

git remote add origin git@github.com:<tu-repositorio>.git
git push -u origin master

Después, asegúrate que este activa la API de Cloud Build para tu proyecto en la consola de GCP. Necesitas darle acceso a Cloud Build a tu proyecto de Firebase, por lo que necesitas ir al menú de IAM y darle permiso de Firebase Admin a la cuenta con terminación @cloudbuild.gserviceaccount.com.

Cloud Build IAM

Firebase no está disponible en la imagen de NPM por defecto en GCP, por lo que necesitamos un community builder, que no es más que un contenedor de Docker con las firebase-tools instaladas. Primero necesitas instalar el SDK de Google Cloud para después clonar el repositorio de los community builders y subirlo a Google Cloud, de esta manera:

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

Ojo que si estas en Windows, necesitas cambiar los caracteres CRLF por caracteres LF en el archivo firebase.bash de cloud-builders-community/firebase para poder subirlo a Google Cloud. Más información sobre esto aquí.

Una vez que subes el builder a Google Cloud podrás verlo en el registro de contenedores.

Cloud Container Registry

Ahora necesitamos definir los pasos que Cloud Build tendrá que seguir para construir y desplegar nuestro proyecto. Para eso creamos el archivo cloudbuild.yaml en la raíz del proyecto.

steps:
  # Instala dependencias
  - name: 'gcr.io/cloud-builders/npm'
    args: ['install']
  # Build
  - name: 'gcr.io/cloud-builders/npm'
    args: ['run', 'build:ssr']
  # Instala dependencias de cloud functions
  - name: 'gcr.io/cloud-builders/npm'
    dir: 'functions'
    args: ['install']
  # Copia a cloud functions
  - name: 'gcr.io/cloud-builders/npm'
    dir: 'functions'
    args: ['run', 'copy']
  # Despliega
  - name: 'gcr.io/[TU-PROYECTO]/firebase'
    args: ['deploy']

Toma en cuenta que en este archivo también puedes definir muchas opciones adicionales, por ejemplo, para correr tus pruebas unitarias si así lo deseas.

¡Casi terminamos! Solo necesitamos conectar nuestro repositorio de GitHub con Cloud Build registrando el build trigger. Asegúrate de dirigir el trigger al archivo cloudbuild.yaml.

Cloud Build Trigger

Ya solo necesitamos crear un nuevo commit y hacer el push a GitHub. Cloud Build empezará a construir la aplicación y podrás ver el progreso en la consola de GCP. ¡Eso es todo! Si necesitas más información para trabajar con Cloud Build, te recomiendo este artículo.


Con esto ya tenemos lista nuestro sitio de React con SSR. Ahora puedes probar tu sitio y asegurarte que todo funciona. Recuerda que tenemos disponible un proyecto de ejemplo en este repositorio, por si necesitas ayuda extra.

Si necesitas ayuda para crear tu sitio web, en DevAces somos expertos en la creación de sitios web profesionales, tiendas en línea y marketing digital. ¡Acércate a nosotros y crearemos la solución digital que más se acomode a tu negocio y presupuesto!

Da clic aquí para conocer nuestros servicios y contactarnos.

No olvides darle like a nuestra página en Facebook para mantenerte al tanto de nuestras publicaciones.