#221 Subdomains in Rails 3
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (13.7 MB)
- webmFull Size VP8 Video (35.2 MB)
- ogvFull Size Theora Video (26.4 MB)
Ya han pasado casi dos años desde que tratamos de los subdominios por última vez. En Rails 3 se ha añadido soporte para los subdominios y en este episodio veremos qué cosas nuevas podemos hacer.
Vamos a trabajar con la misma aplicación del episodio dedicado a los subdominios: se trata de un sencilla aplicación que soporta múltiples blogs. Debajo se muestra la portada en la que aparece un listado de todos los blogs. Cada blog tiene varios artículos asociados.
Si hacemos clic en el enlace para editar un blog veremos que cada uno de ellos tiene dos atributos: name
y subdomain
. Queremos utilizar el atributo subdomain
como subdominio de la URL de dicho blog.
Configuración de subdominios en el entorno de desarrollo
Lo primero que tenemos que resolver es cómo encontrar una forma de gestionar los subdominios en el entorno de desarrollo. La URL de nuestra aplicación es http://localhost:3000 que no permite subdominios, por lo que tendremos que usar algún método alternativo.
Una forma de hacerlo podría ser configurando nuestra aplicación para utilizar
Passenger. Podríamos entonces darle un dominio de tipo http://blog.local
y editar el fichero /etc/hosts
para configurar subdominios de esta manera:
127.0.0.1 personal.blog.local company.blog.local
Lo ideal sería poder utilizar un carácter comodín que casase con cualquier subdominio de forma que no tuviésemos que estar continuamente cambiando este archivo durante el desarrollo de nuestra aplicación, pero por desgracia esto no es posible así que tenemos que buscar otra posibilidad.
Una buena solución es la que que ha presentado
Tim Pope en su blog. Ha comprado el dominio smackaho.st
y ha hecho que sea comodín de localhost
. Uno de los comentaristas en ese blog ha hecho algo parecido con un dominio similar, llamado lvh.me
, y éste es el dominio que vamos a usar.
Si abrimos http://lvh.me:3000/ veremos la página de nuestra aplicación porque el dominio lvh.me
resuelve a la dirección IP 127.0.0.1. La diferencia es que ahora podemos añadir delante cualquier subdominio a la URL y este seguirá apuntando a la misma aplicación.
Como ya podemos usar subdominios en nuestra aplicación, ya podemos configurar sus rutas para que el subdominio personal
vaya a la acción show
del blog correspondiente. Este es el aspecto de nuestro fichero de rutas:
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs root :to => "blogs#index" end
Ahora mismo la ruta raíz va a la acción blogs/index
. Queremos que esto ocurra sólo si no se ha especificado ningún subdominio, si lo hubiera habría que mostrar la acción blogs/show
. Esto en Rails 3 puede hacerse añadiendo una restricción.
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs match '/' => 'blogs#show', :constraints => { :subdomain => /.+/ } root :to => "blogs#index" end
Esta funcionalidad (que ya viene incorporada en Rails 3) habría requerido el uso de un plugin si nuestra aplicación utilizase Rails 2. La opcíon :subdomain
recibe o bien una cadena o una expresión regular, que es lo que hemos usado nosotros, para aceptar cualquier subdominio que tenga al menos una letra. Obsérvese que es importante que la ruta raíz aparezca después de la ruta de subdominio o de lo contrario la ruta raíz tendrá preferencia sobre las rutas de subdominios, por las que nunca pasaremos. Como regla general cuanto más específica es una ruta debería aparecer más arriba en la lista de rutas.
Si ahora visitamos el subdominio personal
veremos un error que era de esperar porque estamos en la acción show
de BlogsController
a la que no le hemos pasado un id
.
Esto es fácil de arreglar. Tan sólo tenemos que modificar la acción show
de forma que encuentre un blog por su identificador de subdominio en lugar de por su id
.
def show @blog = Blog.find_by_subdomain!(request.subdomain) end
Si ahora recargamos la página veremos lo artículos de nuestro blog “Personal” .
Si quitamos el subdominio veremos otra vez la página de inicio que teníamos antes.
Aún nos queda un pequeño problema con las rutas. Si visitamos http://www.lvh.me:3000 nuestra aplicación buscará un blog con el subdominio www
cuando claramente debería redirigirnos a la portada de los blogs. Podríamos intentar arreglar esto haciendo algo astuto con la expresión regular del archivo de rutas, pero para tener más flexibilidad y control sobre los subodminios lo haremos en código Ruby, porque en Rails 3 podemos escribir una clase que gestione las restricciones de las rutas.
En primer lugar vamos a modificar el archivo de rutas para que utilice una nueva clase llamada Subdomain
. Lo haremos utilizando el método constraints
y pasándole la clase como argumento.
Bloggit::Application.routes.draw do |map| resources :comments resources :articles resources :blogs constraints(Subdomain) do match '/' => 'blogs#show' end root :to => "blogs#index" end
A continuación tenemos que crear la clase Subdomain
que pondremos en el directorio lib
. Esta clase tendrá que tener un método de clase llamado matches?
que reciba un objeto de tipo request
como parámetro. Este objeto es el mismo objeto al que tenemos acceso en nuestros controladores y vistas y por tanto podemos invocar los mismos métodos que estamos acostumbrados a utilizar. El método tiene que devolver un valor booleano cuyo valor dependa de si la ruta dada casa con la petición. En nuestro caso queremos que el método devuelva true
si la petición tiene un subdominio siempre que dicho subdominio no sea www
. Nuestra clase, por tanto, tendrá este aspecto:
class Subdomain def self.matches?(request) request.subdomain.present? && request.subdomain != 'www' end end
Si visitamos http://www.lvh.me:3000 veremos la portada tal y como queríamos.
Para arreglar los enlaces
A continuación vamos a corregir los enlaces a cada blog que aparecen en la portada para que apunten al subdominio correspondiente en lugar de a la acción show
de dicho blog (lo que dará un error porque dicha acción ahora espera recibir un subdominio)
En el código de la vista de la acción index
tenemos un enlace estándar a cada blog rodeado por etiquetas h2
.
<% title "Blogs" %> <% for blog in @blogs %> <div> <h2><%= link_to blog.name, blog %></h2> <div class="actions"> <%= link_to "Edit", edit_blog_path(blog) %> | <%= link_to "Destroy", blog, :confirm => 'Are you sure?', :method => :delete %> </div> <% end %>
Queremos cambiar dicho enlace para que apunte a la URL raíz con el subdominio apropiado. Por desgracia a root_url
no le podemos pasar la opción subdomain
como sí que podríamos hacer con subdomain_fu. En su lugar tendremos que crear el nombre de host partiendo de cero e incluyendo el subdominio. Esto lo haremos a partir del atributo subdomain
del blog y el dominio y puerto de la petición actual.
<h2><%= link_to blog.name, root_url(:host => blog.subdomain + '.' + request.domain + request.port_string) %></h2>
Si ahora recargamos la portada veremos que los enlaces de los blogs ahora muestran correctamente el subdominio.
Limpieza del código que cambia el subdominio
Todo funciona correctamente ahora pero el código que crea el enlace al otro subdominio podría ser un poco más claro, lo que nos interesará especialmente si vamos a tener que usarlo mucho. Lo haremos moviéndolo a su propio helper, que llamaremos with_subdomain
y que recibirá un argumento subdomain
. Primero cambiaremos el código en la vista para que invoque al método que estamos a punto de escribir.
<h2><%= link_to blog.name, root_url(:host => with_subdomain(blog.subdomain)) %></h2>
Pondremos el método helper en su propio módulo, en fichero llamado url_helper.rb
.
module UrlHelper def with_subdomain(subdomain) subdomain = (subdomain || "") subdomain += "." unless subdomain.empty? [subdomain, request.domain, request.port_string].join end end
El código del módulo primero asigna subdomain
a una cadena vacía si el valor pasado es nil
y le añade un punto en caso contrario. Finalmente une esta variable subdomain
, con el dominio y el puerto de la petición actual y devuelve dicho valor.
Vamos a añadir este módulo a ApplicationController
para que todos los controladores de nuestra aplicación puedan utilizarlo.
class ApplicationController < ActionController::Base include UrlHelper protect_from_forgery layout 'application' end
Aunque hemos limpiado el código de la vista, aún podríamos mejorarlo aún más si pudiésemos añadir la opción :subdomain
al método root_url
, y esto sólo es posible hacerlo si reescribimos el método url_for
. Tan sólo tenemos que añadir el siguiente método a nuestro módulo UrlHelper
.
def url_for(options = nil) if options.kind_of?(Hash) && options.has_key?(:subdomain) options[:host] = with_subdomain(options.delete(:subdomain)) end super end
Nuestra versión redefinida de url_for
comprueba si en el hash de opciones aparece una clave llamada :subdomain
y, de ser así, pone la opción :host
al valor que devuelva la función with_subdomain
para dicho subdominio. Finalmente, llama a super
para que se ejecute el código por defecto del método y se genere correctamente el resto de la URL. No nos hace falta pasar por alias_method_chain
, bastará con llamar a super
.
Ya podemos actualizar el código de nuestra vista para que utilice la opción :subdomain
.
<h2><%= link_to blog.name, root_url(:subdomain => blog.subdomain) %></h2>
Si recargamos la portada y hacemos clic en uno de los enlaces de los blogs, el enlace sigue dirigiendo a la URL correcta, con su subdominio.
Sería conveniente poder tener en cada blog un enlace de vuelta a la página principal, podemos hacerlo llamando a root_url
con :subdomain
a false
.
<p><%= link_to "All Blogs", root_url(:subdomain => false) %></p>
Con esto tendremos el enlace que nos llevará de vuelta a la portada.
Cómo gestionar diferentes dominios de nivel superior
Una cosa que aún no hemos visto es cómo gestionar nombres de dominio que tengan más de dos partes. Así, si bien nuestro código de subdominios funcionará para dominios .com, no lo hará con dominios que terminen en .co.uk. Para poder trabajar con estos dominios tendremos que cambiar nuestra aplicación y donde quiera que se llame a
request.domain
o request.subdomain
tendremos que especificar el número de puntos que contiene el dominio (sin subdominios). Rails por defecto asume el valor 1, pero tendremos que pasar el valor 2 para dominios como .co.uk.
Tenemos que hacer cambios en dos sitios de nuestra aplicación: en el método UrlHelper
y en la clase Subdomain
que estamos usando en nuestras rutas.
def with_subdomain(subdomain) subdomain = (subdomain || "") subdomain += "." unless subdomain.empty? [subdomain, request.domain(2), request.port_string].join end
class Subdomain def self.matches?(request) request.subdomain(2).present? && request.subdomain(2) != "www" end end
Obviamente no nos interesa que el valor esté grabado a fuego, tendremos que hacerlo dinámico de forma que en desarrollo pueda valer 1 (para dominios como lvh.me
) mientras que en producción utilizaríamos el valor 2. Este valor se puede poner en un archivo externo de configuración que sería cargado por el entorno.
Cookies
Hay un aspecto último que nos hemos dejado para el final de este episodio. Si miramos las cookies de nuestro navegador y filtramos por el nombre de dominio que hemos venido usando para nuestra aplicación veremos que se guarda una cookie de sesión diferente para cada subdominio que hemos visitado. No queremos que esto ocurra, porque esto significa que los subdominios no comparten la sesión.
En el episodio 123 se presentó una solución a este problema pero existe una forma mejor de hacerlo en Rails 3. Sólo tenemos que añadir la opción :domain
en el fichero /config/initializers/session_store.rb
de nuestra aplicación en el método Rails.application.config.session_store
dándole el valor :all
.
Rails.application.config.session_store :cookie_store, :key => '_bloggit_session', :domain => :all
De todas formas, esto todavía no funcionará. La opción :domain
ha sido añadida posteriormente a Rails 3.0 beta 4, que es la última versión de Rails a fecha de escritura de este artículo. Tendremos que pasar a Edge Rails o bien esperar a la siguiente versión candidata para que tener esta funcionalidad. Una vez que tengamos una versión de Rails que soporte esta opción nuestra aplicación podrá compartir sesiones a lo largo de múltiples subodminos. La opción :all
asume que nuestra aplicación tiene sólo un punto en su dominio de nivel superior. De no ser así podemos especificar un nombre de dominio que será utilizado como base para la sesión.
Con esto cerramos este episodio. La gestión de subdominios sin tener que usar plugins adicionales es una gran mejora a Rails que seguramente tendrá muchos usos en nuestras aplicaciones.