Как правильно канонизировать URL в приложении ASP.NET MVC? - PullRequest
10 голосов
/ 26 сентября 2010

Я пытаюсь найти хороший универсальный способ канонизации URL-адресов в приложении ASP.NET MVC 2. Вот что я придумала до сих пор:

// 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);
    }
}

Теперь я могу разметить свои контроллеры следующим образом:

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

Кажется, все это работает довольно хорошо, но у меня есть следующие проблемы:

  1. Мне все еще нужно добавить CanonicalizeAttribute к каждому контроллеру (или методу действия), который я хочу канонизировать, когда трудно представить себе ситуацию, когда я не хочу такого поведения. Похоже, должен быть способ получить такое поведение по всему сайту, а не один контроллер за раз.

  2. Тот факт, что я применяю правило «принуждать к нижнему регистру» в фильтре, кажется неправильным. Конечно, было бы лучше как-то включить это в логику URL-адреса маршрута, но я не могу придумать способ сделать это в моей конфигурации маршрутизации. Я думал о добавлении @"[a-z]*" ограничений к контроллеру и параметрам действия (а также любым другим строковым параметрам маршрута), но я думаю, что это приведет к тому, что маршруты не будут совпадать. Кроме того, поскольку правило нижнего регистра не применяется на уровне маршрута, на моих страницах можно создавать ссылки с заглавными буквами, что выглядит довольно плохо.

Есть ли что-то очевидное, что я здесь пропускаю?

Ответы [ 3 ]

19 голосов
/ 05 октября 2011

Я чувствовал тот же "зуд" в отношении расслабленной природы маршрутизации ASP.NET MVC по умолчанию, игнорирования буквенных символов, конечных слешей и т. Д. Как и вы, я хотел общее решение проблемы, желательно как часть логика маршрутизации в моих приложениях.

Пройдя поиск в Интернете, не найдя полезных библиотек, я решил свернуть один сам. В результате получается Canonicalize , библиотека классов с открытым исходным кодом, дополняющая механизм маршрутизации ASP.NET.

Вы можете установить библиотеку через NuGet: Install-Package Canonicalize

А при регистрации вашего маршрута: routes.Canonicalize().Lowercase();

Помимо строчных букв, в пакет включены несколько других стратегий канонизации URL. Включение или отключение префикса домена www, принудительное включение определенного имени хоста, конечной косой черты и т. Д. Также очень легко добавлять собственные стратегии канонизации URL-адресов, и я очень открыт для принятия патчей, добавляющих больше стратегий к «официальному». Canonicalize дистрибутив.

Я надеюсь, что вы или кто-то еще найдете это полезным, даже если вопросу год:)

8 голосов
/ 30 сентября 2014

MVC 5 и 6 имеет возможность создания строчных URL для ваших маршрутов.Моя конфигурация маршрута показана ниже:

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 });
    }
}

С этим кодом вам больше не нужно канонизировать URL-адреса, как это сделано для вас.Одна проблема, которая может возникнуть, если вы используете HTTP и HTTPS URL и хотите канонический URL для этого.В этом случае довольно легко использовать вышеуказанные подходы и заменить HTTP на HTTPS или наоборот.

Другая проблема заключается в том, что внешние сайты, которые ссылаются на ваш сайт, могут пропустить косую черту или добавить символы в верхнем регистре, и для этого вы должны выполнить перенаправление 301 на правильный URL с косой чертой.Полное использование и исходный код см. В моем блоге и фильтре RedirectToCanonicalUrlAttribute:

/// <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
}

Пример использования, обеспечивающий перенаправление всех запросов 301 на правильный канонический URL:

filters.Add(new RedirectToCanonicalUrlAttribute(
    RouteTable.Routes.AppendTrailingSlash, 
    RouteTable.Routes.LowercaseUrls));
5 голосов
/ 16 ноября 2010

Ниже показано, как я делаю свои канонические URL-адреса в MVC2. Я использую модуль перезаписи IIS7 v2, чтобы сделать все мои URL-адреса строчными, а также удаляю конечные слэши, поэтому мне не нужно делать это из моего кода. ( Полная запись в блоге )

Добавьте это к главной странице в разделе заголовка следующим образом:

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

Создание атрибута фильтра (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);
    }
}

Вызовите это из своих действий:

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

Для некоторых других интересных статей о Поисковой системе Связанные должности проверьте блог Мэтта Каттса

...