feat: add blog feed (#3)

* chore: begin working on post service

* chore: add debug assets

* chore: add whitelist words

* feat: add prism for syntax highlighting

* feat: updated favicon

* feat: add blog feed

* fix: add link to written by image

* feat: add custom error pages

* fix: focus on h2 since h1 is always the journal site title

* feat: use status code pages with redirects

* fix: add discernible text to link

* feat: add support for light and dark theme

* fix: wrap body with main tag

* fix: style main element in pages to take up full width of container

* fix: add heading to home page

* feat: add not-found-corgi image

* chore: add robots.txt file

* chore: clean up place holder posts

* fix: namespace header in layouts

* fix: use logger instead of console

* fix: use new text context in each test when registering services

* chore: update workflow to debug failing tests

* chore: install playwright deps

* fix: extend expect timeout

* fix: extend expect timeout

* fix: capture traces

* fix: capture traces

* fix: index file name
This commit is contained in:
Stevan Freeborn
2024-04-10 01:28:38 -05:00
committed by GitHub
parent d25e5d4944
commit c4795dfdae
42 changed files with 16676 additions and 63 deletions
+108
View File
@@ -0,0 +1,108 @@
namespace Blog.Posts;
class FilePostService(
IOptions<FilePostServiceOptions> options,
IFileSystem fileSystem,
ILogger<FilePostService> logger
) : IPostService
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
};
private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UsePrism()
.Build();
private const string MetaFence = "meta";
private const string IndexFile = "index.md";
private readonly FilePostServiceOptions _options = options.Value;
private readonly IFileSystem _fileSystem = fileSystem;
private readonly ILogger<FilePostService> _logger = logger;
private static (string? metadata, MarkdownDocument document) ParsePost(string postText)
{
var markDoc = Markdown.Parse(postText, MarkdownPipeline);
var postMetadata = markDoc
.Where(
x =>
x is FencedCodeBlock fencedCodeBlock &&
fencedCodeBlock.Arguments is not null &&
fencedCodeBlock.Arguments.Contains(MetaFence)
)
.Select(x => x as FencedCodeBlock)
.FirstOrDefault();
if (postMetadata is not null)
{
markDoc.Remove(postMetadata);
}
var metaContent = postMetadata?.Lines.ToString();
return (metaContent, markDoc);
}
public async Task<List<Post>> GetPostsAsync()
{
var posts = new List<Post>();
if (_fileSystem.Directory.Exists(_options.PostsDirectory) is false)
{
return posts;
}
var subDirectories = _fileSystem.Directory.GetDirectories(_options.PostsDirectory);
if (subDirectories.Length is 0)
{
return posts;
}
foreach (var subDirectory in subDirectories)
{
try
{
var indexFilePath = _fileSystem.Path.Combine(subDirectory, IndexFile);
if (_fileSystem.File.Exists(indexFilePath) is false)
{
continue;
}
var postText = await _fileSystem.File.ReadAllTextAsync(indexFilePath);
var (metaContent, document) = ParsePost(postText);
if (metaContent is null || document.Count is 0)
{
continue;
}
var post = JsonSerializer.Deserialize<Post>(metaContent, JsonSerializerOptions);
if (post is null || post.IsPublished is false)
{
continue;
}
var finalPost = post with
{
Slug = _fileSystem.Path.GetFileName(subDirectory),
};
posts.Add(finalPost);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while processing post in {SubDirectory}", subDirectory);
}
}
return [.. posts.OrderByDescending(x => x.PublishedAt)];
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Blog.Posts;
class FilePostServiceOptions
{
public string PostsDirectory { get; set; } = string.Empty;
}
class FilePostServiceOptionsSetup(IConfiguration configuration) : IConfigureOptions<FilePostServiceOptions>
{
private const string SectionName = nameof(FilePostServiceOptions);
private readonly IConfiguration _configuration = configuration;
public void Configure(FilePostServiceOptions options)
{
_configuration.GetSection(SectionName).Bind(options);
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Blog.Posts;
interface IPostService
{
Task<List<Post>> GetPostsAsync();
}
+15
View File
@@ -0,0 +1,15 @@
namespace Blog.Posts;
record Post
{
public string Title { get; init; } = string.Empty;
public string Lead { get; init; } = string.Empty;
public bool IsPublished { get; init; } = false;
public DateTime PublishedAt { get; init; }
public string Slug { get; init; } = string.Empty;
}
record PostWithContent : Post
{
public string Content { get; init; } = string.Empty;
}