Nitride v0.3.4

Last week, I decided to work on Nitride, my static site generator written in C#. One of the outstanding tasks after I wrote it for this site to handle both HTML and Gemtext versions was to look at the various patterns that I had established and then simplify them.

Cleaning up code is good when starting with something that potentially can get large. I had used Cobblestone for almost a decade between all my websites and I fully expected Nitride to last the name. That means I need to be comfortable with the API. Of course, “comfortable with” usually means “I like it until I stop using it for three months and then I want to rewrite it all.” However, I've managed to avoid that with Cobblestone and I'm “mostly” resisting that with MfGames Writing.

Side note: I'm planning on switching MfGames Writing to configuration-as-code instead of declarative files in the future, just not sure when.

The other plan I had was to write unit tests and examples for every option to basically help document the place. This was going to be injected into https://mfgames.com/ as a project, but I realized that I was trying to do too much and I needed to break it down.

This week's effort was just pattern normalization. I've embraced using dependency injection for setting up the various pipelines, so there is no more mix cases of constructor injection and new operations; it's all constructor injection. That way, I can offload the validation logic to FluentValidators and change the signatures, such as to add logging, without breaking existing code.

public PagesMarkdownPipeline(
    ReadFiles readFiles,
    MoveToIndexPath moveToIndexPath,
    ParseYamlHeader<PageModel> parseYamlHeader,
    RemovePathPrefix removePathPrefix,
    IdentifyMarkdownFromPath identifyMarkdownFromPath)
{
    this.readFiles = readFiles.WithPattern("/pages/**/*");
    // ...
}

As you can tell from the example above, I'm also using the fluent pattern a lot more to handle configuration. I think this works since the initial object has mostly sane defaults and validations for the rest of them while the rest can be customized in the constructor.

I also decided to keep the configuration in the constructor of the pipeline. That makes the actual operation of the pipeline much easier to understand.

/// <inheritdoc />
public override Task<IEnumerable<Entity>> RunAsync(
    IEnumerable<Entity> entities)
{
    IEnumerable<Entity> files = this.readFiles.Run()
        .Run(this.removePathPrefix)
        .Run(this.moveToIndexPath)
        .Run(this.identifyMarkdownFromPath)
        .Run(this.parseYamlHeader)
        .ForEachEntity<PageModel>(this.SetInstantFromPageModel);

    return Task.FromResult(files);
}

As you can tell, it also pulls from my history with gulp a lot since I think the chained .Run() methods works. The ForEachEntity is from Gallium, my simply entity component system that I wrote for this (but also want to use in other places).

The other thing in the above example is that the pipelines (but not necessarily the individual operations) are async. I figured this gives a good mix of allowing async operations at the pipeline level but in most cases, it ends up being fairly synchronous.

Thanks to the C#'s enumerations, the actual value sometimes isn't run until after the pipeline “runs”. There are exceptions, like when I need to create a category pages which requires knowing every other page in the website. Those are resolved immediately in the pipeline to process that, and then returned.

Overall, it is moving forward nicely. I still see a lot of shuffling of method names and I want to break up the index creation pages (the annual, monthly, and daily blog listings) into separate components. But, once I get those, I should be ready to start converting the next site, https://moonfire.us/ and then into the next “big” one, https://mfgames.com/. The “end goal,” as it were, is converting https://fedran.com/.

Metadata

Categories:

Tags: