2010-09-26 16 views
8

Estoy tratando de encontrar una buena forma general para canonicalizar urls en una aplicación ASP.NET MVC 2. Esto es lo que he encontrado hasta el momento:¿Cómo se puede canonicalizar correctamente una URL en una aplicación ASP.NET MVC?

// Using an authorization filter because it is executed earlier than other filters 
public class CanonicalizeAttribute : AuthorizeAttribute 
{ 
    public bool ForceLowerCase { get;set; } 

    public CanonicalizeAttribute() 
     : base() 
    { 
     ForceLowerCase = true; 
    } 

    public override void OnAuthorization(AuthorizationContext filterContext) 
    { 
     RouteValueDictionary values = ExtractRouteValues(filterContext); 
     string canonicalUrl = new UrlHelper(filterContext.RequestContext).RouteUrl(values); 
     if (ForceLowerCase) 
      canonicalUrl = canonicalUrl.ToLower(); 

     if (filterContext.HttpContext.Request.Url.PathAndQuery != canonicalUrl) 
      filterContext.Result = new PermanentRedirectResult(canonicalUrl); 
    } 

    private static RouteValueDictionary ExtractRouteValues(AuthorizationContext filterContext) 
    { 
     var values = filterContext.RouteData.Values.Union(filterContext.RouteData.DataTokens).ToDictionary(x => x.Key, x => x.Value); 
     var queryString = filterContext.HttpContext.Request.QueryString; 
     foreach (string key in queryString.Keys) 
     { 
      if (!values.ContainsKey(key)) 
       values.Add(key, queryString[key]); 
     } 
     return new RouteValueDictionary(values); 
    } 
} 

// Redirect result that uses permanent (301) redirect 
public class PermanentRedirectResult : RedirectResult 
{ 
    public PermanentRedirectResult(string url) : base(url) { } 

    public override void ExecuteResult(ControllerContext context) 
    { 
     context.HttpContext.Response.RedirectPermanent(this.Url); 
    } 
} 

Ahora puede marcar mis controladores de la siguiente manera:

[Canonicalize] 
public class HomeController : Controller { /* ... */ } 

Todo esto parece funcionar bastante bien, pero tengo los siguientes problemas:

  1. todavía tengo que añadir el CanonicalizeAttribute en cada controlador (método o acción) Quiero canónica, cuando es difícil pensar en una situación en la que no querrá este comportamiento. Parece que debería haber una forma de obtener este comportamiento en todo el sitio, en lugar de un controlador a la vez.

  2. El hecho de que estoy implementando la regla 'forzar a minúsculas' en el filtro parece incorrecto. Seguramente sería mejor incluir esto de alguna manera en la lógica de la ruta url, pero no puedo pensar en una forma de hacerlo en mi configuración de enrutamiento. Pensé en agregar restricciones @"[a-z]*" al controlador y a los parámetros de acción (así como a cualquier otro parámetro de ruta de cadena), pero creo que esto hará que las rutas no coincidan. Además, como la regla de minúsculas no se aplica en el nivel de ruta, es posible generar enlaces en mis páginas que tengan letras mayúsculas, lo que parece bastante malo.

¿Hay algo obvio que estoy pasando por alto aquí?

+1

Hay una manera de conseguir en todo el sitio en MVC3 a través de filtros globales, pero en menos de MVC3, deberá crear un controlador base, aplicar el atributo a eso y derivar todos los controladores de él. Sin embargo, tengo que preguntar cuál es el caso de uso para esto. –

+1

SEO. Garantizar que las arañas que entran desde enlaces formateados incorrectamente se redireccionan (permanentemente) al correcto - la carcasa inferior es realmente un lado realmente. http://en.wikipedia.org/wiki/Canonicalization#Search_Engines_and_SEO –

Respuesta

18

he sentido lo mismo "picor" con respecto a la naturaleza relajada del defecto ASP.NET MVC enrutamiento, haciendo caso omiso de carcasa carta, barra final , etc. Como usted, quería una solución general al problema, preferiblemente como parte de la lógica de enrutamiento en mis aplicaciones.

Después de buscar en la web alto y bajo, al no encontrar bibliotecas útiles, decidí rodar una yo mismo. El resultado es Canonicalize, una biblioteca de clases de código abierto que complementa el motor de enrutamiento ASP.NET.

Puede instalar la biblioteca a través de NuGet: Install-Package Canonicalize

Y en su registro de rutas: routes.Canonicalize().Lowercase();

Además minúsculas, varias otras estrategias URL canónicos están incluidos en el paquete. Forzar www prefijo de dominio activado o desactivado, forzar un nombre de host específico, una barra diagonal, etc. También es muy fácil agregar estrategias de canonicalización de URL personalizadas, y estoy muy abierto a aceptar parches agregar más estrategias al canonización oficial Canonicalize distribución.

espero que usted u otra persona se encuentra este útil, incluso si la pregunta es de un año de edad :)

+1

Definitivamente lo verificará. –

+1

funciona bien para redirigir a un nombre de host que maches el certificado SSL - gracias – fiat

+1

Excelente biblioteca, justo lo que estaba buscando. Gracias. –

4

A continuación se muestra cómo realizo mis URL canónicas en MVC2. Utilizo el módulo v2 de reescritura de IIS7 para hacer que todas mis URL sean minúsculas y también para quitar barras diagonales, por lo que no es necesario que lo haga desde mi código. (Full blog post)

añadir esto a la página principal en la sección de cabecera de la siguiente manera:

<%=ViewData["CanonicalURL"] %> 
<!--Your other head info here--> 

crear un atributo de filtro (CanonicalURL.cs):

public class CanonicalURL : ActionFilterAttribute 
{ 
    public string Url { get; private set; } 

    public CanonicalURL(string url) 
    { 
     Url = url; 
    } 

    public override void OnResultExecuting(ResultExecutingContext filterContext) 
    { 
     string fullyQualifiedUrl = "http://www.example.com" + this.Url; 
     filterContext.Controller.ViewData["CanonicalUrl"] = @"<link rel='canonical' href='" + fullyQualifiedUrl + "' />"; 
     base.OnResultExecuting(filterContext); 
    } 
} 

Call esto de tus acciones:

[CanonicalURL("Contact-Us")] 
public ActionResult Index() 
{ 
     ContactFormViewModel contact = new ContactFormViewModel(); 
     return View(contact); 
} 

Para algunos otros artículos interesantes sobre motor de búsqueda Puestos relacionados con un vistazo al blog de Matt Cutts

+1

Supongo que hay un error aquí. Mire SO, si edita el 'título' en la URL a la que todavía redirecciona aquí. Con tu código supongo que representaría una URL incorrecta. SO representa lo mismo independientemente de la ruta – BrunoLM

+1

La URL se inyecta desde la acción. Su trabajo no es redirigir solo para indicar que esta es la URL principal de este recurso. – Andrew

+1

me gusta esta idea, mucho más tidyer que tratar de determinarlo automáticamente de la ruta o algún truco como ese. +1 AAA comentaría de nuevo –

7

MVC 5 y 6 tiene la opción de generar menor URL caso de para sus rutas. Mi config ruta se muestra a continuación:

public static class RouteConfig 
{ 
    public static void RegisterRoutes(RouteCollection routes) 
    { 
     // Imprive SEO by stopping duplicate URL's due to case or trailing slashes. 
     routes.AppendTrailingSlash = true; 
     routes.LowercaseUrls = true; 

     routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 

     routes.MapRoute(
      name: "Default", 
      url: "{controller}/{action}/{id}", 
      defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }); 
    } 
} 

Con este código, ya no debería necesitar el canonicalize la URL es como esto se hace para usted. Un problema que puede ocurrir si está utilizando URLs HTTP y HTTPS y quiere una URL canónica para esto. En este caso, es bastante fácil utilizar los enfoques anteriores y reemplazar HTTP con HTTPS o viceversa.

Otro problema es que los sitios web externos que enlazan con su sitio pueden omitir la barra inclinada o agregar caracteres en mayúscula y para esto debe realizar una redirección permanente 301 a la URL correcta con la barra al final. Para el uso completo y el código fuente, consulte mi blog post y el filtro RedirectToCanonicalUrlAttribute: ejemplo

/// <summary> 
/// To improve Search Engine Optimization SEO, there should only be a single URL for each resource. Case 
/// differences and/or URL's with/without trailing slashes are treated as different URL's by search engines. This 
/// filter redirects all non-canonical URL's based on the settings specified to their canonical equivalent. 
/// Note: Non-canonical URL's are not generated by this site template, it is usually external sites which are 
/// linking to your site but have changed the URL case or added/removed trailing slashes. 
/// (See Google's comments at http://googlewebmastercentral.blogspot.co.uk/2010/04/to-slash-or-not-to-slash.html 
/// and Bing's at http://blogs.bing.com/webmaster/2012/01/26/moving-content-think-301-not-relcanonical). 
/// </summary> 
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)] 
public class RedirectToCanonicalUrlAttribute : FilterAttribute, IAuthorizationFilter 
{ 
    private readonly bool appendTrailingSlash; 
    private readonly bool lowercaseUrls; 

    #region Constructors 

    /// <summary> 
    /// Initializes a new instance of the <see cref="RedirectToCanonicalUrlAttribute" /> class. 
    /// </summary> 
    /// <param name="appendTrailingSlash">If set to <c>true</c> append trailing slashes, otherwise strip trailing 
    /// slashes.</param> 
    /// <param name="lowercaseUrls">If set to <c>true</c> lower-case all URL's.</param> 
    public RedirectToCanonicalUrlAttribute(
     bool appendTrailingSlash, 
     bool lowercaseUrls) 
    { 
     this.appendTrailingSlash = appendTrailingSlash; 
     this.lowercaseUrls = lowercaseUrls; 
    } 

    #endregion 

    #region Public Methods 

    /// <summary> 
    /// Determines whether the HTTP request contains a non-canonical URL using <see cref="TryGetCanonicalUrl"/>, 
    /// if it doesn't calls the <see cref="HandleNonCanonicalRequest"/> method. 
    /// </summary> 
    /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
    /// <see cref="RedirectToCanonicalUrlAttribute"/> attribute.</param> 
    /// <exception cref="ArgumentNullException">The <paramref name="filterContext"/> parameter is <c>null</c>.</exception> 
    public virtual void OnAuthorization(AuthorizationContext filterContext) 
    { 
     if (filterContext == null) 
     { 
      throw new ArgumentNullException("filterContext"); 
     } 

     if (string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.Ordinal)) 
     { 
      string canonicalUrl; 
      if (!this.TryGetCanonicalUrl(filterContext, out canonicalUrl)) 
      { 
       this.HandleNonCanonicalRequest(filterContext, canonicalUrl); 
      } 
     } 
    } 

    #endregion 

    #region Protected Methods 

    /// <summary> 
    /// Determines whether the specified URl is canonical and if it is not, outputs the canonical URL. 
    /// </summary> 
    /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
    /// <see cref="RedirectToCanonicalUrlAttribute" /> attribute.</param> 
    /// <param name="canonicalUrl">The canonical URL.</param> 
    /// <returns><c>true</c> if the URL is canonical, otherwise <c>false</c>.</returns> 
    protected virtual bool TryGetCanonicalUrl(AuthorizationContext filterContext, out string canonicalUrl) 
    { 
     bool isCanonical = true; 

     canonicalUrl = filterContext.HttpContext.Request.Url.ToString(); 
     int queryIndex = canonicalUrl.IndexOf(QueryCharacter); 

     if (queryIndex == -1) 
     { 
      bool hasTrailingSlash = canonicalUrl[canonicalUrl.Length - 1] == SlashCharacter; 

      if (this.appendTrailingSlash) 
      { 
       // Append a trailing slash to the end of the URL. 
       if (!hasTrailingSlash) 
       { 
        canonicalUrl += SlashCharacter; 
        isCanonical = false; 
       } 
      } 
      else 
      { 
       // Trim a trailing slash from the end of the URL. 
       if (hasTrailingSlash) 
       { 
        canonicalUrl = canonicalUrl.TrimEnd(SlashCharacter); 
        isCanonical = false; 
       } 
      } 
     } 
     else 
     { 
      bool hasTrailingSlash = canonicalUrl[queryIndex - 1] == SlashCharacter; 

      if (this.appendTrailingSlash) 
      { 
       // Append a trailing slash to the end of the URL but before the query string. 
       if (!hasTrailingSlash) 
       { 
        canonicalUrl = canonicalUrl.Insert(queryIndex, SlashCharacter.ToString()); 
        isCanonical = false; 
       } 
      } 
      else 
      { 
       // Trim a trailing slash to the end of the URL but before the query string. 
       if (hasTrailingSlash) 
       { 
        canonicalUrl = canonicalUrl.Remove(queryIndex - 1, 1); 
        isCanonical = false; 
       } 
      } 
     } 

     if (this.lowercaseUrls) 
     { 
      foreach (char character in canonicalUrl) 
      { 
       if (char.IsUpper(character)) 
       { 
        canonicalUrl = canonicalUrl.ToLower(); 
        isCanonical = false; 
        break; 
       } 
      } 
     } 

     return isCanonical; 
    } 

    /// <summary> 
    /// Handles HTTP requests for URL's that are not canonical. Performs a 301 Permanent Redirect to the canonical URL. 
    /// </summary> 
    /// <param name="filterContext">An object that encapsulates information that is required in order to use the 
    /// <see cref="RedirectToCanonicalUrlAttribute" /> attribute.</param> 
    /// <param name="canonicalUrl">The canonical URL.</param> 
    protected virtual void HandleNonCanonicalRequest(AuthorizationContext filterContext, string canonicalUrl) 
    { 
     filterContext.Result = new RedirectResult(canonicalUrl, true); 
    } 

    #endregion 
} 

uso para asegurar que todas las solicitudes son 301 redirigido a la correcta URL canónica:

filters.Add(new RedirectToCanonicalUrlAttribute(
    RouteTable.Routes.AppendTrailingSlash, 
    RouteTable.Routes.LowercaseUrls)); 
+1

¿Por qué usar un filtro de autorización cuando debería usar un filtro de acción? Bacon no es gammon por una razón. –

+0

'' 'IAuthorizationFilter''' es la elección correcta. Ver [esto] (https://stackoverflow.com/questions/19249511/difference-between-iactionfilter-and-iauthorizationfilter) answer. El filtro se ejecuta antes en la canalización de solicitud. Idealmente, si vamos a redirigir a un usuario, debemos hacerlo lo antes posible. –

+0

Eso no dice nada acerca de cuándo debe usar filtros de autorización en lugar de filtros de acción. Creo que en esta ocasión deberíamos estar de acuerdo en estar en desacuerdo. –

Cuestiones relacionadas