Принудительная косая черта на пути ASP.NET MVC

В последнем предварительном просмотре MVC я использую этот маршрут для устаревшего URL:

routes.MapRoute(
"Legacy-Firefox", // Route name
"Firefox-Extension/", // URL with parameters
new { controller = "Home", action = "Firefox", id = "" } // Parameter defaults
);

Проблема заключается в том, что оба этих URL работают: http://example.com/Firefox-Extension http://example.com/Firefox-Extension/

Я хочу, чтобы второй работал (для SEO). Кроме того, когда я создаю ссылку на эту страницу, механизм маршрутизации возвращает мне URL-адрес без конечной косой черты.

Это код, который я использую для создания ссылки:

<%= Html.ActionLink("Firefox Extension", "Firefox", "Home")%>

Я считаю, что можно исправить первую проблему, используя обработчик HTTP, чтобы переадресовать 301 URL-адрес с завершающим косой чертой. Тем не менее, я хочу связать URL с конечной косой чертой, и я надеюсь, что вам не придется жестко программировать версию с косой чертой.

Кто-нибудь знает, как заставить маршрут использовать конечную косую черту?

Ответы

Ответ 1

Если у вас есть оболочка над RouteLink, есть простое решение проблемы. Например, у меня был метод-оболочка RouteLinkEx:

public static string RouteLinkEx(this HtmlHelper helper,string text,string routeName,RouteValueDictionary rvd,object htmlAttributes)
      {

      UrlHelper uh = new UrlHelper(helper.ViewContext.RequestContext,helper.RouteCollection);
      // Add trailing slash to the url of the link
      string url = uh.RouteUrl(routeName,rvd) + "/";
      TagBuilder builder = new TagBuilder("a")
      {
        InnerHtml = !string.IsNullOrEmpty(text) ? HttpUtility.HtmlEncode(text) : string.Empty
      };
      builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
      builder.MergeAttribute("href",url);
      return builder.ToString(TagRenderMode.Normal);
      //---  
      }

Как вы видите, я сначала использовал параметры для генерации URL. Затем я добавил "/" в конце URL-адреса. а затем я создал полную ссылку, используя эти URL.

Ответ 2

Я попал по этому сообщению в блоге:

http://www.ytechie.com/2008/10/aspnet-mvc-what-about-seo.html

сегодня утром перед тем, как начать этот вопрос в StackOverflow. В этом сообщении блога (от автора этого вопроса) есть ответ на этот пост в блоге от Скотта Гензельмана с ответом на этот вопрос:

http://www.hanselman.com/blog/ASPNETMVCAndTheNewIIS7RewriteModule.aspx

Я с удивлением обнаружил, что здесь нет ссылки, поэтому я просто добавил ее.:)

Ответ Скотта предполагает использование перезаписи URL.

Ответ 3

Когда вы пишете свои ссылки, вы всегда должны включать финальную косую черту. Я не знаю, относится ли это к инфраструктуре mvc (или URL-маршрутизации в целом), но я знаю, что для статических ресурсов, если вы не поместите косую черту, добавьте небольшие накладные расходы, когда запрос будет выполнен дважды.

Слэш немедленно идентифицирует URL как указывающий на каталог. Нет необходимости разбирать файлы.

Опять же, я не считаю, что это применимо, когда вы используете маршрутизацию URL-адресов, но я не рассматривал его.

Отметьте ЗДЕСЬ для статьи о завершающей косой чертой

изменить: Подумав об этом... Я думаю, что, вероятно, лучше отказаться от косой черты, а не пытаться включить ее. Когда вы используете маршрутизацию URL-адресов, вы используете URL-адрес для прямого перехода на ресурс. В отличие от указания каталога с индексом index.html или default.aspx, вы указываете на определенный файл.

Я знаю, что разница тонкая, но лучше придерживаться не-косая черта для Routed Urls, а не бороться с каркасом.

Используйте конечную косую черту, когда вы на самом деле указываете на каталог. Думаю, я думаю, вы могли бы просто добавить косую черту до конца каждый раз, если вам это действительно не понравилось.

Ответ 4

Здесь перегрузка для RouteLinkEx (HtmlHelper, строка, строка, объект)

        public static string RouteLinkEx(this HtmlHelper helper, string text, string routeName, object routeValues)
    {

        UrlHelper uh = new UrlHelper(helper.ViewContext.RequestContext);

        // Add trailing slash to the url of the link 
        string url = uh.RouteUrl(routeName, routeValues) + "/";
        TagBuilder builder = new TagBuilder("a")
        {
            InnerHtml = !string.IsNullOrEmpty(text) ? HttpUtility.HtmlEncode(text) : string.Empty
        };
        //builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
        builder.MergeAttribute("href", url);
        return builder.ToString(TagRenderMode.Normal);
        //---   
    }

Ответ 5

Вот моя версия для ASP.NET MVC 2

    public static MvcHtmlString RouteLinkEx(this HtmlHelper helper, string text, RouteValueDictionary routeValues)
    {
        return RouteLinkEx(helper, text, null, routeValues, null);
    }

    public static MvcHtmlString RouteLinkEx(this HtmlHelper htmlHelper, string text, string routeName, RouteValueDictionary routeValues, object htmlAttributes)
    {
        string url = UrlHelper.GenerateUrl(routeName, null, null, null, null, null, routeValues, htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, false);

        var builder = new TagBuilder("a")
        {
            InnerHtml = !string.IsNullOrEmpty(text) ? HttpUtility.HtmlEncode(text) : string.Empty
        };
        builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
        // Add trailing slash to the url of the link
        builder.MergeAttribute("href", url + "/");
        return MvcHtmlString.Create(builder.ToString(TagRenderMode.Normal));
    }

Ответ 6

Я думаю, что вы решаете проблему с неправильного угла. Причина, по которой вы хотите заставить единственный URL-адрес для SEO. Я считаю, что это относится к получению дублирования контента, поскольку поисковые системы рассматривают эти два URL-адреса с одним и тем же контентом.

Другим решением этой проблемы является добавление тега CANONICAL на вашу страницу, который сообщает поисковым системам, которые являются "официальным" URL-адресом для страницы. После того, как вы это сделаете, вам больше не нужно будет заставлять URL-адреса и поисковые системы не наказывать вас и направлять результаты поиска на ваш официальный URL.

https://support.google.com/webmasters/answer/139066?hl=en

Ответ 7

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

public static class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        // Imprive SEO by stopping duplicate URL 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-адрес, как это сделано для вас. Одна из проблем, которая может возникнуть, если вы используете URL-адрес HTTP и HTTPS и хотите использовать для этого канонический 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 with/without trailing slashes are treated as different URL by search engines. This 
/// filter redirects all non-canonical URL based on the settings specified to their canonical equivalent. 
/// Note: Non-canonical URL 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 comments at http://googlewebmastercentral.blogspot.co.uk/2010/04/to-slash-or-not-to-slash.html
/// and Bing 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 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));