The website has moved from Blazor back to Razor Pages. This article explains the current architecture of the website from a source code perspective.
1. Why switch from Blazor to Razor Pages
The front end of the website was previously developed using Blazor static SSR. After deeper consideration of content display websites, it was determined that Razor Pages might be more suitable for such scenarios.
2. Website Project Structure
Source Repository: CodeWF, using a front-end/back-end separation architecture:
CodeWF/src/
├── WebApp/ # Front-end site (Razor Pages)
│ ├── Pages/ # Page files
│ ├── Components/ # View Components
│ ├── Controllers/ # API controllers
│ └── wwwroot/ # Static assets
│
└── CodeWF/ # Core class library
├── Models/ # Data models
├── Services/ # Business services
└── Extensions/ # Extension methods
3. Razor Pages Core Implementation
3.1 Page Model
Using the tools page as an example, a typical PageModel implementation:
// 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();
}
}
Corresponding view file Pages/Tool/Index.cshtml:
@page
@model WebApp.Pages.Tool.IndexModel
@{
ViewData["Title"] = "Tools";
}
<div class="container">
<h1 class="mb-4">Online Tools</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 Route Parameter Binding
The article detail page uses route template syntax:
@page "/{year:int}/{month:int}/{slug}"
@model WebApp.Pages.Blog.Post.IndexModel
Corresponding code-behind 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 Shared Layout
Layout file defining the global page structure:
@inject CodeWF.Services.AppService AppService
<!DOCTYPE html>
<html lang="en">
<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">
<!-- Navigation bar content -->
</nav>
</header>
<div style="padding-top: 80px;">
@RenderBody()
</div>
@await Component.InvokeAsync("FriendLink")
<footer>
<!-- Footer content -->
</footer>
</body>
</html>
4. Service Layer Design
4.1 AppService Core Service
AppService.cs is the core service class for the entire application:
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()
{
// Preload all data on startup
await GetAllAlbumItemsAsync();
await GetAllCategoryItemsAsync();
await GetAllBlogPostsAsync();
await GetAllFriendLinkItemsAsync();
await GetAllDocItemsAsync();
await GetAllToolItemsAsync();
}
}
4.2 Data Models
// 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; } // Raw Markdown
public string? HtmlContent { get; set; } // Converted HTML
}
5. Page Request Processing Flow
- User requests
/timestamp - Routing system matches
@page "/timestamp" - Executes
OnGetAsync()method (if exists) - Retrieves business data via
AppService - Renders view and returns HTML
6. Program.cs Configuration
var builder = WebApplication.CreateBuilder(args);
// Add Razor Pages services
builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddHttpClient();
// Dependency inject AppService
builder.Services.AddSingleton<AppService>();
builder.Services.Configure<SiteOption>(builder.Configuration.GetSection("Site"));
var app = builder.Build();
// Preload data on startup
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. Online Tool Example: Timestamp Conversion
Using Timestamp.cshtml to demonstrate front-end interaction:
@page "/timestamp"
@{ ViewData["Title"] = "Timestamp Converter Tool"; }
<div class="container">
<h1 class="mb-4">Timestamp Converter Tool</h1>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Timestamp → Date</h5>
<div class="row mb-3">
<div class="col d-flex align-items-center gap-2">
<span>Timestamp:</span>
<input type="text" class="form-control" style="width: 200px;"
id="inputTimestamp" placeholder="Enter timestamp" />
<select class="form-select" style="width: 100px;" id="timestampUnit">
<option value="s">Seconds</option>
<option value="ms">Milliseconds</option>
</select>
<button class="btn btn-primary btn-sm"
onclick="convertTimestampToDate()">Convert to Date</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. Summary
The source code is open source, and we welcome exchanges and learning.
Repository URL: https://github.com/dotnet9/CodeWF