Sitemaps para tus proyectos Laravel y su importancia

En esta ocasión vamos a ver cómo generar un sitemap para nuestros proyectos Laravel.

Pero empecemos respondiendo algunas preguntas frecuentes.

¬ŅQu√© es y por qu√© es importante tener un sitemap?

Un sitemap es un archivo XML (o bien una ruta que devuelve una respuesta en formato XML), indicando las distintas p√°ginas que est√°n presentes en nuestro sitio web.

No todas, pero sí las más importantes: aquellas que queremos que sean indexadas por los buscadores (como Google).

¬ŅPor qu√© es importante?

Muy buena pregunta.

Si no tienes un sitemap, Google puede indexar tu sitio web de todas formas, pero, puede tardar m√°s tiempo o bien puede no indexar todas las secciones de tu sitio web.

Los sitemaps son importantes para mejorar el SEO de tu p√°gina. SEO significa "Search Engine Optimization", y hace referencia a qu√© tan bien posicionado se encuentra tu sitio web en los motores de b√ļsqueda.

De hecho, si buscas analizadores de SEO (existen varios gratuitos), verás que la gran mayoría (si no es que todos), consideran como un factor importante contar con un sitemap. Si no tienes uno, ya tienes unos puntos menos en tu evaluación.

Ten en cuenta que:

  • Tener enlaces internos (p√°ginas que llevan a otras p√°ginas dentro de tu mismo sitio web) es muy importante.
  • Sin embargo, a veces no tenemos bien estructurados los enlaces de navegaci√≥n en nuestro sitio.
  • Esto significa que podemos navegar hacia y desde las distintas secciones, pero algunas reciben m√°s enlaces que otras.
  • Inclusive, muchas veces no tenemos enlaces internos para ciertas p√°ginas.

Un sitemap es la solución ideal ante dicho escenario.

¬ŅC√≥mo podemos agregar un sitemap a nuestro proyecto Laravel?

Como debes haber notado, hoy en día existen muchos paquetes para Laravel.

  • Existen paquetes que generan sitemaps con TODAS las rutas de nuestro proyecto (gui√°ndose de las rutas que tenemos declaradas).
  • Existen paquetes que hacen "crawling" sobre nuestro sitio para reconocer TODOS los enlaces que est√°n presentes, e identificar as√≠ las p√°ginas que son accesibles.

Sin embargo, en esta ocasión vamos a escribir nuestra propia solución. De paso que conocemos más acerca de la estructura de un sitemap.

¬ŅQu√© rutas deben ser consideradas dentro de un sitemap?

Esto depende de las características de tu proyecto.

Por ejemplo:

  • Para un ecommerce ser√° importante registrar todas las rutas correspondientes a los productos, marcas y categor√≠as.
  • Para un blog ser√° importante enlazar las etiquetas, los temas, y cada uno de los art√≠culos.

Lo que no debemos incluir en un sitemap es m√°s bien:

  • Enlaces que llevan hacia secciones con acceso restringido (por ejemplo p√°ginas s√≥lo disponibles para usuarios que han iniciado sesi√≥n o que requieren de un rol espec√≠fico).
  • P√°ginas de prueba, p√°ginas que existen s√≥lo temporalmente, o aquellas a las que no queremos llevar tr√°fico, ni indexar con relaci√≥n a buscadores.
  • Rutas que son secciones finales de un flujo. Por ejemplo: si tenemos una landing page, cuyo objetivo es llevarnos a una p√°gina final para realizar una compra, no es adecuado considerar en nuestro sitemap la URL de la p√°gina de compra, ya que un usuario podr√≠a llegar all√≠ sin pasar por los pasos previos.
  • Enlaces de descarga o URLs correspondientes a archivos est√°ticos.
  • Rutas que ejecutan acciones. Por ejemplo: si tenemos rutas que guardan preferencias, como agregar a favoritos, que activan un "modo nocturno" en nuestra p√°gina, o que no devuelven una vista, no tiene sentido considerarlas como parte de nuestro sitemap.

    Si est√°s usando los verbos POST, PUT, PATCH y DELETE como es debido, te resultar√° m√°s sencillo excluir estas rutas.

¬ŅQu√© formato debe tener un sitemap?

Un sitemap se define en formato XML y aquí podemos ver un ejemplo usando los datos que típicamente se indican por cada URL:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
   <url>
      <loc>https://programacionymas.com/</loc>
      <lastmod>2019-07-31</lastmod>
      <changefreq>monthly</changefreq>
      <priority>1.0</priority>
   </url>
</urlset>

En el ejemplo anterior tenemos una √ļnica etiqueta url al interior del urlset. La idea es tener una por cada p√°gina p√ļblica de nuestro sitio.

Seguro que ya imaginas el significado de cada dato en su interior:

  • loc: Es la URL de la p√°gina.
  • lastmod: Fecha de su √ļltima modificaci√≥n.
  • changefreq: Frecuencia con que se actualiza.
  • priority: La importancia de esta p√°gina.

Respecto a la prioridad, debe asignarse 1.0 a la p√°gina m√°s importante de nuestro sitio (generalmente la p√°gina de inicio). La prioridad para las dem√°s p√°ginas ser√° menor, seg√ļn su importancia, en este sentido: 0.9, 0.8, 0.7, 0.6, 0.5 (los valores v√°lidos se expresan en d√©cimas).

Sobre la frecuencia de cambio, tenemos varios valores disponibles. Los menciono aquí para que los tengas a tu alcance:

  • always (cambia en cada visita)
  • hourly (cada hora)
  • daily (diariamente)
  • weekly (semanalmente)
  • monthly (mensualmente)
  • yearly (anualmente)
  • never (nunca) - generalmente para p√°ginas archivadas

¬ŅPor d√≥nde empezamos?

En proyectos medianamente grandes, existe una tendencia a organizar nuestras rutas en distintos archivos. Entonces mi sugerencia es:

  • Copia todas tus rutas a un archivo de texto (en tu editor preferido)
  • Comienza a eliminar todas aquellas que no quieres considerar en el sitemap
  • Has una revisi√≥n final para confirmar que te has quedado con las apropiadas
  • Identifica las rutas que son √ļnicas y aquellas que representan a m√ļltiples p√°ginas

¬ŅA qu√© me refiero con esto √ļltimo?

  • Ejemplo de ruta como p√°gina √ļnica: una ruta /planes que lista los planes de suscripci√≥n disponibles.
  • Ejemplo de ruta que representa m√ļtliples p√°ginas: /blog/{article} representa a cada uno de los art√≠culos de nuestro blog.

Una vez hecho ello, lo siguiente es:

  • Asignar una prioridad a cada una (opcionalmente las puedes ordenar de mayor a menor)
  • Determinar con qu√© frecuencia se actualizan estas p√°ginas

¬ŅQu√© hacer con las redirecciones?

A veces encontramos rutas que redirigen hacia otras, porque en alg√ļn momento decidimos cambiarlas.

¬ŅCu√°l de las rutas agregamos al sitemap? ¬ŅLa antigua, la ruta nueva o ambas?

Para tomar una decisi√≥n necesitamos saber si a√ļn recibimos visitas en las rutas antiguas.

Si dichas rutas ya no son visitadas, podemos reciclarlas.

¬ŅPero c√≥mo sabemos si una p√°gina en espec√≠fico de nuestro sitio recibe visitas o no?

Para ello podemos apoyarnos de Google Analytics (si no lo usas, te recomiendo empezar a hacerlo, para que tengas estadísticas de qué secciones son las más visitadas).

Los pasos a seguir son 2:

  • Primero debes ir a Comportamiento > Contenido del sitio > Todas las p√°ginas.
Google Analytics - Contenido del sitio
  • Por √ļltimo, escribe en esta caja de texto la URL de tu inter√©s. En la parte inferior ver√°s el n√ļmero de visitas que ha recibido, entre otros datos.
Google Analytics - Visitas hacia una página específica

Tengo m√ļltiples subdominios, ¬Ņcu√°ntos sitemaps necesito?

Un subdominio resulta muy √ļtil para separar contenido: porque la tem√°tica es distinta, o porque se trata de una variante que necesita su propio espacio.

Por ejemplo, Google usa subdominios distintos para sus productos:

  • news.google.com: Google Noticias
  • maps.google.com: Google Maps
  • play.google.com: Google Play Store

Entonces: lo recomendable es tener un sitemap por cada subdominio.

Si un subdominio es dado de baja, su sitemap correspondiente desaparecer√°, pero esto no afectar√° al sitemap de los dem√°s subdominios (ni al sitemap del dominio principal).

Generación del XML para nuestro sitemap

A estas alturas debes tener identificadas las rutas que quieres considerar en el sitemap de tu sitio web.

A continuación te presento un ejemplo simplificado, con relación a este mismo sitio web, sobre el cual navegas.

Dominio principal

Ruta Descripción Tipo de ruta Prioridad
/ P√°gina de inicio. √önica 1.0
/asesoria Información acerca de sesiones de asesoría. Única 0.8
/contacto Formulario de contacto. √önica 0.8
/becas Información sobre becas y descuentos. Única 0.7
/@{username} P√°ginas de perfil para cada usuario. M√ļltiple 0.7
/blog Últimos artículos. Categorías y etiquetas. Única 0.8
/blog/{slug} Representa a cada art√≠culo del blog. M√ļltiple 0.9
/categorias Todas las categorías del blog. Única 0.7
/categorias/{slug} Lista de art√≠culos para una categor√≠a. M√ļltiple 0.8
/tags Todas las etiquetas del blog. √önica 0.7
/tags/{name} Art√≠culos asociados con la etiqueta seleccionada. M√ļltiple 0.8
/portafolio Listado de algunas aplicaciones desarrolladas. √önica 0.6
/portafolio/{slug} Informaci√≥n detallada sobre una aplicaci√≥n. M√ļltiple 0.6
/{slug} P√°ginas informativas, como gu√≠as o anuncios. M√ļltiple 0.8

Subdominio "series"

Ruta Descripción Tipo de ruta Prioridad
/ P√°gina principal. √önica 1.0
/categorias Listado de categorías (o tecnologías). Única 0.8
/categorias/{slug} Lista de series pertenecientes a una categor√≠a. M√ļltiple 0.8
/lecciones/{episode} Lecci√≥n independiente. M√ļltiple 0.7
/{slug} P√°gina de serie: cap√≠tulos e informaci√≥n. M√ļltiple 0.9
/{slug}/{episode} Un episodio (o cap√≠tulo) perteneciente a una serie. M√ļltiple 0.8

Bien.

Llegados a este punto lo primero que haremos es definir una ruta para mostrar allí nuestro sitemap:

Route::get('/sitemap.xml', 'SiteMapController@index');

Ten en cuenta que, aunque la ruta termina en .xml es una ruta declarada como cualquier otra, y no un archivo.

La ruta puede llamarse de manera diferente si lo prefieres, por ejemplo /sitemap, pero si ese es el caso, deber√°s indicar ello en el archivo robots.txt para que sea accesible por los motores de b√ļsqueda (y sean conscientes de ello).

Entonces, vamos a empezar creando un controlador nuevo:

php artisan make:controller SiteMapController

Y definiendo un método index en su interior:

class SiteMapController extends Controller
{
    private $siteMap;

    public function index()
    {
        $this->siteMap = new SiteMap();

        $this->addUniqueRoutes();
        $this->addArticles();
        $this->addCategories();
        $this->addDynamicPages();
        $this->addTags();
        $this->addProjects();
        $this->addProfilePages();

        return response($this->siteMap->build(), 200)
            ->header('Content-Type', 'text/xml');
    }

    private function addUniqueRoutes()
    {
        // ...
    }

    private function addProfilePages()
    {
        // ...
    }

    private function addArticles()
    {
        // ...
    }

    private function addCategories()
    {
        // ...
    }

    private function addTags()
    {
        // ...
    }

    private function addProjects()
    {
        // ...
    }

    private function addDynamicPages()
    {
        // ...
    }
}
  • En nuestro controlador definimos un atributo $siteMap para que sea accesible en toda la clase.
  • En nuestro m√©todo index creamos una instancia de la clase Sitemap
  • y le asignamos las URLs que queremos considerar, a trav√©s de otros m√©todos que declaramos como privados.

He creído conveniente usar 2 clases: una para representar al sitemap de nuestro sitio, y otra para representar a cada URL en su interior.

Clase Sitemap

class SiteMap
{
    const START_TAG = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
    const END_TAG = '</urlset>';

    // to build the XML content
    private $content;

    public function add(Url $siteMapUrl)
    {
        $this->content .= $siteMapUrl->build();
    }

    public function build()
    {
        return self::START_TAG . $this->content . self::END_TAG;
    }
}

Clase URL

class Url
{
    private $url;
    private $lastUpdate;
    private $frequency;
    private $priority;

    public static function create($url)
    {
        $newNode = new self();
        $newNode->url = url($url);
        return $newNode;
    }

    public function lastUpdate($lastUpdate)
    {
        $this->lastUpdate = $lastUpdate;
        return $this;
    }

    public function frequency($frequency)
    {
        $this->frequency = $frequency;
        return $this;
    }

    public function priority($priority)
    {
        $this->priority = $priority;
        return $this;
    }

    public function build()
    {
        // $url = 'https://programacionymas.com/';
        // $lastUpdate = '2019-07-31T01:06:39+00:00';
        // $frequency = 'monthly';
        // $priority = '1.00';
        return "<url>" .
            "<loc>$this->url</loc>" .
            "<lastmod>$this->lastUpdate</lastmod>" .
            "<changefreq>$this->frequency</changefreq>" .
            "<priority>$this->priority</priority>" .
        "</url>";
    }
}

¬ŅD√≥nde debes situar estas clases?

Eso depende de tu proyecto. Por ejemplo:

  • Si tu proyecto representa un √ļnico sitio web, y s√≥lo vas a necesitar un sitemap, puedes definir estas clases en el mismo archivo donde est√° tu controlador, en la parte superior.
  • Si tu proyecto representa m√ļltiples sitios, y sirve contenido para diferentes subdominios, donde la estructura de los sitemap es distinta, tal vez lo m√°s conveniente para ti sea tener estas clases en archivos independientes, para que las puedas usar desde distintos lugares.

Ahora bien, ¬Ņqu√© representan los m√©todos privados del ejemplo?

  • addUniqueRoutes es un m√©todo para agregar las URLs de las rutas √ļnicas al sitemap.
  • addArticles, addCategories, addDynamicPages y todos los dem√°s son m√©todos para agregar contenido din√°mico al sitemap.

Empecemos definiendo las "rutas √ļnicas", que son constantes (son fijas y no requieren consultar nuestra base de datos):

private function addUniqueRoutes()
{
    $startOfMonth = Carbon::now()->startOfMonth()->format('c');

    $this->siteMap->add(
        Url::create('/')
            ->lastUpdate($startOfMonth)
            ->frequency('monthly')
            ->priority('1.00')
    );

    $this->siteMap->add(
        Url::create('/asesoria')
            ->lastUpdate($startOfMonth)
            ->frequency('monthly')
            ->priority('0.8')
    );

    $this->siteMap->add(
        Url::create('/contacto')
            ->lastUpdate($startOfMonth)
            ->frequency('yearly')
            ->priority('0.8')
    );

    $this->siteMap->add(
        Url::create('/becas')
            ->lastUpdate($startOfMonth)
            ->frequency('monthly')
            ->priority('0.7')
    );

    $this->siteMap->add(
        Url::create('/blog')
            ->lastUpdate($startOfMonth)
            ->frequency('monthly')
            ->priority('0.8')
    );

    $this->siteMap->add(
        Url::create('/categorias')
            ->lastUpdate($startOfMonth)
            ->frequency('yearly')
            ->priority('0.7')
    );

    $this->siteMap->add(
        Url::create('/tags')
            ->lastUpdate($startOfMonth)
            ->frequency('yearly')
            ->priority('0.7')
    );

    $this->siteMap->add(
        Url::create('/portafolio')
            ->lastUpdate($startOfMonth)
            ->frequency('yearly')
            ->priority('0.6')
    );
}

En el ejemplo anterior debes modificar las URLs, la frecuencia de actualizaci√≥n, la prioridad, y agregar o quitar URLs seg√ļn corresponda.

La variable $startOfMonth es una cadena que representa una fecha con el formato requerido por el sitemap.

La fecha representada es el inicio del mes actual. Puedes usar lo mismo o modificar la fecha si conoces cu√°ndo se modificaron por √ļltima vez tales rutas.

Respecto a las rutas que representan m√ļltiples URLs, √©stas generalmente contienen par√°metros de ruta, por lo que debemos iterar sobre nuestros datos, y agregar cada una de tales entidades al sitemap, con el formato adecuado.

La implementación de esto depende de las entidades (o modelos) que tengas definidos en tu proyecto.

De todas formas, a modo de ejemplo, te muestro a continuación cómo agrego los artículos de mi blog al sitemap, y así mismo las categorías, y las páginas que están definidas dinámicamente (a través de un editor y almacenadas en la base de datos):

private function addArticles()
{
    $articles = Article::published()->whereNotNull('slug')->get([
        'slug', 'updated_at'
    ]);

    foreach ($articles as $article) {
        $this->siteMap->add(
            Url::create("/blog/$article->slug")
                ->lastUpdate($article->updated_at->startOfMonth()->format('c'))
                ->frequency('monthly')
                ->priority('0.9')
        );
    }
}

private function addCategories()
{
    $categories = ArticleCategory::withCount('articles')
        ->having('articles_count', '>', 0)
        ->get(['slug', 'updated_at']);

    foreach ($categories as $category) {
        $this->siteMap->add(
            Url::create("/categorias/$category->slug")
                ->lastUpdate($category->updated_at->startOfMonth()->format('c'))
                ->frequency('monthly')
                ->priority('0.8')
        );
    }
}

private function addDynamicPages()
{
    $pages = Page::where('published', true)->get(['slug', 'updated_at']);

    foreach ($pages as $page) {
        $this->siteMap->add(
            Url::create($page->slug)
                ->lastUpdate($page->updated_at->startOfMonth()->format('c'))
                ->frequency('monthly')
                ->priority('0.8')
        );
    }
}

Si eres observador, habr√°s notado que en el ejemplo anterior:

  • No listo todos los art√≠culos, s√≥lo aquellos que tienen estado "publicado".
  • No listo todas las categor√≠as, s√≥lo aquellas que contienen art√≠culos publicados en su interior.
  • Uso el campo updated_at para obtener desde all√≠ una cadena, con el formato requerido para la fecha.

Sitemap almacenado en Caché

Si tienes m√ļltiples entidades y quieres evitar que todas tus consultas se ejecuten continuamente, puedes hacer uso de la clase Cache de Laravel.

Esta clase permite recordar valores temporalmente.

Como en nuestra solución no usamos ninguna vista blade y simplemente creamos una cadena con el contenido adecuado, podemos recordar este valor usando Cache.

¬ŅC√≥mo se hace ello?

Podemos actualizar nuestro método index de esta manera:

public function index()
{

    $siteMapXml = Cache::remember('sitemap', 3, function () {
        $this->siteMap = new SiteMap();

        $this->addUniqueRoutes();
        $this->addArticles();
        $this->addCategories();
        $this->addDynamicPages();
        $this->addTags();
        $this->addProjects();
        $this->addProfilePages();

        return $this->siteMap->build();
    });

    return response($siteMapXml, 200)
        ->header('Content-Type', 'text/xml');
}

El método remember recordará el contenido de nuestro sitemap bajo el nombre "sitemap" durante el tiempo que le indiquemos en el segundo parámetro.

Cuando la variable caduque y no exista (o bien se acceda por 1ra vez), la función usada como 3er argumento se ejecutará, obteniendo un nuevo valor para ser recordado.

Sólo ten cuidado con el 2do parámetro. Por ejemplo:

¬ŅTienes un sitemap muy extenso?

Si tu sitio web crece mucho y de pronto tienes varios cientos o incluso miles de URLs en un mismo sitemap, no te preocupes, puedes separar ese sitemap en varios sitemaps.

Un sitemap que apunta hacia a otros sitemaps se conoce como un índice de sitemaps, y es bueno usarlos, para mantener nuestras rutas organizadas. De hecho, Google nos explica cómo separar el contenido de un extenso sitemap en varios de ellos.

La estructura es la siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <sitemap>
        <loc>https://programacionymas.com/sitemaps/general.xml</loc>
    </sitemap>
    <sitemap>
        <loc>https://programacionymas.com/sitemaps/series.xml</loc>
    </sitemap>
</sitemapindex>

En esta estructura de ejemplo se definen 2 sitemaps, donde:

  • sitemapindex es la etiqueta padre, correspondiente al √≠ndice de sitemaps.
  • sitemap es una etiqueta que representa a cada sitemap listado.
  • Y loc es la ubicaci√≥n de cada sitemap.

Conclusión

Como ves, no es complicado definir un sitemap (o varios de ellos), pero sí requiere algo de tiempo, para revisar todas las rutas que tenemos en nuestro proyecto.

Lo bueno de esto es que, podemos aprovechar la ocasión para identificar:

  • Rutas que ya no necesitamos
  • Funcionalidades que quedaron en stand-by
  • Nombres de rutas que podemos mejorar

Y bien:

Espero que estos ejemplos te hayan sido de ayuda y puedas configurar adecuadamente un sitemap para cada uno de tus proyectos Laravel ?

# laravel

Logo de Programación y más

Comparte este post si te fue de ayuda ūüôā.

Regístrate

Accede a todos los cursos, y resuelve todas tus dudas.

Cursos Recomendados

Imagen para el curso Aprende Laravel

Aprende Laravel

Aprende Laravel desde cero y desarrolla aplicaciones web reales, en tiempo récord, de la mano de Laravel.

Iniciar curso
Imagen para el curso Laravel Upgrade

Laravel Upgrade

Actualiza tus proyectos desde cualquier versi√≥n hasta la √ļltima versi√≥n estable de Laravel.

Iniciar curso
Imagen para el curso Laravel y Vue

Laravel y Vue

Desarrollemos un Messenger! Aprende sobre Channels, Queues, Vuex, JWT, Sesiones, BootstrapVue y mucho m√°s.

Iniciar curso

Espera un momento ...

¬ŅTe gustar√≠a llevar mi curso de Laravel, gratis?

Sólo debes ingresar tus datos: