Mejora el SEO de tu Aplicación agregando un Slug a tus modelos en Laravel

Mejora el SEO de tu Aplicación agregando un Slug a tus modelos en Laravel

Si estás interesado en mejorar el posicionamiento de tu sitio web en los motores de búsqueda, entonces seguramente ya sabes lo importante que es el SEO. Y dentro de las muchas técnicas que existen para mejorar la optimización de tus páginas, los slugs son una herramienta clave que no puedes pasar por alto.

En este contenido te explicaré por qué los slugs son tan importantes para el SEO y cómo añadirlos en tus modelos en Laravel puede mejorar la comprensión del contenido de tu página por parte de los motores de búsqueda.

¿Qué son los Slug?

Los slugs son importantes para el SEO porque ayudan a los motores de búsqueda a comprender el contenido de una página de manera más clara y efectiva. Los slugs son una versión simplificada y legible por humanos de la URL de una página, que generalmente incluye el título del artículo o del contenido.

Por ejemplo, si tenemos una publicación titulada “Laravel 10 y sus novedades” entonces nuestro slug sería “laravel-10-y-sus-novedades”.

Requisitos

Crearemos un modelo y su respectiva migración (-m) de la tabla posts en el terminal.

1php artisan make:model Post -m

Nuestra migración tendrá las siguientes columnas.

1public function up(): void
2{
3 Schema::create('posts', function (Blueprint $table) {
4 $table->id();
5 
6 $table->string('title');
7 $table->text('description');
8 $table->text('content');
9 $table->string('image_path');
10 $table->string('slug');
11 $table->timestamp('published_at');
12 
13 $table->timestamps();
14 });
15}

y lo ejecutamos con el siguiente comando.

1php artisan migrate

Nuestra BD se vería de la siguiente manera.

Tablas de la Base de Datos

Ahora agregaremos una ruta en el archivo routes/web.php que retornará la vista donde estarán la lista de todos los posts.

1use App\Models\Post;
2 
3Route::get('posts', function () {
4 $posts = Post::select(['id', 'title', 'image_path', 'description', 'slug', 'published_at'])
5 ->orderByDesc('published_at')
6 ->get();
7 
8 return view('posts', compact('posts'));
9})->name('show-posts');

La vista la crearemos en resources/views/posts.blade.php y tendrá el siguiente código HTML con estilos Tailwind que hemos extraído de Tailwind Components.

1<!DOCTYPE html>
2<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 
7 <title>Laravel</title>
8 
9 <!-- Fonts -->
10 <link rel="preconnect" href="https://fonts.bunny.net">
11 <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
12 
13 <script src="https://cdn.tailwindcss.com"></script>
14 </head>
15 <body class="antialiased">
16 <!-- component -->
17 <section class="bg-white dark:bg-gray-900">
18 <div class="container px-6 py-10 mx-auto">
19 <h1 class="text-3xl font-semibold text-gray-800 capitalize lg:text-4xl dark:text-white">From the blog</h1>
20 
21 <div class="grid grid-cols-1 gap-8 mt-8 md:mt-16 md:grid-cols-2">
22 @foreach($posts as $post)
23 <div class="lg:flex">
24 <img class="object-cover w-full h-56 rounded-lg lg:w-64" src="{{ $post->image_path }}" alt="">
25 
26 <div class="flex flex-col justify-between py-6 lg:mx-6">
27 <a href="/posts/{{ $post->slug }}" class="text-xl font-semibold text-gray-800 hover:underline dark:text-white ">
28 {{ $post->title }}
29 </a>
30 
31 <span class="text-sm text-gray-500 dark:text-gray-300">{{ $post->published_at }}</span>
32 </div>
33 </div>
34 @endforeach
35 </div>
36 </div>
37 </section>
38 </body>
39</html>

Dado que no tenemos ningún post creado, la vista no nos mostrará nada así que crearemos un factory para crear varios posts automáticamente.

1php artisan make:factory PostFactory

En nuestro archivo database/factory/PostFactory.php agregamos las columnas con los datos de prueba que usaremos.

1<?php
2 
3namespace Database\Factories;
4 
5use Illuminate\Database\Eloquent\Factories\Factory;
6 
7/**
8 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
9 */
10class PostFactory extends Factory
11{
12 /**
13 * Define the model's default state.
14 *
15 * @return array<string, mixed>
16 */
17 public function definition(): array
18 {
19 return [
20 'title' => fake()->text(40),
21 'description' => fake()->text(100),
22 'content' => fake()->text(300),
23 'image_path' => fake()->imageUrl,
24 'slug' => null,
25 'published_at' => fake()->dateTimeThisYear
26 ];
27 }
28}

Agregando el Slug

Lo que vamos hacer es que cada vez que se guarde un post, el slug se cree automáticamente en base al nombre, para ello tenemos que crear un mutator en nuestro archivo Models/Post.php.

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Database\Eloquent\Model;
8use Illuminate\Support\Str;
9 
10class Post extends Model
11{
12 use HasFactory;
13 
14 protected function slug(): Attribute
15 {
16 return Attribute::make(
17 get: fn ($value) => $value,
18 set: fn ($value) => Str::slug($this->title)
19 );
20 }
21}
  • get: Cuando queramos obtener el slug de un post simplemente devolvemos el valor.
  • set: Cuando queramos guardar el slug de un post, usaremos la función Str::slug lo cual convierte en slug cualquier string que en nuestro caso será el título del post.

Para poder probar vamos a crear nuestros posts con el factory, para ello usaremos Laravel Tinker

1php artisan tinker

Dentro generaremos 40 posts automáticamente con este código:

1App\Models\Post::factory()->count(40)->create()

Si entramos a nuestra vista “/posts” podemos ver la lista de los posts que hemos creado.

Vista de los Posts

Además veremos que los registros de nuestra tabla posts incluye la columna slug que se ha creado automáticamente gracias a nuestro mutator.

Slugs creados

Lo que haremos ahora es que al dar clic a un post nos envié al contenido. Para ello agregamos otra ruta en el archivo routes/web.php.

1Route::get('posts/{post:slug}', function (Post $post) {
2 return view('post', compact('post'));
3})->name('show-post');

Cabe destacar que estamos usando el Route Model Binding para que podamos obtener el post pasándole el slug.

La vista la obtendremos de Tailwind Toolbox y la pondremos en el archivo resources/views/post.blade.php

1<!DOCTYPE html>
2<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 
7 <title>Laravel</title>
8 
9 <!-- Fonts -->
10 <link rel="preconnect" href="https://fonts.bunny.net">
11 <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
12 
13 <script src="https://cdn.tailwindcss.com"></script>
14 </head>
15 <body class="antialiased">
16 <div class="container w-full md:max-w-3xl mx-auto pt-20">
17 
18 <div class="w-full px-4 md:px-6 text-xl text-gray-800 leading-normal" style="font-family:Georgia,serif;">
19 
20 <!--Title-->
21 <div class="font-sans">
22 <p class="text-base md:text-sm text-green-500 font-bold">&lt; <a href="/posts" class="text-base md:text-sm text-green-500 font-bold no-underline hover:underline">BACK TO BLOG</a></p>
23 <h1 class="font-bold font-sans break-normal text-gray-900 pt-6 pb-2 text-3xl md:text-4xl">{{ $post->title }}</h1>
24 <p class="text-sm md:text-base font-normal text-gray-600">Published {{ $post->published_at }}</p>
25 </div>
26 
27 
28 {{ $post->content }}
29 
30 </div>
31 
32 <!--Tags -->
33 <div class="text-base md:text-sm text-gray-500 px-4 py-6">
34 Tags: <a href="#" class="text-base md:text-sm text-green-500 no-underline hover:underline">Link</a> . <a href="#" class="text-base md:text-sm text-green-500 no-underline hover:underline">Link</a>
35 </div>
36 
37 <!--Divider-->
38 <hr class="border-b-2 border-gray-400 mb-8 mx-4">
39 
40 
41 <!--Subscribe-->
42 <div class="container px-4">
43 <div class="font-sans bg-gradient-to-b from-green-100 to-gray-100 rounded-lg shadow-xl p-4 text-center">
44 <h2 class="font-bold break-normal text-xl md:text-3xl">Subscribe to my Newsletter</h2>
45 <h3 class="font-bold break-normal text-gray-600 text-sm md:text-base">Get the latest posts delivered right to your inbox</h3>
46 <div class="w-full text-center pt-4">
47 <form action="#">
48 <div class="max-w-xl mx-auto p-1 pr-0 flex flex-wrap items-center">
49 <input type="email" placeholder="youremail@example.com" class="flex-1 mt-4 appearance-none border border-gray-400 rounded shadow-md p-3 text-gray-600 mr-2 focus:outline-none">
50 <button type="submit" class="flex-1 mt-4 block md:inline-block appearance-none bg-green-500 text-white text-base font-semibold tracking-wider uppercase py-4 rounded shadow hover:bg-green-400">Subscribe</button>
51 </div>
52 </form>
53 </div>
54 </div>
55 </div>
56 <!-- /Subscribe-->
57 
58 
59 
60 <!--Author-->
61 <div class="flex w-full items-center font-sans px-4 py-12">
62 <img class="w-10 h-10 rounded-full mr-4" src="http://i.pravatar.cc/300" alt="Avatar of Author">
63 <div class="flex-1 px-2">
64 <p class="text-base font-bold text-base md:text-xl leading-none mb-2">Jo Bloggerson</p>
65 <p class="text-gray-600 text-xs md:text-base">Minimal Blog Tailwind CSS template by <a class="text-green-500 no-underline hover:underline" href="https://www.tailwindtoolbox.com">TailwindToolbox.com</a></p>
66 </div>
67 <div class="justify-end">
68 <button class="bg-transparent border border-gray-500 hover:border-green-500 text-xs text-gray-500 hover:text-green-500 font-bold py-2 px-4 rounded-full">Read More</button>
69 </div>
70 </div>
71 <!--/Author-->
72 
73 <!--Divider-->
74 <hr class="border-b-2 border-gray-400 mb-8 mx-4">
75 
76 <!--Next & Prev Links-->
77 <div class="font-sans flex justify-between content-center px-4 pb-12">
78 <div class="text-left">
79 <span class="text-xs md:text-sm font-normal text-gray-600">&lt; Previous Post</span><br>
80 <p><a href="#" class="break-normal text-base md:text-sm text-green-500 font-bold no-underline hover:underline">Blog title</a></p>
81 </div>
82 <div class="text-right">
83 <span class="text-xs md:text-sm font-normal text-gray-600">Next Post &gt;</span><br>
84 <p><a href="#" class="break-normal text-base md:text-sm text-green-500 font-bold no-underline hover:underline">Blog title</a></p>
85 </div>
86 </div>
87 
88 
89 <!--/Next & Prev Links-->
90 
91 </div>
92 <!--/container-->
93 </body>
94</html>

¡Y listo! ya podemos navegar a nuestros posts usando el slug.

Probando la funcionalidad

Versión Video

Repositorio en Github

Regístrate para que cada semana aprendas algo nuevo

Tendrás tutoriales, tips, conceptos y puedas convertirte en un artesano de todo el ecosistema Laravel.

Nuevo Curso
Chat en Tiempo Real con Laravel Broadcasting

Revisa los detalles del nuevo curso en desarrollo

App screenshot