サイトは再び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/ # 拡張メソッド
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. ページリクエスト処理フロー
- ユーザーが
/timestampをリクエスト - ルーティングシステムが
@page "/timestamp"にマッチ OnGetAsync()メソッドを実行(存在する場合)AppServiceを介してビジネスデータを取得- ビューをレンダリングして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