feat: add preliminary /blog routes
This change also introduces toml file config
This commit is contained in:
parent
ba8e186cb5
commit
bd999f0ed8
42
OliverBooth/Data/Blog/Author.cs
Normal file
42
OliverBooth/Data/Blog/Author.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an author of a blog post.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Author : IEquatable<Author>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the email address of the author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The email address.</value>
|
||||||
|
public string? EmailAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The ID.</value>
|
||||||
|
public int Id { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The name.</value>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool Equals(Author? other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
return Id == other.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return ReferenceEquals(this, obj) || obj is Author other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return Id;
|
||||||
|
}
|
||||||
|
}
|
66
OliverBooth/Data/Blog/BlogPost.cs
Normal file
66
OliverBooth/Data/Blog/BlogPost.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a blog post.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlogPost : IEquatable<BlogPost>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The author ID.</value>
|
||||||
|
public int AuthorId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the body of the blog post.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The body.</value>
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the blog post.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The ID.</value>
|
||||||
|
public int Id { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the date and time at which the blog post was published.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The publish timestamp.</value>
|
||||||
|
public DateTimeOffset Published { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL slug of the blog post.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The URL slug.</value>
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the title of the blog post.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The title.</value>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the legacy WordPress ID of the blog post.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The legacy WordPress ID.</value>
|
||||||
|
public int? WordPressId { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(BlogPost? other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
return Id == other.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return ReferenceEquals(this, obj) || obj is BlogPost other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return Id;
|
||||||
|
}
|
||||||
|
}
|
21
OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs
Normal file
21
OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration for the <see cref="Author" /> entity.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(EntityTypeBuilder<Author> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Author");
|
||||||
|
builder.HasKey(e => e.Id);
|
||||||
|
|
||||||
|
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
builder.Property(e => e.Name).HasMaxLength(100).IsRequired();
|
||||||
|
builder.Property(e => e.EmailAddress).HasMaxLength(255).IsRequired(false);
|
||||||
|
}
|
||||||
|
}
|
25
OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs
Normal file
25
OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration for the <see cref="BlogPost" /> entity.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(EntityTypeBuilder<BlogPost> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("BlogPost");
|
||||||
|
builder.HasKey(e => e.Id);
|
||||||
|
|
||||||
|
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
builder.Property(e => e.WordPressId).IsRequired(false);
|
||||||
|
builder.Property(e => e.Slug).HasMaxLength(100).IsRequired();
|
||||||
|
builder.Property(e => e.AuthorId).IsRequired();
|
||||||
|
builder.Property(e => e.Published).IsRequired();
|
||||||
|
builder.Property(e => e.Title).HasMaxLength(255).IsRequired();
|
||||||
|
builder.Property(e => e.Body).IsRequired();
|
||||||
|
}
|
||||||
|
}
|
44
OliverBooth/Data/BlogContext.cs
Normal file
44
OliverBooth/Data/BlogContext.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
using OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a session with the blog database.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlogContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public BlogContext(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the set of authors.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The set of authors.</value>
|
||||||
|
public DbSet<Author> Authors { get; internal set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the set of blog posts.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The set of blog posts.</value>
|
||||||
|
public DbSet<BlogPost> BlogPosts { get; internal set; } = null!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
string connectionString = _configuration.GetConnectionString("Blog") ?? string.Empty;
|
||||||
|
ServerVersion serverVersion = ServerVersion.AutoDetect(connectionString);
|
||||||
|
optionsBuilder.UseMySql(connectionString, serverVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.ApplyConfiguration(new AuthorConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
||||||
|
}
|
||||||
|
}
|
28
OliverBooth/Data/WebContext.cs
Normal file
28
OliverBooth/Data/WebContext.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a session with the web database.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WebContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public WebContext(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
string connectionString = _configuration.GetConnectionString("Web") ?? string.Empty;
|
||||||
|
ServerVersion serverVersion = ServerVersion.AutoDetect(connectionString);
|
||||||
|
optionsBuilder.UseMySql(connectionString, serverVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/>
|
||||||
|
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||||
|
<PackageReference Include="Markdig" Version="0.32.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.9"/>
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.9"/>
|
||||||
<PackageReference Include="NLog" Version="5.2.3"/>
|
<PackageReference Include="NLog" Version="5.2.3"/>
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3"/>
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3"/>
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0"/>
|
||||||
<PackageReference Include="X10D" Version="3.2.2"/>
|
<PackageReference Include="X10D" Version="3.2.2"/>
|
||||||
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
9
OliverBooth/Pages/Blog/Article.cshtml
Normal file
9
OliverBooth/Pages/Blog/Article.cshtml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@page "/blog/{year:int}/{month:int}/{slug}"
|
||||||
|
@model OliverBooth.Pages.Blog.Article
|
||||||
|
|
||||||
|
@if (Model.Post is { } post)
|
||||||
|
{
|
||||||
|
<h1>@post.Title</h1>
|
||||||
|
<p class="text-muted">@post.Published.ToString("MMMM dd, yyyy") • @Model.Author?.Name</p>
|
||||||
|
@Html.Raw(Model.SanitizeContent(post.Body))
|
||||||
|
}
|
50
OliverBooth/Pages/Blog/Article.cshtml.cs
Normal file
50
OliverBooth/Pages/Blog/Article.cshtml.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
namespace OliverBooth.Pages.Blog;
|
||||||
|
|
||||||
|
public class Article : PageModel
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public Article(IDbContextFactory<BlogContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Author? Author { get; private set; }
|
||||||
|
|
||||||
|
public BlogPost? Post { get; private set; }
|
||||||
|
|
||||||
|
public string SanitizeContent(string content)
|
||||||
|
{
|
||||||
|
content = content.Replace("<more>", string.Empty);
|
||||||
|
|
||||||
|
while (content.Contains("\n\n"))
|
||||||
|
{
|
||||||
|
content = content.Replace("\n\n", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Markdig.Markdown.ToHtml(content.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet(int year, int month, string slug)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
Post = context.BlogPosts.FirstOrDefault(p => p.Published.Year == year &&
|
||||||
|
p.Published.Month == month &&
|
||||||
|
p.Slug == slug);
|
||||||
|
|
||||||
|
if (Post is null)
|
||||||
|
{
|
||||||
|
Response.StatusCode = 404;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Author = context.Authors.FirstOrDefault(a => a.Id == Post.AuthorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
OliverBooth/Pages/Blog/Index.cshtml
Normal file
16
OliverBooth/Pages/Blog/Index.cshtml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@page
|
||||||
|
@using OliverBooth.Data.Blog
|
||||||
|
@model OliverBooth.Pages.Blog.Index
|
||||||
|
|
||||||
|
@foreach (BlogPost post in Model.BlogPosts)
|
||||||
|
{
|
||||||
|
<h2>
|
||||||
|
<a asp-page="/blog/article" asp-route-year="@post.Published.Year" asp-route-month="@post.Published.Month.ToString().PadLeft(2, '0')" asp-route-slug="@post.Slug">@post.Title</a>
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted">@post.Published.ToString("MMMM dd, yyyy") • @Model.GetAuthor(post)?.Name</p>
|
||||||
|
<p>@Html.Raw(Model.SanitizeContent(Model.TrimContent(post.Body, out bool trimmed)))</p>
|
||||||
|
if (trimmed)
|
||||||
|
{
|
||||||
|
<p><a asp-page="/blog/article" asp-route-year="@post.Published.Year" asp-route-month="@post.Published.Month.ToString().PadLeft(2, '0')" asp-route-slug="@post.Slug">Read more...</a></p>
|
||||||
|
}
|
||||||
|
}
|
51
OliverBooth/Pages/Blog/Index.cshtml.cs
Normal file
51
OliverBooth/Pages/Blog/Index.cshtml.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
namespace OliverBooth.Pages.Blog;
|
||||||
|
|
||||||
|
public class Index : PageModel
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public Index(IDbContextFactory<BlogContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<BlogPost> BlogPosts { get; private set; } = ArraySegment<BlogPost>.Empty;
|
||||||
|
|
||||||
|
public string SanitizeContent(string content)
|
||||||
|
{
|
||||||
|
content = content.Replace("<more>", string.Empty);
|
||||||
|
|
||||||
|
while (content.Contains("\n\n"))
|
||||||
|
{
|
||||||
|
content = content.Replace("\n\n", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Markdig.Markdown.ToHtml(content.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public string TrimContent(string content, out bool trimmed)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<char> span = content.AsSpan();
|
||||||
|
int moreIndex = span.IndexOf("<more>", StringComparison.Ordinal);
|
||||||
|
trimmed = moreIndex != -1 || span.Length > 256;
|
||||||
|
return moreIndex != -1 ? span[..moreIndex].Trim().ToString() : content.Truncate(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Author? GetAuthor(BlogPost post)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
return context.Authors.FirstOrDefault(a => a.Id == post.AuthorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
BlogPosts = context.BlogPosts.ToArray();
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,17 @@
|
|||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using X10D.Hosting.DependencyInjection;
|
using X10D.Hosting.DependencyInjection;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddNLog();
|
builder.Logging.AddNLog();
|
||||||
builder.Services.AddHostedSingleton<LoggingService>();
|
builder.Services.AddHostedSingleton<LoggingService>();
|
||||||
|
|
||||||
|
builder.Services.AddDbContextFactory<BlogContext>();
|
||||||
|
builder.Services.AddDbContextFactory<WebContext>();
|
||||||
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||||
|
Loading…
Reference in New Issue
Block a user