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 srcDir = 'src';
|
||||
const destDir = 'OliverBooth.Common/wwwroot';
|
||||
const destDir = 'OliverBooth/wwwroot';
|
||||
|
||||
function compileSCSS() {
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth", "OliverBooth\OliverBooth.csproj", "{A58A6FA3-480C-400B-822A-3786741BF39C}"
|
||||
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\UI.ts = src\ts\UI.ts
|
||||
src\ts\Input.ts = src\ts\Input.ts
|
||||
src\ts\BlogUrl.ts = src\ts\BlogUrl.ts
|
||||
EndProjectSection
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{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 OliverBooth.Blog.Data.Configuration;
|
||||
using OliverBooth.Data.Blog.Configuration;
|
||||
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a session with the blog database.
|
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SmartFormat;
|
||||
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed class BlogPost : IBlogPost
|
@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace OliverBooth.Blog.Data.Configuration;
|
||||
namespace OliverBooth.Data.Blog.Configuration;
|
||||
|
||||
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace OliverBooth.Blog.Data.Configuration;
|
||||
namespace OliverBooth.Data.Blog.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the <see cref="Template" /> entity.
|
@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace OliverBooth.Blog.Data.Configuration;
|
||||
namespace OliverBooth.Data.Blog.Configuration;
|
||||
|
||||
internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the author of a blog post.
|
@ -1,4 +1,4 @@
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a blog post.
|
@ -1,4 +1,4 @@
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user which can log in to the blog.
|
@ -1,6 +1,6 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
namespace OliverBooth.Data.Blog.Rss;
|
||||
|
||||
public sealed class AtomLink
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
namespace OliverBooth.Data.Blog.Rss;
|
||||
|
||||
public sealed class BlogChannel
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
namespace OliverBooth.Data.Blog.Rss;
|
||||
|
||||
public sealed class BlogItem
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
namespace OliverBooth.Data.Blog.Rss;
|
||||
|
||||
[XmlRoot("rss")]
|
||||
public sealed class BlogRoot
|
@ -1,6 +1,4 @@
|
||||
using OliverBooth.Common.Data;
|
||||
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a MediaWiki-style template.
|
@ -3,7 +3,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Cysharp.Text;
|
||||
|
||||
namespace OliverBooth.Blog.Data;
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user.
|
@ -1,4 +1,4 @@
|
||||
namespace OliverBooth.Common.Data;
|
||||
namespace OliverBooth.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a template.
|
@ -1,5 +1,3 @@
|
||||
using OliverBooth.Common.Data;
|
||||
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,8 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Data.Web.Configuration;
|
||||
|
||||
namespace OliverBooth.Data;
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a session with the web database.
|
@ -1,8 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace OliverBooth.Common.Extensions;
|
||||
namespace OliverBooth.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IWebHostBuilder" />.
|
||||
@ -23,31 +21,35 @@ public static class WebHostBuilderExtensions
|
||||
return builder.UseKestrel(options =>
|
||||
{
|
||||
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))
|
||||
{
|
||||
Console.Error.WriteLine("Certificate not found. Using HTTP");
|
||||
options.ListenAnyIP(httpPort);
|
||||
return;
|
||||
}
|
||||
|
||||
string? keyPath = Environment.GetEnvironmentVariable("SSL_KEY_PATH");
|
||||
if (string.IsNullOrWhiteSpace(keyPath) || !File.Exists(keyPath)) keyPath = null;
|
||||
|
||||
options.ListenAnyIP(httpsPort, options =>
|
||||
if (string.IsNullOrWhiteSpace(keyPath))
|
||||
{
|
||||
X509Certificate2 cert = CreateCertFromPemFile(certPath, keyPath);
|
||||
options.UseHttps(cert);
|
||||
});
|
||||
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));
|
||||
Console.WriteLine("Certificate found, but no key provided. Using certificate only");
|
||||
keyPath = null;
|
||||
}
|
||||
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 SmartFormat.Core.Extensions;
|
||||
|
||||
namespace OliverBooth.Common.Formatting;
|
||||
namespace OliverBooth.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SmartFormat formatter that formats a date.
|
@ -1,8 +1,7 @@
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SmartFormat.Core.Extensions;
|
||||
|
||||
namespace OliverBooth.Common.Formatting;
|
||||
namespace OliverBooth.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a SmartFormat formatter that formats markdown.
|
@ -1,8 +1,8 @@
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using OliverBooth.Common.Services;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Common.Markdown;
|
||||
namespace OliverBooth.Markdown.Template;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Markdown extension that adds support for MediaWiki-style templates.
|
@ -1,6 +1,6 @@
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace OliverBooth.Common.Markdown;
|
||||
namespace OliverBooth.Markdown.Template;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Markdown inline element that represents a MediaWiki-style template.
|
@ -2,7 +2,7 @@ using Cysharp.Text;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
|
||||
namespace OliverBooth.Common.Markdown;
|
||||
namespace OliverBooth.Markdown.Template;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
|
@ -1,8 +1,8 @@
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using OliverBooth.Common.Services;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Common.Markdown;
|
||||
namespace OliverBooth.Markdown.Template;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
@ -1,10 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Xml.Serialization;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Blog.Services;
|
||||
using OliverBooth.Data.Rss;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Blog.Rss;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Blog.Middleware;
|
||||
namespace OliverBooth.Middleware;
|
||||
|
||||
internal sealed class RssMiddleware
|
||||
{
|
@ -8,7 +8,22 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@page "/{year:int}/{month:int}/{day:int}/{slug}"
|
||||
@using Humanizer
|
||||
@using OliverBooth.Blog.Data
|
||||
@using OliverBooth.Data.Blog
|
||||
@model Article
|
||||
|
||||
@if (Model.Post is not { } post)
|
||||
@ -47,9 +47,8 @@
|
||||
}
|
||||
</p>
|
||||
|
||||
<article>
|
||||
@* @Html.Raw(BlogService.GetContent(post)) *@
|
||||
@Html.Raw(post.Body)
|
||||
<article data-blog-post="true" data-blog-id="@post.Id.ToString("D")">
|
||||
<p class="text-center">Loading ...</p>
|
||||
</article>
|
||||
|
||||
<hr>
|
@ -1,9 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Blog.Services;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Blog.Pages;
|
||||
namespace OliverBooth.Pages.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the page model for the <c>Article</c> page.
|
@ -1,9 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Blog.Services;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Blog.Pages;
|
||||
namespace OliverBooth.Pages.Blog;
|
||||
|
||||
[Area("blog")]
|
||||
public class Index : PageModel
|
@ -1,10 +1,10 @@
|
||||
using Cysharp.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Blog.Services;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Blog.Pages;
|
||||
namespace OliverBooth.Pages.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the page model for the <c>RawArticle</c> page.
|
@ -1,7 +1,9 @@
|
||||
using OliverBooth.Common;
|
||||
using OliverBooth.Common.Extensions;
|
||||
using OliverBooth.Common.Services;
|
||||
using OliverBooth.Data;
|
||||
using Markdig;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Extensions;
|
||||
using OliverBooth.Markdown.Template;
|
||||
using OliverBooth.Markdown.Timestamp;
|
||||
using OliverBooth.Services;
|
||||
using Serilog;
|
||||
|
||||
@ -15,15 +17,28 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSerilog();
|
||||
|
||||
builder.Services.AddMarkdownPipeline();
|
||||
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||
.Use<TimestampExtension>()
|
||||
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
|
||||
.UseAdvancedExtensions()
|
||||
.UseBootstrap()
|
||||
.UseEmojiAndSmiley()
|
||||
.UseSmartyPants()
|
||||
.Build());
|
||||
|
||||
builder.Services.AddDbContextFactory<BlogContext>();
|
||||
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.AddControllersWithViews();
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
|
||||
if (builder.Environment.IsProduction())
|
||||
{
|
||||
builder.WebHost.AddCertificateFromEnvironment(2845, 5049);
|
||||
}
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Humanizer;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
|
||||
namespace OliverBooth.Blog.Services;
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an implementation of <see cref="IBlogPostService" />.
|
||||
@ -10,7 +12,8 @@ namespace OliverBooth.Blog.Services;
|
||||
internal sealed class BlogPostService : IBlogPostService
|
||||
{
|
||||
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IBlogUserService _blogUserService;
|
||||
private readonly MarkdownPipeline _markdownPipeline;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlogPostService" /> class.
|
||||
@ -18,20 +21,35 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
/// <param name="dbContextFactory">
|
||||
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
||||
/// </param>
|
||||
/// <param name="userService">The <see cref="IUserService" />.</param>
|
||||
public BlogPostService(IDbContextFactory<BlogContext> dbContextFactory, IUserService userService)
|
||||
/// <param name="blogUserService">The <see cref="IBlogUserService" />.</param>
|
||||
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
||||
public BlogPostService(IDbContextFactory<BlogContext> dbContextFactory,
|
||||
IBlogUserService blogUserService,
|
||||
MarkdownPipeline markdownPipeline)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.BlogPosts
|
||||
.OrderByDescending(post => post.Published)
|
||||
.Take(limit)
|
||||
.AsEnumerable().Select(CacheAuthor).ToArray();
|
||||
IQueryable<BlogPost> ordered = context.BlogPosts.OrderByDescending(post => post.Published);
|
||||
if (limit > -1)
|
||||
{
|
||||
ordered = ordered.Take(limit);
|
||||
}
|
||||
|
||||
return ordered.AsEnumerable().Select(CacheAuthor).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -42,22 +60,30 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
.OrderByDescending(post => post.Published)
|
||||
.Skip(page * pageSize)
|
||||
.Take(pageSize)
|
||||
.AsEnumerable().Select(CacheAuthor).ToArray();
|
||||
.ToArray().Select(CacheAuthor).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RenderExcerpt(IBlogPost post, out bool wasTrimmed)
|
||||
{
|
||||
// TODO implement excerpt trimming
|
||||
wasTrimmed = false;
|
||||
return post.Body;
|
||||
string body = post.Body;
|
||||
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||
|
||||
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 />
|
||||
public string RenderPost(IBlogPost post)
|
||||
{
|
||||
// TODO render markdown
|
||||
return post.Body;
|
||||
return Markdig.Markdown.ToHtml(post.Body, _markdownPipeline);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -114,7 +140,7 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
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;
|
||||
}
|
@ -1,23 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
|
||||
namespace OliverBooth.Blog.Services;
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an implementation of <see cref="IUserService" />.
|
||||
/// Represents an implementation of <see cref="IBlogUserService" />.
|
||||
/// </summary>
|
||||
internal sealed class UserService : IUserService
|
||||
internal sealed class BlogUserService : IBlogUserService
|
||||
{
|
||||
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserService" /> class.
|
||||
/// Initializes a new instance of the <see cref="BlogUserService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">
|
||||
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
||||
/// </param>
|
||||
public UserService(IDbContextFactory<BlogContext> dbContextFactory)
|
||||
public BlogUserService(IDbContextFactory<BlogContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
|
||||
namespace OliverBooth.Blog.Services;
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service for managing blog posts.
|
||||
@ -19,6 +19,12 @@ public interface IBlogPostService
|
||||
/// </remarks>
|
||||
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>
|
||||
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
|
||||
/// returned per page.
|
@ -1,12 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using OliverBooth.Blog.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
|
||||
namespace OliverBooth.Blog.Services;
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service for managing users.
|
||||
/// </summary>
|
||||
public interface IUserService
|
||||
public interface IBlogUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Represents a service that renders MediaWiki-style templates.
|
@ -1,10 +1,8 @@
|
||||
using System.Buffers.Binary;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Common.Formatting;
|
||||
using OliverBooth.Common.Markdown;
|
||||
using OliverBooth.Common.Services;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Formatting;
|
||||
using OliverBooth.Markdown.Template;
|
||||
using SmartFormat;
|
||||
using SmartFormat.Extensions;
|
||||
|
||||
@ -16,15 +14,15 @@ namespace OliverBooth.Services;
|
||||
internal sealed class TemplateService : ITemplateService
|
||||
{
|
||||
private static readonly Random Random = new();
|
||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||
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="WebContext" /> factory.</param>
|
||||
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<WebContext> webContextFactory)
|
||||
/// <param name="webContextFactory">The <see cref="BlogContext" /> factory.</param>
|
||||
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<BlogContext> webContextFactory)
|
||||
{
|
||||
_formatter = Smart.CreateDefaultSmartFormat();
|
||||
_formatter.AddExtensions(new DefaultSource());
|
||||
@ -40,7 +38,7 @@ internal sealed class TemplateService : ITemplateService
|
||||
{
|
||||
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);
|
||||
if (template is null)
|
||||
{
|
||||
|
@ -16,20 +16,6 @@ services:
|
||||
ports:
|
||||
- "2845:2845"
|
||||
restart: always
|
||||
|
||||
oliverbooth-blog:
|
||||
container_name: blog.oliverbooth.dev
|
||||
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
|
||||
environment:
|
||||
- SSL_CERT_PATH=${SSL_CERT_PATH}
|
||||
- SSL_KEY_PATH=${SSL_KEY_PATH}
|
||||
|
@ -2,7 +2,7 @@ import BlogPost from "./BlogPost";
|
||||
import Author from "./Author";
|
||||
|
||||
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";
|
||||
|
||||
static async getBlogPostCount(): Promise<number> {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import BlogUrl from "./BlogUrl";
|
||||
|
||||
class BlogPost {
|
||||
private readonly _id: string;
|
||||
private readonly _commentsEnabled: boolean;
|
||||
@ -7,7 +9,7 @@ class BlogPost {
|
||||
private readonly _authorId: string;
|
||||
private readonly _published: Date;
|
||||
private readonly _updated?: Date;
|
||||
private readonly _url: string;
|
||||
private readonly _url: BlogUrl;
|
||||
private readonly _trimmed: boolean;
|
||||
private readonly _identifier: string;
|
||||
private readonly _humanizedTimestamp: string;
|
||||
@ -22,7 +24,7 @@ class BlogPost {
|
||||
this._authorId = json.author;
|
||||
this._published = new Date(json.published * 1000);
|
||||
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._identifier = json.identifier;
|
||||
this._humanizedTimestamp = json.humanizedTimestamp;
|
||||
@ -61,7 +63,7 @@ class BlogPost {
|
||||
return this._updated;
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
get url(): BlogUrl {
|
||||
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;
|
||||
|
||||
class UI {
|
||||
public static get blogPost(): HTMLDivElement {
|
||||
return document.querySelector("article[data-blog-post='true']");
|
||||
}
|
||||
|
||||
public static get blogPostContainer(): HTMLDivElement {
|
||||
return document.querySelector("#all-blog-posts");
|
||||
}
|
||||
@ -44,7 +48,7 @@ class UI {
|
||||
post: {
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
url: post.url,
|
||||
url: `${post.url.year}/${post.url.month}/${post.url.day}/${post.url.slug}`,
|
||||
date: TimeUtility.formatRelativeTimestamp(post.published),
|
||||
formattedDate: post.formattedDate,
|
||||
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");
|
||||
});
|
||||
|
||||
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;
|
||||
if (blogPostContainer) {
|
||||
const authors = [];
|
||||
|
Loading…
Reference in New Issue
Block a user