GitHub Pages に配置した Blazor WebAssembly アプリの検索エンジン最適化のため、Blazor コンポーネントを HTML 文字列にレンダリングする方法

# はじめに

大抵の場合、Blazor WebAssembly アプリのインターネット検索結果は芳しくないことと思います。

つまり、結果は単に「Loading...」になってしまうのではないでしょうか (下図のように)。

![Loading...](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/c701e94d-3483-30a7-1854-3e26fccb9ca5.png)

幸い、サーバー側実装にて事前レンダリングを行えば、つまり検索エンジンクローラが HTTP GET 要求したときに返される `index.html` 内に Blazor WebAssembly アプリがブラウザ上で稼働開始した初期内容と同じ HTML が記載済みであれば、検索結果を改善できます。

> _余談かもですが、Google の検索クローラーは、Blazor WebAssembly アプリなどの WebAssembly プロセスを処理しているという噂を聞いたような覚えもあります。しかし現時点では出典も今となっては不明で、かつ、証拠の持ち合わせもありません。_

まず前提条件として、Blazor アプリのホスティングモデルが WebAssembly であっても、サーバー側でASP.NET Core プロセスを実行することで、サーバー側の事前レンダリングを遂行する必要があります。

もしサーバー側実装として ASP.NET Core サーバーを稼働させることに問題がなければ、サーバー側の事前レンダリングの実装そのものは非常に簡単です。

具体的には、以下の手順で実装を進めれば済みます。

1. 事前レンダリング用の Razor ページ（.cshtmlファイル）を作成し、
2. その Razor ページをフォールバックページとして指定します。
3. 最後に、`RenderMode="Static"` プロパティ指定した、ASP.NET Core タグヘルパーのひとつである `<component ... />` タグを記述します。

なお、上記実装による事前レンダリング処理は、(ブラウザ上ではなく) サーバー上で実行されますから、サーバー側 Web API へ HTTP 通信している場合はどうするかなど、実装上の問題を考慮する必要があることもあります。

しかしとにかく、サーバー側の事前レンダリングの基本的な実装は、上記のように非常に簡単です。

- [参考記事: "クライアントサイド Blazor でも、SEO 対策のためにサーバーサイドレンダリングしたい - Qiita"](https://qiita.com/jsakamoto/items/25dcd56be8a573bc6827)

# しかし、静的コンテンツのみのホスティング環境では...

いっぽう、Blazor WebAssmebly アプリが、GitHub Pages に配置されている場合などのように、ASP.NET Core サーバープロセスを使用できない場合は、上記のやり方はできません。

幸い、事前レンダリングするチャンスがもう1つあります。
それは「発行 (publish)」処理です。

つまり、発行処理の直後の追加処理として、発行された先の `index.html` に、事前レンダリングされた HTML 文字列を挿入することができるはずです。

ということで、Blazor コンポーネントを HTML 文字列にレンダリングする方法がないか考えてみました。

いくつか思いついたのですが、そのうちの1つは、この目的のために一時的に ASP.NET Core ホストを起動し、それに対して (Web ブラウザと同じように) HTTP GET 要求を送信して事前レンダリングされたHTML コンテンツを取得する、そういうプログラムを書いて実行するという方法です。

実のところ、自分はこの方法を実際に試したことはないのですが、でもきっとうまくいくとは思います。

ただ、この方法は実装が複雑になりそうな心配がありました。
ひとつ例を挙げれば、ASP.NET Core ホスト用の空き TCP ポートを動的に見つける必要があろう、など、基本コンセプトは問題ないけれども、実現するにあたっては細かい考慮事項がままありそうな予感がした次第です。

ということで、次に別の方法として、`<component ... />` ASP.NET Core タグヘルパーに注目しました。

つまり、`<component ... />` ASP.NET Core タグヘルパーの出力から事前レンダリングされた HTML 文字列を取得できるのでは? と思ったのです。
かつ、この方法は、TCP 通信などの I/O を伴わずにすべてオンメモリで処理できて、実装が簡単に済むのではないか? と想像したのです。

ということで、この方法を試してみたところ、見事成功しました！

# `<component ... />` ASP.NET Core タグヘルパーを使用して Blazor コンポーネントを HTML 文字列にレンダリング

`<component ... />` ASP.NET Core タグヘルパーの正体は、`Microsoft.AspNetCore.Mvc.TagHelpers` 名前空間に存在する `ComponentTagHelper` クラスです。

`ComponentTagHelper` クラスを使用して Blazor コンポーネントを HTML 文字列にレンダリングするには、`ComponentTagHelper` が依存するいくつかのオブジェクトを設定する必要がありました。

それら必要となるオブジェクトのクラス/インターフェースを列記してみると、以下のとおりとなります。

- ServiceCollection
- IServiceProvider（Logger および Razor Pages のサービスを追加）
- IServiceScope
- FeatureCollection
- IHttpRequestFeature
- HttpResponseFeature
- DefaultHttpContextFactory
- HttpContext
- TagHelperAttributeList
- TagHelperContext
- TagHelperOutput
- DefaultTagHelperContent

これらのオブジェクトを構築し、HTML 文字列を得るための C# コードは約 35 行程になりました。

実際のコードは、最後でも触れますが、[GitHub 上、こちら](https://github.com/jsakamoto/jsakamoto.github.io/blob/master/CUIFlavoredPortfolioSite.PreRenderer/Program.cs#L41)で見ることができます。

上記実際のコードを見てもらえるとわかると思いますが、若干面倒くささはあるとはいえ、粛々とオブジェクトを構築していくだけなので、それほど大変な作業ということもありません。

文書化されていない API を使用するだとか、リフレクション技術を使ってこねくりまわすなどの特別なハックも必要なく、素直に・愚直に、オブジェクトを積み上げていけば済みました。

こうしてこれら必要オブジェクト（`HttpContext`、`TagHelperContext`、`TagHelperOutput` など）をセットアップできたら、以下のように `ComponentTagHelper` クラスのインスタンスを作成できます。

```csharp
var componentTagHelper = new ComponentTagHelper
{
    // 👇 ここでHTML文字列にレンダリングするBlazorコンポーネントクラスを指定します。
    ComponentType = typeof(App),
    RenderMode = RenderMode.Static,
    ViewContext = new ViewContext {HttpContext = httpContext}
};
```

`ComponentTagHelper` オブジェクトを作成したら、`ProcessAsync()` メソッドを呼び出します。

```csharp
await componentTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
```

`ProcessAsync()` メソッドの呼び出しから返ってくると、`ProcessAsync()` メソッドの引数の 1 つとして渡した `TagHelperOutput` オブジェクトに、指定した Blazor コンポーネントを HTML 文字列にレンダリングした結果が含まれています。

ということで、この `TagHelperOutput` オブジェクトから HTML 文字列を取得できます。

```csharp
var content = tagHelperOutput.Content;
var stringWriter = new StringWriter();
content.WriteTo(stringWriter, HtmlEncoder.Default);

// 👇 これが、Blazor コンポーネントを HTML 文字列にレンダリングした結果です。
var html = stringWriter.ToString();
```

以上実装により、「発行」の後処理として、事前にレンダリングされた HTML 文字列を取得し、発行先の `index.html` に挿入することができました。

# まとめ

実際の実装の全体は、GitHub Pages 上に配置してある、自分の「CUI 風ポートフォリオサイト」Blazor WebAssembly アプリのソースコードで確認できます。

ソースコードは以下の GitHub リポジトリにあります。

- https://github.com/jsakamoto/jsakamoto.github.io

なお、この記事手順だけでは、`<title>` や `<meta name = "og：..." ...>` といった、Blazor コンポーネント外部の要素については、事前レンダリングできないことに注意してください。

これらの要素を事前レンダリングすることは可能なはずですが、追加の実装や、別の手法を選択する必要があることでしょう。

でもまぁ、とにかく、この技法を使うことで、GitHub Pages でホストされている Blazor WebAssembly アプリのインターネット検索結果を改善することができました!

![Improved](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/851b50ae-a4aa-6df8-789a-95602bfbfc80.png)

Happy Coding :)