ASP.NET MVC路由上的尾部斜杠

时间:2008-09-30 18:19:34

标签: asp.net-mvc asp.net-mvc-routing

在最新的MVC预览中,我正在使用此路由获取旧网址:

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,我希望不必用斜杠对版本进行硬编码。

任何人都知道如何强制路线使用尾部斜线?

7 个答案:

答案 0 :(得分:3)

如果你有一个基于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生成了完整的链接。

答案 1 :(得分:3)

我发生在这篇博客文章中:

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

今天早上在StackOverflow上遇到这个问题之前。该博客文章(来自该问题的作者)对Scott Hanselman撰写的这篇博客文章进行了回溯,并回答了这个问题:

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

我很惊讶地发现从这里到那里没有链接,所以我只是添加了它。 :)

Scott的回答建议使用URL重写。

答案 2 :(得分:2)

编写链接时,应始终包含最终斜杠。我不知道这是否适用于mvc框架(或一般的URL路由),但我知道对于静态资源,如果你没有把斜杠放在你的位置,那么在请求完成两次时会增加一点点开销。

斜杠会立即将url标识为指向目录。无需解析文件。

同样,我不相信这在您使用URL路由时适用,但我没有调查过。

检查HERE for an article about the trailing slash

编辑: 考虑到这一点......我认为最好不要削减斜线,而不是试图包括它。当您使用URL路由时,您正在使用URL直接路由到资源。与指向具有index.html或default.aspx的目录相反,您指向的是特定文件。

我知道差异是微妙的,但坚持使用Routed Urls的非斜杠可能会更好,而不是与框架斗争。

当您实际指向目录时,请严格使用尾部斜杠。我想如果你真的不喜欢的话,我猜你每次都可以加上一个斜线。

答案 3 :(得分:1)

这里是RouteLinkEx的重载(HtmlHelper,string,string,object)

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

答案 4 :(得分:1)

这是我的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));
    }

答案 5 :(得分:1)

我认为你是从错误的角度解决问题。想要强制单个URL的原因是SEO。我认为这是指获得重复的内容惩罚,因为搜索引擎认为这两个网址具有相同的内容。

这个问题的另一个解决方案是在你的页面上添加一个CANONICAL标签,告诉搜索引擎哪个是&#34;官方&#34;页面的网址。一旦你这样做,你不再需要强制URL和搜索引擎不会惩罚你,并将搜索结果路由到你的官方网址。

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

答案 6 :(得分:0)

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,则可能会出现一个问题。在这种情况下,使用上述方法并使用HTTPS替换HTTP非常容易,反之亦然。

另一个问题是链接到您网站的外部网站可能会忽略尾部斜杠或添加大写字符,为此您应该使用尾部斜杠执行301永久重定向到正确的URL。有关完整用法和源代码,请参阅我的blog postRedirectToCanonicalUrlAttribute过滤器:

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