URL "articles" にアクセスされたら "articles/1" に自動で遷移 する Blazor アプリを作る - HTTP 302 を返すところまでやる

## URL パラメータ省略時は、既定のアイテムが指定された URL へ自動遷移させたい

`https://.../articles/{記事ID}` のような URL を指定されたら、その指定された記事 ID の記事を表示するような、Blazor アプリケーションプロジェクトがあるとしましょう。つまり、ざっくり以下のような作りの Razor コンポーネントが実装されているイメージです。
 
```js:Articles.razor
@page "/articles/{Id:int}"

<!--
// 記事 ID をキーにサーバ等から取得した記事本文を
// レンダリングする実装・マークアップが必要だが、省略
-->

@code
{
    [Parameter]
    public int Id { get; set; }

    // さらには、OnInitializedAsync などのライフサイクルメソッド内にて、
    // 記事 ID から記事本文をサーバ等に取りに行くなどの処理が必要だが、省略
}
```

さてここで、記事 ID が未指定の URL、`https://.../articles` が指定されたときは、既定の1つめの記事の URL へ自動で移動するように、この Blazor アプリケーションに機能追加したいと思います。

## URL パラメーターなしの URL にマッチする Razor コンポーネントを書けば OK

ということで、記事 ID 未指定の URL にマッチする Razor コンポーネントを、もうひとつ新たに作ることにします。そのコンポーネントは、初回レンダリング時に、既定の 1 つめの記事の ID を求めた上で、`https://.../articles/{記事ID}` の URL に `NavigationService` を使ってナビゲートすればよい、という方針です。

もうちょっと具体的にしていきます。名前は `AriclesDefault.razor` とでもしてみます。冒頭には `@page` ディレクティブを記述し、記事 ID 未指定の URL、`https://.../articles` にマッチするようにします。

```js:ArticlesDefault.razor
@page "/articles" @* // 👈これを追加 *@

```

続けて、指定の URL に遷移するために `NavigationManager` サービスを使うので、これを DI コンテナから注入してもらうよう `@inject` ディレクティブを追記し...

```js:ArticlesDefault.razor
@page "/articles"
@inject NavigationManager NavigationManager  @* // 👈これを追加 *@
```

仕上げに、`@code` ブロックを追加し、`OnAfterRender` メソッド内で、既定の 1 つめの記事の URL へ自動で遷移 (ナビゲート) するよう実装して完成です。

```js:ArticlesDefault.razor
@page "/articles"
@inject NavigationManager NavigationManager

@code
{
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            // 既定の 1 つめの記事を指す URL に、自動でナビゲートする。
            // (実際には記事データベースから既定の記事を探索する処理などがあるはずだが、割愛)
            this.NavigationManager.NavigateTo("articles/1");
        }
    }
}
```

以上で、URL `https://.../articles` を開くと、`https://.../articles/1` に自動で遷移する Blazor アプリケーションができあがりです。

## もっと工夫の余地がある

### サーバー側レンダリング時は HTTP 302 を返すとよいのでは?

さて、いちおうこれにてできあがりではあるのですが、サーバー側レンダリングを行なう Blazor アプリケーションの場合 (ASP.NET Core サーバーでホストされた、サーバ側プリレンダリングを行なう Blazor WebAssembly アプリも含まれます) は、もうちょっと工夫できます。

Blazor の対話モードが起動している最中のページ遷移ではなく、**直接、記事 ID なし URL が開かれた場合は HTTP 302 Found 応答を返すようにする**、すなわちリダイレクトの動作を実現するほうが、**SEO 的に望ましい**のではないでしょうか?


### 実装してみる

ということで実際にやってみたいと思います。

まず、`ArticelsDefault.razor` 内の `@code` ブロック内にて、`HttpContext` 型のカスケーディングパラメータープロパティを追加します。

```js:ArticlesDefault.razor
...
@code
{
    [CascadingParameter]
    public HttpContext? HttpContext { get; set; }
    ...
```

この `HttpContext` カスケーディングパラメーターには、**サーバー側静的レンダリング (プリレンダリングを含みます) が行なわれる際**、つまりはブラウザからの HTTP GET 要求への対応最中であることを意味しますが、その HTTP GET 要求の状況を示す **`HttpContext` のインスタンスが設定される**ようになっています。

そこで、このコンポーネントの `OnInitializedAsync` ライフサイクルメソッド内にて、この `HttpContext` カスケーディングパラメーターが null でなければ、この `HttpContext` を介して、遷移先の URL (既定の 1 つめの記事 ID が含まれる URL) を添えた HTTP 304 Found 応答を返せばよいです。具体的な実装例を以下に示します。

```js:ArticlesDefault.razor
...
@code
{
    ...
    protected override async Task OnInitializedAsync()
    {
        // カスケーディングパラメーター "HttpContext" が null でなければ、
        // サーバー側静的レンダリング、つまりブラウザからの HTTP GET 要求の最中のはずなので...
        if (this.HttpContext is not null)
        {
            // (実際には記事データベースから既定の記事を探索する処理などがあるはずだが、割愛)

            // HTTP 302 Found を返して、指定の URL へリダイレクトするよう指示
            this.HttpContext.Response.Redirect($"/articles/1");

            // これ以上、このコンポーネントのレンダリングを継続してクライアントに返す必要もない
            // (むしろ使われないコンテンツを返信することになり通信が無駄になる) ので、
            // ここまでで HTTP 応答を完了してしまう
            await this.HttpContext.Response.CompleteAsync();
        }
    }
    ...
```

以上で完成です。

試しにターミナルを開いて、curl コマンドで `https://.../articles` への HTTP GET 要求を送ってみると...

```ps
> curl -i https://localhost:7286/articles
HTTP/1.1 302 Found
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 04 Dec 2024 12:08:14 GMT
Server: Kestrel
Cache-Control: no-cache, no-store
Location: /articles/1
Pragma: no-cache
Set-Cookie: .AspNetCore.Antiforgery.4oiuVnI1jG0=CfDJ8Lm_2IoRzSRJi28blm6qC0KCUZvK1T_LI_2YVA5l5WrciuXl5AiqG425Cl2ob9YUhQFHLFvwO0yIs3hO4yL2cnSZwX8GbT4PPt7JPYTZLE0kHyCSMEjamrE-GLMj1XdU1XfyDvI3658JklUiKvsEtI0; path=/; samesite=strict; httponly
Content-Security-Policy: frame-ancestors 'self'
blazor-enhanced-nav: allow
X-Frame-Options: SAMEORIGIN

> _
```

いい感じに、不要なコンテンツが含まれることもなくシンプルに、`/articles/1` への HTTP 302 Found リダイレクト応答になっていることが確認できました。

## おわりに

Blazor は、もともとは、いわゆるシングルページアプリケーション、SPA を実装するものとしてプロジェクトが開始したものと認識しています。しかし .NET 8 以降、今やサーバー側レンダリングアプリケーション (SPA に対する MPA とでも言うような) もコンポーネント指向のアーキテクチャはそのままに実装することができるようになっています。そのため、今回やってみたように HTTP 302 Found 応答を返すなどといった、基礎的な HTTP 要求と応答のモデルにも対応できていて、なかなかに面白いなぁ、と思いました。