Blazorの簡単なOGP、SEO対策

この内容は [Blazor Advent Calendar 2020](https://qiita.com/advent-calendar/2020/blazor)の20日目の記事です

#BlazorとOGP
BlazorはSPAフレームワークです。SPAは通常OGP対策が必要です。OGPとは簡単に言えばFacebookやTwitterにURLを貼った場合にサムネイルの画像と簡単な説明を出してくれるあの機能です。Blazorで作った場合、index.htmlにOGPの設定をしてページ毎にjsでその設定を書き換えても、OGPクローラーはjsの書き換えを解釈してくれないので、結局全てのページがindex.htmlの情報として扱われてしまいます。

#BlazorとSEO
SPAでもう一つ問題になるのがSEOです。SEOは簡単に言えばGoogleの検索結果を上位にするための技術です。実はSEOに関しては、jsでtitleを書き換えてさえいれば、Googleクローラは対応してくれるようです。自分のサイトもtitleなどの書き換えだけしてそれ以外特に何もしていないですが、ちゃんとtitleなどがページ毎に書き換わって[検索にヒット](https://www.google.com/search?q=site%3Amanga-yonda.com&oq=site%3Am&aqs=chrome.1.69i57j69i59j69i58.3805j0j7&sourceid=chrome&ie=UTF-8)しました。有難うGoogle。残念、Facebook、Twitter他。

```html
<head>
    <title>マンガ読んだ！！ - 自分が読んだ漫画のログを残すサービス</title>
```
```js
function setTitle(title) {
    document.title = title; //jsでtitleを書き換えればgoogleクローラーはちゃんとそれを見てくれた
}
```

#BlazorのOGP対策が見つからない
さて、BlazorのOGP対策についてピンポイントでやり方が書いてあるサイトは見つかりませんでした。自分でもどうしたものかなと持っている時に丁度Qiitaでバズっている記事を見ました。[SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d)です。Vue.jsの記事ですが、これを元に考えていけば何とかなりそうな気がしました。

(20/12/23追記)@jsakamoto さんより情報頂きました！ちゃんと日本語で情報ありました。有難う御座います！
>[Blazor で、タイトルや OGP 用メタ要素を設定する](https://qiita.com/jsakamoto/items/a68a62c5e0c13a827da0)
[クライアントサイド Blazor でも、SEO 対策のためにサーバーサイドレンダリングしたい](https://qiita.com/jsakamoto/items/25dcd56be8a573bc6827)

#OGPを動的に生成したいパスのみ、Hostingの設定でFunctionsを呼び出す
先ずポイントとして[OGPを動的に生成したいパスのみ、Hostingの設定でFunctionsを呼び出す](https://qiita.com/yuneco/items/5e526464939082862f5d#ogp%E3%82%92%E5%8B%95%E7%9A%84%E3%81%AB%E7%94%9F%E6%88%90%E3%81%97%E3%81%9F%E3%81%84%E3%83%91%E3%82%B9%E3%81%AE%E3%81%BFhosting%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%A7functions%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99)を参考に考えました。特に図はかなり何度も見ました。書かれている通りのアプローチもあると思いますが、突き詰めるとサーバサイドでjsによるリダイレクトを使って、人間とクローラーで処理を分けさえすれば他の方法でも行けそうと思いました。

#Blazorでサーバとクライアントで同名のルーティングを書いてみる
Blazorは、テンプレート通りに作ればclientとserverが分かれます。サーバ側はページルーティングはなく、クライアントでページルーティングします。テンプレートにないだけでサーバでもページルーティングは書けるので、それを使うことにしました。試しにサーバとクライアントで同名のルーティングを書くと、ブラウザで直接URLを書いた場合はサーバに行きました。但し、クライアント内で移動した場合はサーバに同名があってもサーバにいかずクライアントのままでルーティングされました。

#Vueのrouterで"元URL+α"を元URLに書き戻す
別の言い方をするとサーバもクライアントも同名の状態ではサーバからリダイレクトしてクライアントには行けません。サーバから別名のクライアントへのリダイレクトは当然クライアントに行けます。そこでまた[Vueのrouterで"元URL+α"を元URLに書き戻す](https://qiita.com/yuneco/items/5e526464939082862f5d#vue%E3%81%AErouter%E3%81%A7%E5%85%83url%CE%B1%E3%82%92%E5%85%83url%E3%81%AB%E6%9B%B8%E3%81%8D%E6%88%BB%E3%81%99)を参考にしました。クライアントに別名でリダイレクトして、さらにクライアントで同名に再リダイレクトすれば目的が達成できそうです。

#自分のサイトでのリダイレクトの動き
自分のサイトで説明すると、ブラウザで[http://manga-yonda.com/manga](http://manga-yonda.com/manga)にアクセスされた場合、まずサーバサイドに飛びます。そこですぐにjsで[http://manga-yonda.com/_manga](http://manga-yonda.com/_manga)(mangaではなく、**_manga**)にリダイレクトします。するとこれはクライアントに行きます。クライアントの最初の処理でURLにアンダーバーが入っているかをチェックします。**_manga**なので、チェックにかかり、ここで_を消したURLにリダイレクトします。クライアントのリダイレクトなので、晴れてクライアントの[http://manga-yonda.com/manga](http://manga-yonda.com/manga)に行きます。

#サーバの具体的なコード
つまり↑に書いた状態でサーバのページにOGPの設定をしれやれば、jsを解釈できる人間の操作の場合はクライアントに飛び、OGPクローラーの場合はjsを解釈出来ないのでサーバを見ます。具体的にはServerのコントローラーに以下のようなメソッドを用意します。

```cs:OgpController.cs
private ViewResult GetView(string title, string image, string url, string description)
{
    ViewData["OgTitle"] = title + "|マンガ読んだ！！";
    ViewData["OgImage"] = image;
    ViewData["OgUrl"] = $"https://{Request.Host.Value}/{url}";
    ViewData["OgDescription"] = description;
    ViewData["ReturnUrl"] = Request.Path.Value.Substring(1) + Request.QueryString.Value;
    return View();
}
```
次にルーティングを書きます。

```cs:OgpController.cs
[HttpGet("manga")]
public IActionResult Index() => GetView("マンガ一覧", $"./content/manga.jpg", "manga", $"マンガ読んだ！！のマンガページです");
```
で、対応したViewを作ります。

```html:Index.cshtml
<div>
    <!-- このページのレイアウトはリダイレクトされるので自由。デバッグでも使えるようにOGPが読む情報の見える化した -->
    <h1>OGP用ページ</h1>
    <h2>すぐにリダイレクトされます。そのままお待ちください</h2>
    <div class="m-2">
        <img class="cover" src="@ViewData["OgImage"]" alt='' title="@ViewData["OgTitle"]" width="300" height="480" >
    </div>
    <div>
        <div>
            <h3 class="font-weight-bold">@ViewData["OgTitle"]</h3>
        </div>
        <div>
            @ViewData["OgDescription"]
        </div>
    </div>
    <!-- OGPクローラーは以下は見ない -->
    <script type="text/javascript">window.location = "/_@ViewData["ReturnUrl"]";</script>
</div>  
```
共通の_Layout.cshtmlにもViewDataを記述します。これでページ毎の正しいOPGが設定できます。

```html:_Layout.cshtml
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"]</title>
    <!-- og系のpropertyが正しく設定されていればOGPクローラーはそれを見る -->
    <meta property="og:url" content="@ViewData["OgUrl"]">
    <meta property="og:title" content="@ViewData["OgTitle"]">
    <meta property="og:description" content="@ViewData["OgDescription"]">
    <meta property="og:image" content="@ViewData["OgImage"]">
    <meta property="og:type" content="article">
    <meta property="og:site_name" content="マンガ読んだ！！">
</head>
```
#クライアントの具体的なコード
クライアントのapp.razorには以下のようなメソッドを用意します。

```cs:app.razor
@code{
    protected override void OnInitialized()
    {
        //URLの中に_があった場合
        if (new Uri(NavigationManager.Uri).AbsolutePath.Contains("_"))
        {
            //_を外してHTMLデコードして投げる
            NavigationManager.NavigateTo(WebUtility.HtmlDecode(NavigationManager.Uri.Replace("_", "")));
        }
    }
}
```
これで人間は目的としているページに飛ぶので、BlazorのOGP対策が完成です！[マンガ読んだ！！](https://manga-yonda.com)はマンガのページをFacebookやTwitterに貼った場合、ちゃんとそのページのOGPが表示され、人間はSPAのページに移動します！

#まとめ
BlazorでもOGPやSEOの簡単な対策は出来ました。OGPについてはBlazor Advent Calendar 2020の17日書かれた[ASP.NET Core Blazor WebAssembly のプリレンダリングを試してみよう](https://qiita.com/MNakae_IG/items/0ca29a93bd0c0a726d4d)の手法でも対応が出来るかなと思います。ただ、SEOはGoogleの検索結果に出ているというだけで、必ずしも望んだ形では出ていないこともあり、ちゃんとやるならもっと深い技術が必要になってくるのかなと思います。また最近読んだ[Next.js 4年目の知見：SSRはもう古い、VercelにAPIサーバを置くな](https://qiita.com/jagaapple/items/faf125e28f8c2860269c)ではSSR自体に対して意見を投げているので、こういった対応は今後変わってくるのかなとも思いました。

#引用
>
[SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d#vue%E3%81%AErouter%E3%81%A7%E5%85%83url%CE%B1%E3%82%92%E5%85%83url%E3%81%AB%E6%9B%B8%E3%81%8D%E6%88%BB%E3%81%99)
[Next.js 4年目の知見：SSRはもう古い、VercelにAPIサーバを置くな](https://qiita.com/jagaapple/items/faf125e28f8c2860269c)
[ASP.NET Core Blazor WebAssembly のプリレンダリングを試してみよう](https://qiita.com/MNakae_IG/items/0ca29a93bd0c0a726d4d)