Compare commits
2 Commits
be44fb4b4b
...
67d89c1831
Author | SHA1 | Date | |
---|---|---|---|
67d89c1831 | |||
0a9c2e82d5 |
@ -7,7 +7,7 @@ const terser = require('gulp-terser');
|
|||||||
const webpack = require('webpack-stream');
|
const webpack = require('webpack-stream');
|
||||||
|
|
||||||
const srcDir = 'src';
|
const srcDir = 'src';
|
||||||
const destDir = 'OliverBooth.Common/wwwroot';
|
const destDir = 'OliverBooth/wwwroot';
|
||||||
|
|
||||||
function compileSCSS() {
|
function compileSCSS() {
|
||||||
return gulp.src(`${srcDir}/scss/**/*.scss`)
|
return gulp.src(`${srcDir}/scss/**/*.scss`)
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 443
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["OliverBooth.Blog/OliverBooth.Blog.csproj", "OliverBooth.Blog/"]
|
|
||||||
RUN dotnet restore "OliverBooth.Blog/OliverBooth.Blog.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/OliverBooth.Blog"
|
|
||||||
RUN dotnet build "OliverBooth.Blog.csproj" -c Release -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
RUN dotnet publish "OliverBooth.Blog.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "OliverBooth.Blog.dll"]
|
|
@ -1,13 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,2 +0,0 @@
|
|||||||
@namespace OliverBooth.Blog.Pages
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -1,3 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
using OliverBooth.Blog.Data;
|
|
||||||
using OliverBooth.Blog.Middleware;
|
|
||||||
using OliverBooth.Blog.Services;
|
|
||||||
using OliverBooth.Common;
|
|
||||||
using OliverBooth.Common.Extensions;
|
|
||||||
using OliverBooth.Common.Services;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
|
||||||
.WriteTo.Console()
|
|
||||||
.WriteTo.File("logs/latest.log", rollingInterval: RollingInterval.Day)
|
|
||||||
.CreateLogger();
|
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
|
||||||
builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
builder.Logging.AddSerilog();
|
|
||||||
|
|
||||||
builder.Services.AddMarkdownPipeline();
|
|
||||||
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
|
||||||
builder.Services.AddDbContextFactory<BlogContext>();
|
|
||||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
|
||||||
builder.Services.AddSingleton<IUserService, UserService>();
|
|
||||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
|
||||||
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
|
||||||
builder.Services.AddControllersWithViews();
|
|
||||||
|
|
||||||
builder.WebHost.AddCertificateFromEnvironment(2846, 5050);
|
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
app.UseRouting();
|
|
||||||
app.MapRssFeed("/feed");
|
|
||||||
app.MapRazorPages();
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
app.Run();
|
|
@ -1,70 +0,0 @@
|
|||||||
using System.Buffers.Binary;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using OliverBooth.Blog.Data;
|
|
||||||
using OliverBooth.Common.Formatting;
|
|
||||||
using OliverBooth.Common.Markdown;
|
|
||||||
using OliverBooth.Common.Services;
|
|
||||||
using SmartFormat;
|
|
||||||
using SmartFormat.Extensions;
|
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a service that renders MediaWiki-style templates.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class TemplateService : ITemplateService
|
|
||||||
{
|
|
||||||
private static readonly Random Random = new();
|
|
||||||
private readonly IDbContextFactory<BlogContext> _webContextFactory;
|
|
||||||
private readonly SmartFormatter _formatter;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
|
||||||
/// <param name="webContextFactory">The <see cref="BlogContext" /> factory.</param>
|
|
||||||
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<BlogContext> webContextFactory)
|
|
||||||
{
|
|
||||||
_formatter = Smart.CreateDefaultSmartFormat();
|
|
||||||
_formatter.AddExtensions(new DefaultSource());
|
|
||||||
_formatter.AddExtensions(new ReflectionSource());
|
|
||||||
_formatter.AddExtensions(new DateFormatter());
|
|
||||||
_formatter.AddExtensions(new MarkdownFormatter(serviceProvider));
|
|
||||||
|
|
||||||
_webContextFactory = webContextFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string RenderTemplate(TemplateInline templateInline)
|
|
||||||
{
|
|
||||||
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
|
||||||
|
|
||||||
using BlogContext webContext = _webContextFactory.CreateDbContext();
|
|
||||||
Template? template = webContext.Templates.Find(templateInline.Name);
|
|
||||||
if (template is null)
|
|
||||||
{
|
|
||||||
return $"{{{{{templateInline.Name}}}}}";
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> randomBytes = stackalloc byte[20];
|
|
||||||
Random.NextBytes(randomBytes);
|
|
||||||
|
|
||||||
var formatted = new
|
|
||||||
{
|
|
||||||
templateInline.ArgumentList,
|
|
||||||
templateInline.ArgumentString,
|
|
||||||
templateInline.Params,
|
|
||||||
RandomInt = BinaryPrimitives.ReadInt32LittleEndian(randomBytes[..4]),
|
|
||||||
RandomGuid = new Guid(randomBytes[4..]).ToString("N"),
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _formatter.Format(template.FormatString, formatted);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return $"{{{{{templateInline.Name}|{templateInline.ArgumentString}}}}}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
using Markdig;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using OliverBooth.Common.Markdown;
|
|
||||||
using OliverBooth.Common.Services;
|
|
||||||
|
|
||||||
namespace OliverBooth.Common.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Extension methods for <see cref="IServiceCollection" />.
|
|
||||||
/// </summary>
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Adds the Markdown pipeline to the <see cref="IServiceCollection" />.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceCollection">The <see cref="IServiceCollection" />.</param>
|
|
||||||
/// <returns>The <see cref="IServiceCollection" />.</returns>
|
|
||||||
public static IServiceCollection AddMarkdownPipeline(this IServiceCollection serviceCollection)
|
|
||||||
{
|
|
||||||
return serviceCollection.AddSingleton(provider => new MarkdownPipelineBuilder()
|
|
||||||
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
|
|
||||||
.UseAdvancedExtensions()
|
|
||||||
.UseBootstrap()
|
|
||||||
.UseEmojiAndSmiley()
|
|
||||||
.UseSmartyPants()
|
|
||||||
.Build());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
|
||||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<SupportedPlatform Include="browser"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
|
||||||
<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.Components.Web" Version="7.0.10"/>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.10"/>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.10"/>
|
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0"/>
|
|
||||||
<PackageReference Include="Serilog" Version="3.0.1"/>
|
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0"/>
|
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
|
|
||||||
<PackageReference Include="SmartFormat.NET" Version="3.2.2"/>
|
|
||||||
<PackageReference Include="X10D" Version="3.2.2"/>
|
|
||||||
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
|
||||||
<PackageReference Include="ZString" Version="2.5.0"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="wwwroot\**\*"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,41 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace OliverBooth.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the middleware to configure static file options.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class OliverBoothConfigureOptions : IPostConfigureOptions<StaticFileOptions>
|
|
||||||
{
|
|
||||||
private readonly IWebHostEnvironment _environment;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="OliverBoothConfigureOptions" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="environment">The <see cref="IWebHostEnvironment" />.</param>
|
|
||||||
public OliverBoothConfigureOptions(IWebHostEnvironment environment)
|
|
||||||
{
|
|
||||||
_environment = environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void PostConfigure(string? name, StaticFileOptions options)
|
|
||||||
{
|
|
||||||
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
|
||||||
options.ContentTypeProvider ??= new FileExtensionContentTypeProvider();
|
|
||||||
|
|
||||||
if (options.FileProvider == null && _environment.WebRootFileProvider == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Missing FileProvider.");
|
|
||||||
}
|
|
||||||
|
|
||||||
options.FileProvider ??= _environment.WebRootFileProvider;
|
|
||||||
|
|
||||||
var filesProvider = new ManifestEmbeddedFileProvider(GetType().Assembly, "wwwroot");
|
|
||||||
options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
@namespace OliverBooth.Common.Pages
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth", "OliverBooth\OliverBooth.csproj", "{A58A6FA3-480C-400B-822A-3786741BF39C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth", "OliverBooth\OliverBooth.csproj", "{A58A6FA3-480C-400B-822A-3786741BF39C}"
|
||||||
EndProject
|
EndProject
|
||||||
@ -32,12 +32,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ts", "ts", "{BB9F76AC-292A-
|
|||||||
src\ts\TimeUtility.ts = src\ts\TimeUtility.ts
|
src\ts\TimeUtility.ts = src\ts\TimeUtility.ts
|
||||||
src\ts\UI.ts = src\ts\UI.ts
|
src\ts\UI.ts = src\ts\UI.ts
|
||||||
src\ts\Input.ts = src\ts\Input.ts
|
src\ts\Input.ts = src\ts\Input.ts
|
||||||
|
src\ts\BlogUrl.ts = src\ts\BlogUrl.ts
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Blog", "OliverBooth.Blog\OliverBooth.Blog.csproj", "{B114A2ED-3015-43C5-B0CE-B755B18F49D0}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{38DEB2FA-3DF4-4D37-A12D-22CAEEA3A8AB}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -48,14 +45,6 @@ Global
|
|||||||
{A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B114A2ED-3015-43C5-B0CE-B755B18F49D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B114A2ED-3015-43C5-B0CE-B755B18F49D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B114A2ED-3015-43C5-B0CE-B755B18F49D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B114A2ED-3015-43C5-B0CE-B755B18F49D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{38DEB2FA-3DF4-4D37-A12D-22CAEEA3A8AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{38DEB2FA-3DF4-4D37-A12D-22CAEEA3A8AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{38DEB2FA-3DF4-4D37-A12D-22CAEEA3A8AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{38DEB2FA-3DF4-4D37-A12D-22CAEEA3A8AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{822F528E-3CA7-4B7D-9250-BD248ADA7BAE} = {8A323E64-E41E-4780-99FD-17BF58961FB5}
|
{822F528E-3CA7-4B7D-9250-BD248ADA7BAE} = {8A323E64-E41E-4780-99FD-17BF58961FB5}
|
||||||
|
89
OliverBooth/Controllers/Blog/BlogApiController.cs
Normal file
89
OliverBooth/Controllers/Blog/BlogApiController.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
using OliverBooth.Services;
|
||||||
|
|
||||||
|
namespace OliverBooth.Controllers.Blog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a controller for the blog API.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/blog")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public sealed class BlogApiController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IBlogPostService _blogPostService;
|
||||||
|
private readonly IBlogUserService _userService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
|
||||||
|
/// <param name="userService">The <see cref="IUserService" />.</param>
|
||||||
|
public BlogApiController(IBlogPostService blogPostService, IBlogUserService userService)
|
||||||
|
{
|
||||||
|
_blogPostService = blogPostService;
|
||||||
|
_userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("count")]
|
||||||
|
public IActionResult Count()
|
||||||
|
{
|
||||||
|
return Ok(new { count = _blogPostService.GetBlogPostCount() });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("posts/{page:int?}")]
|
||||||
|
public IActionResult GetAllBlogPosts(int page = 0)
|
||||||
|
{
|
||||||
|
const int itemsPerPage = 10;
|
||||||
|
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage);
|
||||||
|
return Ok(allPosts.Select(post => CreatePostObject(post)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("author/{id:guid}")]
|
||||||
|
public IActionResult GetAuthor(Guid id)
|
||||||
|
{
|
||||||
|
if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
id = author.Id,
|
||||||
|
name = author.DisplayName,
|
||||||
|
avatarUrl = author.AvatarUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("post/{id:guid?}")]
|
||||||
|
public IActionResult GetPost(Guid id)
|
||||||
|
{
|
||||||
|
if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound();
|
||||||
|
return Ok(CreatePostObject(post, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CreatePostObject(IBlogPost post, bool includeContent = false)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
id = post.Id,
|
||||||
|
commentsEnabled = post.EnableComments,
|
||||||
|
identifier = post.GetDisqusIdentifier(),
|
||||||
|
author = post.Author.Id,
|
||||||
|
title = post.Title,
|
||||||
|
published = post.Published.ToUnixTimeSeconds(),
|
||||||
|
formattedDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
|
||||||
|
updated = post.Updated?.ToUnixTimeSeconds(),
|
||||||
|
humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
|
||||||
|
excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed),
|
||||||
|
content = includeContent ? _blogPostService.RenderPost(post) : null,
|
||||||
|
trimmed,
|
||||||
|
url = new
|
||||||
|
{
|
||||||
|
year = post.Published.ToString("yyyy"),
|
||||||
|
month = post.Published.ToString("MM"),
|
||||||
|
day = post.Published.ToString("dd"),
|
||||||
|
slug = post.Slug
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Blog.Data.Configuration;
|
using OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a session with the blog database.
|
/// Represents a session with the blog database.
|
@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using SmartFormat;
|
using SmartFormat;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
internal sealed class BlogPost : IBlogPost
|
internal sealed class BlogPost : IBlogPost
|
@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data.Configuration;
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data.Configuration;
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the configuration for the <see cref="Template" /> entity.
|
/// Represents the configuration for the <see cref="Template" /> entity.
|
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data.Configuration;
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the author of a blog post.
|
/// Represents the author of a blog post.
|
@ -1,4 +1,4 @@
|
|||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a blog post.
|
/// Represents a blog post.
|
@ -1,4 +1,4 @@
|
|||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a user which can log in to the blog.
|
/// Represents a user which can log in to the blog.
|
@ -1,6 +1,6 @@
|
|||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace OliverBooth.Data.Rss;
|
namespace OliverBooth.Data.Blog.Rss;
|
||||||
|
|
||||||
public sealed class AtomLink
|
public sealed class AtomLink
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace OliverBooth.Data.Rss;
|
namespace OliverBooth.Data.Blog.Rss;
|
||||||
|
|
||||||
public sealed class BlogChannel
|
public sealed class BlogChannel
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace OliverBooth.Data.Rss;
|
namespace OliverBooth.Data.Blog.Rss;
|
||||||
|
|
||||||
public sealed class BlogItem
|
public sealed class BlogItem
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace OliverBooth.Data.Rss;
|
namespace OliverBooth.Data.Blog.Rss;
|
||||||
|
|
||||||
[XmlRoot("rss")]
|
[XmlRoot("rss")]
|
||||||
public sealed class BlogRoot
|
public sealed class BlogRoot
|
@ -1,6 +1,4 @@
|
|||||||
using OliverBooth.Common.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a MediaWiki-style template.
|
/// Represents a MediaWiki-style template.
|
@ -3,7 +3,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Cysharp.Text;
|
using Cysharp.Text;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Data;
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a user.
|
/// Represents a user.
|
@ -1,4 +1,4 @@
|
|||||||
namespace OliverBooth.Common.Data;
|
namespace OliverBooth.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a template.
|
/// Represents a template.
|
@ -1,5 +1,3 @@
|
|||||||
using OliverBooth.Common.Data;
|
|
||||||
|
|
||||||
namespace OliverBooth.Data.Web;
|
namespace OliverBooth.Data.Web;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Data.Web;
|
|
||||||
using OliverBooth.Data.Web.Configuration;
|
using OliverBooth.Data.Web.Configuration;
|
||||||
|
|
||||||
namespace OliverBooth.Data;
|
namespace OliverBooth.Data.Web;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a session with the web database.
|
/// Represents a session with the web database.
|
@ -1,8 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
|
|
||||||
namespace OliverBooth.Common.Extensions;
|
namespace OliverBooth.Extensions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for <see cref="IWebHostBuilder" />.
|
/// Extension methods for <see cref="IWebHostBuilder" />.
|
||||||
@ -23,31 +21,35 @@ public static class WebHostBuilderExtensions
|
|||||||
return builder.UseKestrel(options =>
|
return builder.UseKestrel(options =>
|
||||||
{
|
{
|
||||||
string certPath = Environment.GetEnvironmentVariable("SSL_CERT_PATH")!;
|
string certPath = Environment.GetEnvironmentVariable("SSL_CERT_PATH")!;
|
||||||
|
if (string.IsNullOrWhiteSpace(certPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Certificate path not specified. Using HTTP");
|
||||||
|
options.ListenAnyIP(httpPort);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!File.Exists(certPath))
|
if (!File.Exists(certPath))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine("Certificate not found. Using HTTP");
|
||||||
options.ListenAnyIP(httpPort);
|
options.ListenAnyIP(httpPort);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? keyPath = Environment.GetEnvironmentVariable("SSL_KEY_PATH");
|
string? keyPath = Environment.GetEnvironmentVariable("SSL_KEY_PATH");
|
||||||
if (string.IsNullOrWhiteSpace(keyPath) || !File.Exists(keyPath)) keyPath = null;
|
if (string.IsNullOrWhiteSpace(keyPath))
|
||||||
|
|
||||||
options.ListenAnyIP(httpsPort, options =>
|
|
||||||
{
|
{
|
||||||
X509Certificate2 cert = CreateCertFromPemFile(certPath, keyPath);
|
Console.WriteLine("Certificate found, but no key provided. Using certificate only");
|
||||||
options.UseHttps(cert);
|
keyPath = null;
|
||||||
});
|
|
||||||
return;
|
|
||||||
|
|
||||||
static X509Certificate2 CreateCertFromPemFile(string certPath, string? keyPath)
|
|
||||||
{
|
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
return X509Certificate2.CreateFromPemFile(certPath, keyPath);
|
|
||||||
|
|
||||||
//workaround for windows issue https://github.com/dotnet/runtime/issues/23749#issuecomment-388231655
|
|
||||||
using var cert = X509Certificate2.CreateFromPemFile(certPath, keyPath);
|
|
||||||
return new X509Certificate2(cert.Export(X509ContentType.Pkcs12));
|
|
||||||
}
|
}
|
||||||
|
else if (!File.Exists(keyPath))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Certificate found, but the provided key was not. Using certificate only");
|
||||||
|
keyPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Using HTTPS with certificate found at {certPath}:{keyPath}");
|
||||||
|
var certificate = X509Certificate2.CreateFromPemFile(certPath, keyPath);
|
||||||
|
options.ListenAnyIP(httpsPort, configure => configure.UseHttps(certificate));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using SmartFormat.Core.Extensions;
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Formatting;
|
namespace OliverBooth.Formatting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a SmartFormat formatter that formats a date.
|
/// Represents a SmartFormat formatter that formats a date.
|
@ -1,8 +1,7 @@
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using SmartFormat.Core.Extensions;
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Formatting;
|
namespace OliverBooth.Formatting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a SmartFormat formatter that formats markdown.
|
/// Represents a SmartFormat formatter that formats markdown.
|
@ -1,8 +1,8 @@
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
using Markdig.Renderers;
|
using Markdig.Renderers;
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Markdown;
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Markdown extension that adds support for MediaWiki-style templates.
|
/// Represents a Markdown extension that adds support for MediaWiki-style templates.
|
@ -1,6 +1,6 @@
|
|||||||
using Markdig.Syntax.Inlines;
|
using Markdig.Syntax.Inlines;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Markdown;
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Markdown inline element that represents a MediaWiki-style template.
|
/// Represents a Markdown inline element that represents a MediaWiki-style template.
|
@ -2,7 +2,7 @@ using Cysharp.Text;
|
|||||||
using Markdig.Helpers;
|
using Markdig.Helpers;
|
||||||
using Markdig.Parsers;
|
using Markdig.Parsers;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Markdown;
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
|
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
|
@ -1,8 +1,8 @@
|
|||||||
using Markdig.Renderers;
|
using Markdig.Renderers;
|
||||||
using Markdig.Renderers.Html;
|
using Markdig.Renderers.Html;
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Markdown;
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements.
|
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements.
|
@ -1,4 +1,4 @@
|
|||||||
namespace OliverBooth.Blog.Middleware;
|
namespace OliverBooth.Middleware;
|
||||||
|
|
||||||
internal static class RssEndpointExtensions
|
internal static class RssEndpointExtensions
|
||||||
{
|
{
|
@ -1,10 +1,10 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Blog.Services;
|
using OliverBooth.Data.Blog.Rss;
|
||||||
using OliverBooth.Data.Rss;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Middleware;
|
namespace OliverBooth.Middleware;
|
||||||
|
|
||||||
internal sealed class RssMiddleware
|
internal sealed class RssMiddleware
|
||||||
{
|
{
|
@ -8,7 +8,22 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
|
<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.Components.Web" Version="7.0.10"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.10"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.10"/>
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0"/>
|
||||||
|
<PackageReference Include="Serilog" Version="3.0.1"/>
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0"/>
|
||||||
|
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0"/>
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0"/>
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
|
||||||
|
<PackageReference Include="SmartFormat.NET" Version="3.2.2"/>
|
||||||
|
<PackageReference Include="X10D" Version="3.2.2"/>
|
||||||
|
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
||||||
|
<PackageReference Include="ZString" Version="2.5.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@page "/{year:int}/{month:int}/{day:int}/{slug}"
|
@page "/{year:int}/{month:int}/{day:int}/{slug}"
|
||||||
@using Humanizer
|
@using Humanizer
|
||||||
@using OliverBooth.Blog.Data
|
@using OliverBooth.Data.Blog
|
||||||
@model Article
|
@model Article
|
||||||
|
|
||||||
@if (Model.Post is not { } post)
|
@if (Model.Post is not { } post)
|
||||||
@ -47,9 +47,8 @@
|
|||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<article>
|
<article data-blog-post="true" data-blog-id="@post.Id.ToString("D")">
|
||||||
@* @Html.Raw(BlogService.GetContent(post)) *@
|
<p class="text-center">Loading ...</p>
|
||||||
@Html.Raw(post.Body)
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
@ -1,9 +1,9 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Blog.Services;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Pages;
|
namespace OliverBooth.Pages.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the page model for the <c>Article</c> page.
|
/// Represents the page model for the <c>Article</c> page.
|
@ -1,9 +1,9 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Blog.Services;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Pages;
|
namespace OliverBooth.Pages.Blog;
|
||||||
|
|
||||||
[Area("blog")]
|
[Area("blog")]
|
||||||
public class Index : PageModel
|
public class Index : PageModel
|
@ -1,10 +1,10 @@
|
|||||||
using Cysharp.Text;
|
using Cysharp.Text;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Blog.Services;
|
using OliverBooth.Services;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Pages;
|
namespace OliverBooth.Pages.Blog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the page model for the <c>RawArticle</c> page.
|
/// Represents the page model for the <c>RawArticle</c> page.
|
@ -1,7 +1,9 @@
|
|||||||
using OliverBooth.Common;
|
using Markdig;
|
||||||
using OliverBooth.Common.Extensions;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Data.Web;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Extensions;
|
||||||
|
using OliverBooth.Markdown.Template;
|
||||||
|
using OliverBooth.Markdown.Timestamp;
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -15,15 +17,28 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddSerilog();
|
builder.Logging.AddSerilog();
|
||||||
|
|
||||||
builder.Services.AddMarkdownPipeline();
|
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||||
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
.Use<TimestampExtension>()
|
||||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
|
||||||
|
.UseAdvancedExtensions()
|
||||||
|
.UseBootstrap()
|
||||||
|
.UseEmojiAndSmiley()
|
||||||
|
.UseSmartyPants()
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
builder.Services.AddDbContextFactory<BlogContext>();
|
||||||
builder.Services.AddDbContextFactory<WebContext>();
|
builder.Services.AddDbContextFactory<WebContext>();
|
||||||
|
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||||
|
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||||
|
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||||
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);
|
||||||
|
|
||||||
|
if (builder.Environment.IsProduction())
|
||||||
|
{
|
||||||
builder.WebHost.AddCertificateFromEnvironment(2845, 5049);
|
builder.WebHost.AddCertificateFromEnvironment(2845, 5049);
|
||||||
|
}
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Humanizer;
|
||||||
|
using Markdig;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an implementation of <see cref="IBlogPostService" />.
|
/// Represents an implementation of <see cref="IBlogPostService" />.
|
||||||
@ -10,7 +12,8 @@ namespace OliverBooth.Blog.Services;
|
|||||||
internal sealed class BlogPostService : IBlogPostService
|
internal sealed class BlogPostService : IBlogPostService
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||||
private readonly IUserService _userService;
|
private readonly IBlogUserService _blogUserService;
|
||||||
|
private readonly MarkdownPipeline _markdownPipeline;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BlogPostService" /> class.
|
/// Initializes a new instance of the <see cref="BlogPostService" /> class.
|
||||||
@ -18,20 +21,35 @@ internal sealed class BlogPostService : IBlogPostService
|
|||||||
/// <param name="dbContextFactory">
|
/// <param name="dbContextFactory">
|
||||||
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="userService">The <see cref="IUserService" />.</param>
|
/// <param name="blogUserService">The <see cref="IBlogUserService" />.</param>
|
||||||
public BlogPostService(IDbContextFactory<BlogContext> dbContextFactory, IUserService userService)
|
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
||||||
|
public BlogPostService(IDbContextFactory<BlogContext> dbContextFactory,
|
||||||
|
IBlogUserService blogUserService,
|
||||||
|
MarkdownPipeline markdownPipeline)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_userService = userService;
|
_blogUserService = blogUserService;
|
||||||
|
_markdownPipeline = markdownPipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int GetBlogPostCount()
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
return context.BlogPosts.Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<IBlogPost> GetAllBlogPosts(int limit = -1)
|
public IReadOnlyList<IBlogPost> GetAllBlogPosts(int limit = -1)
|
||||||
{
|
{
|
||||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
return context.BlogPosts
|
IQueryable<BlogPost> ordered = context.BlogPosts.OrderByDescending(post => post.Published);
|
||||||
.OrderByDescending(post => post.Published)
|
if (limit > -1)
|
||||||
.Take(limit)
|
{
|
||||||
.AsEnumerable().Select(CacheAuthor).ToArray();
|
ordered = ordered.Take(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered.AsEnumerable().Select(CacheAuthor).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -42,22 +60,30 @@ internal sealed class BlogPostService : IBlogPostService
|
|||||||
.OrderByDescending(post => post.Published)
|
.OrderByDescending(post => post.Published)
|
||||||
.Skip(page * pageSize)
|
.Skip(page * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.AsEnumerable().Select(CacheAuthor).ToArray();
|
.ToArray().Select(CacheAuthor).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string RenderExcerpt(IBlogPost post, out bool wasTrimmed)
|
public string RenderExcerpt(IBlogPost post, out bool wasTrimmed)
|
||||||
{
|
{
|
||||||
// TODO implement excerpt trimming
|
string body = post.Body;
|
||||||
wasTrimmed = false;
|
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||||
return post.Body;
|
|
||||||
|
if (moreIndex == -1)
|
||||||
|
{
|
||||||
|
string excerpt = body.Truncate(255, "...");
|
||||||
|
wasTrimmed = body.Length > 255;
|
||||||
|
return Markdig.Markdown.ToHtml(excerpt, _markdownPipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasTrimmed = true;
|
||||||
|
return Markdig.Markdown.ToHtml(body[..moreIndex], _markdownPipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string RenderPost(IBlogPost post)
|
public string RenderPost(IBlogPost post)
|
||||||
{
|
{
|
||||||
// TODO render markdown
|
return Markdig.Markdown.ToHtml(post.Body, _markdownPipeline);
|
||||||
return post.Body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -114,7 +140,7 @@ internal sealed class BlogPostService : IBlogPostService
|
|||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_userService.TryGetUser(post.AuthorId, out IUser? user) && user is IBlogAuthor author)
|
if (_blogUserService.TryGetUser(post.AuthorId, out IUser? user) && user is IBlogAuthor author)
|
||||||
{
|
{
|
||||||
post.Author = author;
|
post.Author = author;
|
||||||
}
|
}
|
@ -1,23 +1,23 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an implementation of <see cref="IUserService" />.
|
/// Represents an implementation of <see cref="IBlogUserService" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class UserService : IUserService
|
internal sealed class BlogUserService : IBlogUserService
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserService" /> class.
|
/// Initializes a new instance of the <see cref="BlogUserService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dbContextFactory">
|
/// <param name="dbContextFactory">
|
||||||
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
||||||
/// </param>
|
/// </param>
|
||||||
public UserService(IDbContextFactory<BlogContext> dbContextFactory)
|
public BlogUserService(IDbContextFactory<BlogContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a service for managing blog posts.
|
/// Represents a service for managing blog posts.
|
||||||
@ -19,6 +19,12 @@ public interface IBlogPostService
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
IReadOnlyList<IBlogPost> GetAllBlogPosts(int limit = -1);
|
IReadOnlyList<IBlogPost> GetAllBlogPosts(int limit = -1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the total number of blog posts.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The total number of blog posts.</returns>
|
||||||
|
int GetBlogPostCount();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
|
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
|
||||||
/// returned per page.
|
/// returned per page.
|
@ -1,12 +1,12 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using OliverBooth.Blog.Data;
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
namespace OliverBooth.Blog.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a service for managing users.
|
/// Represents a service for managing users.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUserService
|
public interface IBlogUserService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to find a user with the specified ID.
|
/// Attempts to find a user with the specified ID.
|
@ -1,6 +1,6 @@
|
|||||||
using OliverBooth.Common.Markdown;
|
using OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
namespace OliverBooth.Common.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a service that renders MediaWiki-style templates.
|
/// Represents a service that renders MediaWiki-style templates.
|
@ -1,10 +1,8 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Common.Formatting;
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Common.Markdown;
|
using OliverBooth.Formatting;
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Markdown.Template;
|
||||||
using OliverBooth.Data;
|
|
||||||
using OliverBooth.Data.Web;
|
|
||||||
using SmartFormat;
|
using SmartFormat;
|
||||||
using SmartFormat.Extensions;
|
using SmartFormat.Extensions;
|
||||||
|
|
||||||
@ -16,15 +14,15 @@ namespace OliverBooth.Services;
|
|||||||
internal sealed class TemplateService : ITemplateService
|
internal sealed class TemplateService : ITemplateService
|
||||||
{
|
{
|
||||||
private static readonly Random Random = new();
|
private static readonly Random Random = new();
|
||||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
private readonly IDbContextFactory<BlogContext> _webContextFactory;
|
||||||
private readonly SmartFormatter _formatter;
|
private readonly SmartFormatter _formatter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||||
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
/// <param name="webContextFactory">The <see cref="BlogContext" /> factory.</param>
|
||||||
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<WebContext> webContextFactory)
|
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<BlogContext> webContextFactory)
|
||||||
{
|
{
|
||||||
_formatter = Smart.CreateDefaultSmartFormat();
|
_formatter = Smart.CreateDefaultSmartFormat();
|
||||||
_formatter.AddExtensions(new DefaultSource());
|
_formatter.AddExtensions(new DefaultSource());
|
||||||
@ -40,7 +38,7 @@ internal sealed class TemplateService : ITemplateService
|
|||||||
{
|
{
|
||||||
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
||||||
|
|
||||||
using WebContext webContext = _webContextFactory.CreateDbContext();
|
using BlogContext webContext = _webContextFactory.CreateDbContext();
|
||||||
Template? template = webContext.Templates.Find(templateInline.Name);
|
Template? template = webContext.Templates.Find(templateInline.Name);
|
||||||
if (template is null)
|
if (template is null)
|
||||||
{
|
{
|
||||||
|
@ -16,20 +16,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "2845:2845"
|
- "2845:2845"
|
||||||
restart: always
|
restart: always
|
||||||
|
environment:
|
||||||
oliverbooth-blog:
|
- SSL_CERT_PATH=${SSL_CERT_PATH}
|
||||||
container_name: blog.oliverbooth.dev
|
- SSL_KEY_PATH=${SSL_KEY_PATH}
|
||||||
pull_policy: build
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: OliverBooth/Dockerfile
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/log/oliverbooth/blog
|
|
||||||
target: /app/logs
|
|
||||||
- type: bind
|
|
||||||
source: /etc/oliverbooth/blog
|
|
||||||
target: /app/data
|
|
||||||
ports:
|
|
||||||
- "2846:2846"
|
|
||||||
restart: always
|
|
||||||
|
@ -2,7 +2,7 @@ import BlogPost from "./BlogPost";
|
|||||||
import Author from "./Author";
|
import Author from "./Author";
|
||||||
|
|
||||||
class API {
|
class API {
|
||||||
private static readonly BASE_URL: string = "https://api.oliverbooth.dev";
|
private static readonly BASE_URL: string = "/api";
|
||||||
private static readonly BLOG_URL: string = "/blog";
|
private static readonly BLOG_URL: string = "/blog";
|
||||||
|
|
||||||
static async getBlogPostCount(): Promise<number> {
|
static async getBlogPostCount(): Promise<number> {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import BlogUrl from "./BlogUrl";
|
||||||
|
|
||||||
class BlogPost {
|
class BlogPost {
|
||||||
private readonly _id: string;
|
private readonly _id: string;
|
||||||
private readonly _commentsEnabled: boolean;
|
private readonly _commentsEnabled: boolean;
|
||||||
@ -7,7 +9,7 @@ class BlogPost {
|
|||||||
private readonly _authorId: string;
|
private readonly _authorId: string;
|
||||||
private readonly _published: Date;
|
private readonly _published: Date;
|
||||||
private readonly _updated?: Date;
|
private readonly _updated?: Date;
|
||||||
private readonly _url: string;
|
private readonly _url: BlogUrl;
|
||||||
private readonly _trimmed: boolean;
|
private readonly _trimmed: boolean;
|
||||||
private readonly _identifier: string;
|
private readonly _identifier: string;
|
||||||
private readonly _humanizedTimestamp: string;
|
private readonly _humanizedTimestamp: string;
|
||||||
@ -22,7 +24,7 @@ class BlogPost {
|
|||||||
this._authorId = json.author;
|
this._authorId = json.author;
|
||||||
this._published = new Date(json.published * 1000);
|
this._published = new Date(json.published * 1000);
|
||||||
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
|
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
|
||||||
this._url = json.url;
|
this._url = new BlogUrl(json.url);
|
||||||
this._trimmed = json.trimmed;
|
this._trimmed = json.trimmed;
|
||||||
this._identifier = json.identifier;
|
this._identifier = json.identifier;
|
||||||
this._humanizedTimestamp = json.humanizedTimestamp;
|
this._humanizedTimestamp = json.humanizedTimestamp;
|
||||||
@ -61,7 +63,7 @@ class BlogPost {
|
|||||||
return this._updated;
|
return this._updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
get url(): string {
|
get url(): BlogUrl {
|
||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
src/ts/BlogUrl.ts
Normal file
32
src/ts/BlogUrl.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
class BlogUrl {
|
||||||
|
private readonly _year: string;
|
||||||
|
private readonly _month: string;
|
||||||
|
private readonly _day: string;
|
||||||
|
private readonly _slug: string;
|
||||||
|
|
||||||
|
constructor(json: any) {
|
||||||
|
this._year = json.year;
|
||||||
|
this._month = json.month;
|
||||||
|
this._day = json.day;
|
||||||
|
this._slug = json.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get year(): string {
|
||||||
|
return this._year;
|
||||||
|
}
|
||||||
|
|
||||||
|
get month(): string {
|
||||||
|
return this._month;
|
||||||
|
}
|
||||||
|
|
||||||
|
get day(): string {
|
||||||
|
return this._day;
|
||||||
|
}
|
||||||
|
|
||||||
|
get slug(): string {
|
||||||
|
return this._slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogUrl;
|
@ -7,6 +7,10 @@ declare const katex: any;
|
|||||||
declare const Prism: any;
|
declare const Prism: any;
|
||||||
|
|
||||||
class UI {
|
class UI {
|
||||||
|
public static get blogPost(): HTMLDivElement {
|
||||||
|
return document.querySelector("article[data-blog-post='true']");
|
||||||
|
}
|
||||||
|
|
||||||
public static get blogPostContainer(): HTMLDivElement {
|
public static get blogPostContainer(): HTMLDivElement {
|
||||||
return document.querySelector("#all-blog-posts");
|
return document.querySelector("#all-blog-posts");
|
||||||
}
|
}
|
||||||
@ -44,7 +48,7 @@ class UI {
|
|||||||
post: {
|
post: {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
excerpt: post.excerpt,
|
excerpt: post.excerpt,
|
||||||
url: post.url,
|
url: `${post.url.year}/${post.url.month}/${post.url.day}/${post.url.slug}`,
|
||||||
date: TimeUtility.formatRelativeTimestamp(post.published),
|
date: TimeUtility.formatRelativeTimestamp(post.published),
|
||||||
formattedDate: post.formattedDate,
|
formattedDate: post.formattedDate,
|
||||||
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
||||||
|
@ -34,6 +34,15 @@ declare const Prism: any;
|
|||||||
window.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "_blank");
|
window.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "_blank");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const blogPost = UI.blogPost;
|
||||||
|
if (blogPost) {
|
||||||
|
const id = blogPost.dataset.blogId;
|
||||||
|
API.getBlogPost(id).then((post) => {
|
||||||
|
blogPost.innerHTML = post.content;
|
||||||
|
UI.updateUI(blogPost);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const blogPostContainer = UI.blogPostContainer;
|
const blogPostContainer = UI.blogPostContainer;
|
||||||
if (blogPostContainer) {
|
if (blogPostContainer) {
|
||||||
const authors = [];
|
const authors = [];
|
||||||
|
Loading…
Reference in New Issue
Block a user