#314 Pretty URLs with FriendlyId
- Download:
- source codeProject Files in Zip (79.8 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (9.46 MB)
- webmFull Size VP8 Video (10.5 MB)
- ogvFull Size Theora Video (20.9 MB)
Supongamos que tenemos una sencilla aplicación de blog en Rails. La página principal muestra un listado con los artículos más recientes así como un enlace a cada uno. Si hacemos clic en uno de estos enlaces iremos a la página de dicho artículo pero la URL que aparece no resulta muy descriptiva del contenido que ofrece.
El artículo queda descrito en la URL sólo por su id
interno, que es el comportamiento por defecto en Rails. Las URLs de nuestra aplicación quedarían más descriptivas si de alguna manera se incluyese el nombre del artículo.
La forma más fácil de hacer esto en Rails es sobreescribiendo el método to_param
en el modelo correspondiente a las URLs que queremos cambiar, en este caso Article
. Se trata de un método interno que Rails usa para convertir un objeto en un parámetro de URL.
class Article < ActiveRecord::Base def to_param "#{id} #{name}".parameterize end end
Hemos sobreescrito el método para que devuelva también el nombre del artículo. Tenemos que llamar a parameterize
sobre la cadena para convertirla a un valor que se pueda mostrar en la URL. Con esto la URL de nuestro artículo pasará a ser así:
http://localhost:3000/articles/1-superman
Es importante tener en cuenta que el id
del objeto está al principio de la URL de forma que el método find
de ActiveRecord sigue funcionando. Si no queremos incluir el id
numérico del objeto la cosa se complica un poco más.
Introducción a FriendlyId
Aquí es donde aparece FriendlyId. Se trata de un plugin que facilita el uso del nombre del modelo en la URL sin tener que poner el id
como prefijo. La gema ofrece varias posibilidades pero antes de repasarlas veremos qué es lo que hay que hacer para añadirla a nuestra aplicación. Como siempre lo primero que hay que hacer es añadirlo al Gemfile
de nuestra aplicación y luego ejecutar bundle
para la instalación.
source 'http://rubygems.org' gem 'rails', '3.1.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'friendly_id'
Ahora en lugar de redefinir el método to_param
extendemos el modelo con el módulo FriendlyId
y definimos el atributo que queremos utilizar en la URL.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name end
Nuestro artículo ahora aparecerá con una URL que incluye el atributo name
pero no el id
.
http://localhost:3000/articles/Superman
Sin embargo este enfoque no resulta particularmente atractivo para artículos que tienen nombres más complicados; un artículo llamado “Batman & Robin” acabará con la siguiente URL:
http://localhost:3000/articles/Batman%20&%20Robin
Uso de slugs
La URL se genera a partir del nombre completo del artículo incluyendo espacios y puntos. Esto no es un problema si el parámetro que se pasa a FriendlyId ya es susceptible de aparecer en una URL, pero no es este el caso con los nombres de nuestros artículos por lo que tendremos que utilizar un nuevo atributo para el slug, lo que podremos hacer utilizando la opción use
del método friendly_id
.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged end
Este método buscará una columna slug
en la tabla de articles
en la base de datos por lo que tendremos que crearla con una migración.
$ rails g migration add_slug_to_articles slug:string
Siempre es buena idea añadir un índice para este atributo porque será el que usaremos para encontrar registros.
class AddSlugToArticles < ActiveRecord::Migration def change add_column :articles, :slug, :string add_index :articles, :slug end end
Ya podemos lanzar rake db:migrate
para añadir la columna y su índice a la base de datos. Tenemos que hacer una cosa más: ya tenemos algunos artículos y su columna slug
estará vacía por lo que tenemos que rellenarla. Para corregir esto podemos abrir la consola Rails y volver a guardar cada registro.
1.9.2-p290 :001 > Article.find_each(&:save)
Nuestro artículo “Batman & Robin” tendrá ahora una URL mucho mejor basada en su slug:
http://localhost:3000/articles/batman-robin
Cómo gestionar los cambios de nombre
Si editamos un artículo y modificamos su nombre, el slug también cambiará. Por ejemplo si cambiamos nuestro artículo llamado “Batman & Robin” para que se llame “Batman & Robin 2” esto hará que cambie su slug a batman-robin-2
y por tanto la URL también lo hará. Si visitamos la URL antigua del artículo veremos un mensaje de error.
Hay varias soluciones para este problema. Una es hacer que FriendlyId no actualice el slug cuando se modifique el nombre del artículo, lo que podemos hacer modificando el método should_generate_new_friendly_id?
.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged def should_generate_new_friendly_id? new_record? end end
Con este código el slug sólo se generará cuando se cree un nuevo Article
. Si actualizamos el artículo “Batman & Robin 2” y le ponemos “Batman & Robin 3” el slug seguirá siendo batman-robin-2
.
Pero ¿y si queremos lo mejor de ambos mundos, de forma que se actualice el slug cuando cambie el nombre pero las URLs antiguas sigan funcionando? Podemos hacerlo utilizando la opción history
de FriendlyId. Tendremos que eliminar el método should_generate_new_friendly_id?
que hemos redefinido antes y añadir la opción history
que mantiene un registro de los slugs anteriores.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: [:slugged, :history] end
Por supuesto hay que guardar este historial en algún sitio por lo que necesitamos una nueva tabla en la base de datos. Con FriendlyId se incluye un generador que lo hará por nosotros.
$ rails g friendly_id create db/migrate/20120101000001_create_friendly_id_slugs.rb
Cuando ejecutemos rake db:migrate
esta orden creará una nueva tabla friendly_id_slugs
.
Pero hay una trampa contra la que tenemos que estar prevenidos: la funcionalidad de historial sólo funcionará con los registros nuevos, si tenemos registros anteriores tendremos que regenerarlos para que adquieran su historial. Crearemos un nuevo artículo llamado “Hello World” para poder hacer la prueba. El artículo tendrá la siguiente URL:
http://localhost:3000/articles/hello-world
Si editamos el artículo y ponemos el nombre a “Hello World 2” el slug cambiará a hello-world-2
pero la URL original seguirá funcionando.
Sería mejor que nos devolviesen una redirección a la URL más moderna cuando visitemos una URL antigua, para lo que tenemos que hacer algunos cambios al controlador. El controlador ArticlesController
es un controlador RESTful normal con las siete acciones habituales. Tendremos que cambiar la acción show
que es la que muestra un artículo. Este es el aspecto que tiene ahora mismo:
def show @article = Article.find(params[:id]) end
Queremos que la acción nos redirija a la URL actual del artículo si la URL por la que hemos llegado no es la más reciente. Podemos hacer esto si la ruta que se ha utilizado para acceder la página no es igual que la ruta actual del artículo: si es este el caso, es que estamos usando un slug antiguo o el id
del artículo. En cualquier caso, debemos hacer una redirección.
def show @article = Article.find(params[:id]) if request.path != article_path(@article) redirect_to @article, status: :moved_permanently end end
Si visitamos http://localhost:3000/articles/hello-world iremos a la URL actual del artículo.
Con esto ya hemos visto varias de las funcionalidades de FriendlyId, pero conviene leer la documentación para ver el resto. Por ejemplo el módulo Reserved permite reservar ciertas palabras de forma que no puedan usar (por ejemplo 'new' y 'edit'). El módulo Scoped permite restringir el ámbito de los slugs dentro de una asociación y el módulo SimpleI18n añade soporte para múltiples idiomas.