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

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

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

基本用法

於 Startup.cs 的 Configure() 方法裡面,在我們想要的位置插入 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;
    }
}

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

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

參考資料

相關資源

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