Aprende a generar un Sitemap para tus proyectos Laravel

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, te consideran unos puntos menos en su 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, e incluso 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í qué páginas 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 correspondientes a secciones finales de un flujo. Por ejemplo: si tenemos una landing page, que lleva hacia una página, ésta lleva hacia otra y tenemos una página final para realizar la 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', '[email protected]');

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. Y cuando la variable caduque y no exista (o bien se acceda por 1ra vez), la función que está en el 3er parámetro se ejecutará, obteniendo un nuevo valor para ser recordado.

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

Conclusión

Como ves, no es complicado definir un sitemap, pero sí requiere algo de tiempo 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

Cursos recomendados

Curso de Laravel 5.5

Aprende Laravel

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

Ver más
Curso de Laravel y OAuth 2 (Login con redes sociales)

Laravel y OAuth2

Veamos cómo implementar un login mediante redes sociales! Aprende y aplica esto sobre cualquier proyecto Laravel.

Ver más
Curso de Laravel, Vue.js y Pusher

Aprende Vue.js

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

Ver más
Logo de Programación y más

¿Tienes alguna duda?

Si algo no te quedó claro o tienes alguna sugerencia, escribe un comentario aquí debajo.

Además recuerda compartir el post si te resultó de ayuda. Gracias!

Antes que te vayas

Inscríbete en nuestro curso gratuito de Laravel