[小菜一碟] ASP.NET Core 解決 SEO 要求網址全小寫及無斜線結尾的問題

之前有寫過一篇在 IIS 用 URL Rewrite 解決 SEO 要求網址全小寫及有無斜線結尾的問題,到了 ASP.NET Core 雖然沒有 URL Rewrite 擴充套件可以安裝,但是有一個 URL Rewriting Middleware 可以來幫助我們做到一樣的事情。

原本這件事情我是想要在 Nginx 直接用設定的方式解決,但是找到的解法都是需要簽出 Nginx 的原始碼,透過新增插件、調整編譯參數的方式重新編譯 Nginx,這就讓事情變得複雜了,所以轉而使用 URL Rewriting Middleware。

基本用法

Startup.csConfigure() 方法裡面,在我們想要的位置插入 app.UseRewriter(...) 程式碼,底下是一個強制將 http 重新導向到 https 的規則。

上面這個只是其中一個已經內建的規則,最重要的是我們可以寫正則表達式來建立我們想要的規則,底下是一個將 home/*.aspx 網址,做 301 轉址到 home/* 的規則。

這邊要注意的是,比對規則是不算 / 根路徑符號的,所以我們如果這樣寫 AddRedirect("/home/(.*)\\.aspx", "/home/${1}", 301) 是不會匹配到的,如果有需要強調是 home 開頭,則改寫為 AddRedirect("^home/(.*)\\.aspx", "home/${1}", 301) 除非匹配邏輯是我們自己寫的,就另當別論了。

美中不足

正當以為掌握了寫 URL Rewriting 規則的方法之後,將網址大寫轉小寫只是蛋糕一塊的時候,發現它並沒有那麼簡單,原因在於 AddRedirect() 擴充方法是使用 Match.Result() 這個方法來替換匹配字元,而 replacement 的語法裡面沒有支援將匹配字元轉成大寫或小寫的語法,只好依樣畫葫蘆,參考 RedirectRule.cs 原始碼新增一個 RedirectLowercaseRule

internal sealed class RedirectLowercaseRule : IRule
{
    public void ApplyRule(RewriteContext context)
    {
        var path = context.HttpContext.Request.Path == PathString.Empty
                       ? context.HttpContext.Request.Path.ToString()
                       : context.HttpContext.Request.Path.ToString().Substring(1);

        if (!Uri.UnescapeDataString(path).Any(char.IsUpper)) return;

        var pathBase = context.HttpContext.Request.PathBase;

        // 強制大寫轉小寫
        var newPath = Regex.Replace(path, "[A-Z]", m => m.Value.ToLower());

        var response = context.HttpContext.Response;

        // 永久轉址
        response.StatusCode = 301;

        context.Result = RuleResult.EndResponse;

        if (string.IsNullOrEmpty(newPath))
        {
            response.Headers[HeaderNames.Location] = pathBase.HasValue ? pathBase.Value : "/";
            return;
        }

        if (newPath.IndexOf("://", StringComparison.Ordinal) == -1 && newPath[0] != '/')
        {
            newPath = string.Concat("/", newPath);
        }

        var split = newPath.IndexOf('?');
        if (split >= 0)
        {
            var query = context.HttpContext.Request.QueryString.Add(QueryString.FromUriComponent(newPath.Substring(split)));

            response.Headers[HeaderNames.Location] = string.Concat(pathBase, newPath.Substring(0, split), query.ToUriComponent());
        }
        else
        {
            response.Headers[HeaderNames.Location] = string.Concat(pathBase, newPath, context.HttpContext.Request.QueryString.ToUriComponent());
        }
    }
}

接著,為了方便操作,新增一個 RewriteOptions 的擴充方法 AddRedirectToLowercasePermanent()

public static class RewriteOptionsExtension
{
    public static RewriteOptions AddRedirectToLowercasePermanent(this RewriteOptions options)
    {
        options.Rules.Add(new RedirectLowercaseRule());

        return options;
    }
}

最後,加上網址無斜線結尾的規則,就大功告成了。

RewriteRedirect 的差別在於,Rewrite 是伺服器內部的網址轉換行為,網址經過轉換之後,實際上 Route 到的是另一個服務,而 Client 端所看到的網址不會變,但是呈現的會是另一個服務的內容;Redirect 是 Client 端的轉址行為,Client 的網址會變,呈現的會是另一個網址的內容。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學