Sitemaps para tus proyectos Laravel y su importancia
Tiempo de lectura: 10.57 minutos
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.
- 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.
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 Noticiasmaps.google.com
: Google Mapsplay.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 claseSitemap
- 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:
- En la versión 5.4 de Laravel, el 2do parámetro se expresaba en minutos.
- Sin embargo desde la versión 5.8, el 2do parámetro se expresa en segundos.
¿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 ?