AIによるRazor Pagesサイトの再構築完了

AIによるRazor Pagesサイトの再構築完了

Blazor静的SSRからRazor Pagesへの回帰、ソースコードで解説するサイトアーキテクチャ設計とコア実装

最終更新 2026/04/16 23:00
沙漠尽头的狼
読了目安 5 分
カテゴリ
.NET フロントエンド
タグ
.NET C# ASP.NET Core Blazor Razor Pages

サイトは再びBlazorからRazor Pagesに戻りました。本稿では、ソースコードの観点からサイトの現在のアーキテクチャ設計を解説します。

1. なぜBlazorからRazor Pagesに戻したのか

サイトのフロントエンドは以前Blazorの静的SSRで開発していましたが、コンテンツ表示型サイトについて深く考察した結果、Razor Pagesの方がこのようなシナリオに適していると判断しました。

2. サイトのプロジェクト構成

ソースコードリポジトリ:CodeWF。フロントエンドとバックエンドが分離されたアーキテクチャです。

CodeWF/src/
├── WebApp/                 # フロントエンドサイト(Razor Pages)
│   ├── Pages/             # ページファイル
│   ├── Components/        # View Components
│   ├── Controllers/      # APIコントローラー
│   └── wwwroot/          # 静的リソース
│
└── CodeWF/                # コアクラスライブラリ
    ├── Models/           # データモデル
    ├── Services/         # ビジネスサービス
    └── Extensions/       # 拡張メソッド

CodeWFアーキテクチャ図

3. Razor Pagesのコア実装

3.1 ページモデル(PageModel)

ツールページを例に、典型的なPageModelの記述を示します。

// Pages/Tool/Index.cshtml.cs
namespace WebApp.Pages.Tool;

public class IndexModel : PageModel
{
    private readonly AppService _appService;

    public List<ToolItem>? Tools { get; set; }

    public IndexModel(AppService appService)
    {
        _appService = appService;
    }

    public async Task OnGetAsync()
    {
        Tools = await _appService.GetAllToolItemsAsync();
    }
}

対応するビューファイル Pages/Tool/Index.cshtml

@page
@model WebApp.Pages.Tool.IndexModel
@{
    ViewData["Title"] = "ツール";
}

<div class="container">
    <h1 class="mb-4">オンラインツール</h1>
    <div class="row">
        @foreach (var tool in Model.Tools)
        {
            <div class="col-md-4 mb-4">
                <div class="card shadow-sm h-100">
                    <div class="card-body">
                        <h4 class="card-title">@tool.Name</h4>
                        @if (!string.IsNullOrEmpty(tool.Memo))
                        {
                            <p class="card-text text-muted">@tool.Memo</p>
                        }
                        @if (tool.Children != null && tool.Children.Any())
                        {
                            <ul class="list-group list-group-flush mt-3">
                                @foreach (var child in tool.Children)
                                {
                                    <li class="list-group-item">
                                        <a href="@child.Slug">@child.Name</a>
                                    </li>
                                }
                            </ul>
                        }
                    </div>
                </div>
            </div>
        }
    </div>
</div>

3.2 ルートパラメータのバインド

記事詳細ページではルートテンプレート構文を使用します。

@page "/{year:int}/{month:int}/{slug}"
@model WebApp.Pages.Blog.Post.IndexModel

対応するコードビハインド Post/Index.cshtml.cs

public class IndexModel : PageModel
{
    public BlogPost? Post { get; set; }

    public async Task OnGetAsync(int year, int month, string slug)
    {
        Post = await _appService.GetPostBySlug(slug);
    }
}

3.3 共有レイアウト(Layout)

Layoutファイルでグローバルなページ構造を定義します。

@inject CodeWF.Services.AppService AppService
<!DOCTYPE html>
<html lang="ja-JP">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - CodeWF</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-dark bg-tech-nav fixed-top">
            <!-- ナビゲーションバー内容 -->
        </nav>
    </header>

    <div style="padding-top: 80px;">
        @RenderBody()
    </div>

    @await Component.InvokeAsync("FriendLink")

    <footer>
        <!-- フッター内容 -->
    </footer>
</body>
</html>

4. サービス層の設計

4.1 AppServiceコアサービス

AppService.cs はアプリケーション全体のコアサービスです。

public class AppService(IOptions<SiteOption> siteOption)
{
    private List<BlogPost>? _blogPosts;
    private List<ToolItem>? _toolItems;
    private List<DocItem>? _docItems;
    private List<CategoryItem>? _categoryItems;
    private List<AlbumItem>? _albumItems;

    public async Task<List<BlogPost>?> GetAllBlogPostsAsync()
    {
        if (_blogPosts?.Any() == true) return _blogPosts;

        _blogPosts = [];
        var endYear = DateTime.Now.Year;

        for (var start = siteOption.Value.StartYear; start <= endYear; start++)
        {
            var postDir = Path.Combine(siteOption.Value.LocalAssetsDir, start.ToString());
            if (!Directory.Exists(postDir)) continue;

            var postFiles = Directory.GetFiles(postDir, "*.md", SearchOption.AllDirectories);
            foreach (var postFile in postFiles)
            {
                var blogPost = await ReadBlogPostAsync(postFile);
                if (!blogPost.Draft)
                {
                    _blogPosts.Add(blogPost);
                }
            }
        }

        _blogPosts = _blogPosts
            .OrderByDescending(post => post.Lastmod ?? post.Date ?? DateTime.MinValue)
            .ThenByDescending(post => post.Date ?? DateTime.MinValue)
            .ToList();
        return _blogPosts;
    }

    public async Task SeedAsync()
    {
        // 起動時に全データをプリロード
        await GetAllAlbumItemsAsync();
        await GetAllCategoryItemsAsync();
        await GetAllBlogPostsAsync();
        await GetAllFriendLinkItemsAsync();
        await GetAllDocItemsAsync();
        await GetAllToolItemsAsync();
    }
}

4.2 データモデル

// BlogPost.cs
public class BlogPostBrief
{
    public string? Title { get; set; }
    public string? Slug { get; set; }
    public string? Description { get; set; }
    public DateTime? Date { get; set; }
    public DateTime? Lastmod { get; set; }
    public string? Author { get; set; }
    public string? Cover { get; set; }
    public List<string>? Categories { get; set; }
    public List<string>? Tags { get; set; }
}

public class BlogPost : BlogPostBrief
{
    public string? Content { get; set; }      // 元のMarkdown
    public string? HtmlContent { get; set; }  // 変換後のHTML
}

5. ページリクエスト処理フロー

ページリクエスト処理フロー

  1. ユーザーが /timestamp をリクエスト
  2. ルーティングシステムが @page "/timestamp" にマッチ
  3. OnGetAsync() メソッドを実行(存在する場合)
  4. AppService を介してビジネスデータを取得
  5. ビューをレンダリングしてHTMLを返却

6. Program.csの設定

var builder = WebApplication.CreateBuilder(args);

// Razor Pagesサービスを追加
builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddHttpClient();

// AppServiceの依存性注入
builder.Services.AddSingleton<AppService>();
builder.Services.Configure<SiteOption>(builder.Configuration.GetSection("Site"));

var app = builder.Build();

// 起動時にデータをプリロード
using (var serviceScope = app.Services.CreateScope())
{
    var service = serviceScope.ServiceProvider.GetRequiredService<AppService>();
    await service.SeedAsync();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapRazorPages();
app.MapControllers();

app.Run();

7. オンラインツールの例:タイムスタンプ変換

Timestamp.cshtml を例にフロントエンドのインタラクションを示します。

@page "/timestamp"
@{ ViewData["Title"] = "タイムスタンプ変換ツール"; }

<div class="container">
    <h1 class="mb-4">タイムスタンプ変換ツール</h1>

    <div class="card mb-3">
        <div class="card-body">
            <h5 class="card-title">タイムスタンプ → 日付</h5>
            <div class="row mb-3">
                <div class="col d-flex align-items-center gap-2">
                    <span>タイムスタンプ:</span>
                    <input type="text" class="form-control" style="width: 200px;"
                           id="inputTimestamp" placeholder="タイムスタンプを入力" />
                    <select class="form-select" style="width: 100px;" id="timestampUnit">
                        <option value="s">秒</option>
                        <option value="ms">ミリ秒</option>
                    </select>
                    <button class="btn btn-primary btn-sm"
                            onclick="convertTimestampToDate()">日付に変換</button>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script>
        function convertTimestampToDate() {
            const timestamp = document.getElementById('inputTimestamp').value;
            const unit = document.getElementById('timestampUnit').value;
            const ms = unit === 's' ? timestamp * 1000 : parseInt(timestamp);
            const date = new Date(ms);
            document.getElementById('outputDate').value = date.toLocaleString();
        }
    </script>
}

8. まとめ

ソースコードはオープンソース化されています。ぜひご覧いただき、ご意見をお寄せください。

リポジトリURL:https://github.com/dotnet9/CodeWF

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2025/03/10

駐車連絡用QRコード生成ツールの開発実践

本記事では、C#とAvaloniaを使用したデスクトップ版、およびBlazorフロントエンドと.NET Web APIを使用したオンライン版の駐車連絡用QRコード生成ツールの開発方法について、要件分析、コアコード実装、UI設計、MVVMパターンの適用を含めて紹介します。

続きを読む
同じカテゴリ / 同じタグ 2024/06/20

CodeWF.EventBus:軽量イベントバス、コミュニケーションをよりスムーズに

CodeWF.EventBusは、モジュール間の疎結合通信を実現する柔軟なイベントバスライブラリです。WPF、WinForms、ASP.NET Coreなど、さまざまな.NETプロジェクトタイプに対応しています。シンプルな設計で、コマンドのパブリッシュとサブスクライブ、リクエストとレスポンスを簡単に実装できます。順序付けられたイベント処理により、イベントが適切に処理されることを保証します。コードを簡素化し、システムの保守性を向上させます。

続きを読む
同じカテゴリ / 同じタグ 2024/01/19

.NET ベースの FluentValidation 検証チュートリアル

FluentValidationは、.NETベースの検証フレームワークで、オープンソースかつ無料、そしてエレガントです。チェーン操作をサポートし、理解しやすく、機能が充実しています。さらに、MVC5、WebApi2、ASP.NET Coreと深く統合でき、コンポーネント内には十数種類の一般的なバリデーターが用意されており、拡張性が高く、カスタムバリデーターをサポートし、ローカライズ多言語にも対応しています。

続きを読む