Last November, I switched my static site generator from CobblestoneJS (my homebrew Gulp-based one) to Statiq. There were a number of reasons, all of them still good but mainly to support what I want to do with fedran.com and my other sites.
This holiday weekend, I ended up ripping out all of the Statiq and wrote another static site generator, this time in C# and using an Entity-Component-System (ECS). I also migrated my site over to Gemini as part of the effort.
I liked many of the ideas that Statiq provided:
- Treating the website generation as code.
- Having pipelines with dependencies for generation.
- Immutable objects on the pipeline.
However, I found myself struggling with concepts. It just didn't sit well with me and I would spend two weeks trying to implement a feature and getting stuck. When I realized I had spent a month not fixing something that was bothering me because I didn't want to delve into the code, I knew it was time to change.
That isn't to say Statiq isn't bad. It just isn't for me. That's it.
About once a year, I get 4-7 days of “alone time” to do what I want. This year, I decided to work on a new static site generator that did work the way I work today and that I hoped would carry me over for the next five years or so.
One thing that Statiq did (but differently) was implemented the system as an ECS. Basically, you have a lightweight object (the “entity”) and add various components into it. Those components are what provide the features: the text content, flags to say if it is HTML or Markdown, or the path.
While Statiq had a number of these elements built-into the
Document call (basically their entity), there were a lot of assumptions that didn't always fit. Likewise,
Statiq.Web had some nice opinionated ways of handling it, including a document type (binary, text, etc), but I couldn't find an easy way to extend it.
With my ECS,
Entity is only a collection plus an integer identifier. Components can be added, removed, and replaced easily using generics to determine the type. Methods are chained together but not pseudo-English fluent (which I'm also not fond of). Entities are immutable, so all the operations return a clone of that entity with the new components added, removed, or otherwise changed. (Thanks to functional programming for some of those ideas.)
Entity entity = new(); Entity entityWithTwoComponents = entity .Set(new Uri("https://d.moonfire.us")) .Set<FileSystemInfo>(new FileInfo("/bob.txt")); Entity entityWithReplacedUri = entityWithTwoComponents .Set(new Uri("http://path-to-replace-d.moonfire.us/")); Entity entityWithOnlyUri = entityWithReplacedUri .Remove<FileSystemInfo>();
I also really like chained operations, so most of the processing looks like this:
IEnumerable<Entity> input; var onlyHtmlOutputs = input .OrderBy(x => x.Get<FileInfo>().FullPath) .ForComponents<Uri>((entity, uri) => this.Uri(entity, uri)) .WhereComponents<IsHtml>();
The whole idea is
ForComponents<T1, T2, T3> will go through the list and for all entities that have
T3, it will do the callback, otherwise it will passs it on. Likewise
WhereComponents<T1> is basically a
Where that says only the entities with the given components.
Those ideas really simplified a lot of the difficulties I had with CobblestoneJS. Overall, most of the logic “felt” right for me, so I'm really happy with the results. Plus, it is based on a far more stable package ecosystem (NuGet) and in a language I enjoy greatly (C#).
Also, the language uses Autofac as my preferred dependency injection of choice. I really like the library plus NodaTime when coding, so I went with these. It's a bit opinionated, but… only a few people ever used Cobblestone, so I'm going to assume very few are going to use this.
Once I get it cleaned up, I'll probably call the ECS “Gallium” because my original name was “Gallium Nitride” (GaN, because it's a cool name and I like what the molecule does). The static site generator would be named Nitride.
Nitride is just a multi-threaded pipeline static generator. It uses pipelines much like Statiq but based on C#'s thread control (
The rough code looks like this:
List<Entity> list = entities .Run(new IdentifyMarkdown()) .Run(new ParseYamlHeader<PageModel>()) .ForComponents<PageModel>(this.SetHandlebarsFromPageModel) .Run(this.setInstantFromComponent) .Run(this.filterOutFutureInstants) .Run(this.createCategoryIndexes) .Run(this.createTagIndexes) .ToList();
Again, DI or direct instantiate of modules, it all works the same and really ties into using Linq and C# generics. All of the pipelines are
async but most of the operations (
ParseYamlHeader) are not. But since the pipelines are, it is easy to make something
await without changing signatures.
I really like the pipelines. For my site, I have the following:
PostsPipeline: Load posts and create blog archives
PagesPipeline: Load static pages.
PagesPipeline, creates categories/tag indexes, handle filtering out future pages.
ContentPipelineand converts into bare, unstyled HTML. Also creates the RSS and Atom feeds from the bare HTML.
WebpackPipeline: Runs Webpack.
WebpackPipelineand makes it styled using handlebars.
ContentPipelineand turns the Markdown into Gemini, applies some simple styling, and generates Gemtext pages.
That's it, but I'm happy with the result because I've taken lessons learned from my previous attempts to created something that will handle Fedran's massive cross-linking and project pages, MfGames's pulling in of separate Git repositories, and also some of the more complex formatting of my new sci-fi fiction website.
I like the idea of Gemini. It is a low-overhead protocol that has almost no extra features, no cookies, and basically focused on presenting content. In my case, I really want to see all of my sites on Gemini because I think it has some significant merits, more so as I want to get away from heavily styled content written by people who like tiny fonts or don't have my color contrast issues.
To do that, I ended up taking inspiration from md2gemini and wrote a C# library that converts Markdown into Gemtext (Gemini's markup format inspired by Markdown). The end result is pretty nice, I think and I'm really happy with the results.
Of course, it meant I had to get a virtual machine to host a Gemini server next to a HTTP one, but that was going to happen sooner or later anyways.
I write a lot libraries that I think are interesting but very few people worry about. They rise up, either I stick with them or I trail off, but they always scratch my itches. On that front, the following things are left to do:
- Split out Gallium more formally into
Gallium, not sure.
- Clean up the API for
Nitrideand make sure everything is consistent. Some of the names are a bit… off, but the functionality is good.
- Break all the modules into separate Gitlab projects and set up CI for releasing.
- Document everything.
- Convert five other websites sites to use it to figure out what is missing.
I haven't found the money to get a developer's signing certificate, so I'll probably just put everything up on my public MyGet repository.