When I decided to restart my blog a month ago, I wanted to move from Blogspot and use this opportunity to try some new technologies. Static website generators always looked like a very interesting technology and my blog seemed like an ideal candidate for the first try. There are many static website generators available, the most prominent ones are Jekyll, Hugo or Next. But my eye got caught by another one - Wyam. It has nice clear documentation, seems to be well maintained and it is written in C#, so when I need, I can dive into the source code without any problems.

Wyam

Wyam's website describes it as

A highly modular and extremely configurable static content generator and toolkit.

The process of output generation is described by the configuration file. The configuration file defines multiple pipelines and each pipeline consists of several modules. A module is a small component that takes input documents, does something based on those documents (possibly transforming them), and outputs documents as a result of whatever operation was performed. See the documentation for more details.

Blog recipe

Wyam provides pre-built configurations for several types of websites called recipes. One of them is the Blog recipe. It provides almost all functionalities I wanted, so I decided to use it for my blog.

The behaviour of the recipe can by altered by settings in the configuration file:

Settings[Keys.Host] = "blog.kabrt.cz";
Settings[BlogKeys.Title] = "Lukas Kabrt";
Settings[BlogKeys.Description] = "Lukas Kabrt";

Settings[BlogKeys.CaseInsensitiveTags] = true;
Settings[BlogKeys.IndexFullPosts] = false;

I was able to customize the recipe with the settings to my needs, but when I wanted to deploy the generated files to Azure I found a problem. Azure static website allows you to define a single default document e.g. index.html, but the Blog recipe generates HTML files with other names.

Generated filenames
Filenames generated by the Blog recipe.

It worked, I was able to access posts with the full path e.g. /posts/2018-11-serverless-asp-net-mvc.html but I couldn't use "nice URL" /posts/2018-11-serverless-asp-net-mvc. What I needed was to change the structure of generated HTML files to something like this:

Generated filenames
Filenames generated by the Blog recipe.

This can't be archived with the recipe settings, so I had to customize the recipe pipelines. It turned out, that it isn't a big problem, because Wyam is really very flexible.

Recipe customization

I needed to change output paths for pages, posts and tag lists. I took several different approaches:

Posts

For blog posts, I inserted a new module to the RenderBlogPosts pipeline, that sets correct WritePath meta information to the post. This information is used later in the pipeline by the WriteFile module when saving the actual files.

var renderBlogsPipeline = Pipelines[Blog.RenderBlogPosts];
var renderBlogsWriteFileModuleIndex = renderBlogsPipeline.IndexOf("WriteFiles");
renderBlogsPipeline.Insert(renderBlogsWriteFileModuleIndex,
    new Meta(Keys.WritePath, (doc, ctx) => doc.Get<string>(Keys.RelativeFilePath).EndsWith("index.html") ? doc.Get<string>(Keys.RelativeFilePath) : doc.Get<string>(Keys.RelativeFileDir) + "/" + System.IO.Path.GetFileNameWithoutExtension(doc.Get<string>(Keys.RelativeFilePath)) + "/index.html")
);

Tag lists

For tag lists, I used another approach ... I removed the whole Tags pipeline and reinserted it with different settings.

int tagsIndex = Pipelines.IndexOf(Blog.Tags);
Pipelines.Remove(Blog.Tags);
Pipelines.Insert(tagsIndex, Blog.Tags, new Wyam.Web.Pipelines.Archive(
    Blog.Tags,
    new Wyam.Web.Pipelines.ArchiveSettings
    {
        Pipelines = new string[] { Blog.BlogPosts },
        TemplateFile = ctx => "_Tag.cshtml",
        Layout = "/_Layout.cshtml",
        Group = (doc, ctx) => doc.List<string>(BlogKeys.Tags),
        CaseInsensitiveGroupComparer = ctx => ctx.Bool(BlogKeys.CaseInsensitiveTags),
        PageSize = ctx => ctx.Get(BlogKeys.TagPageSize, int.MaxValue),
        Title = (doc, ctx) => doc.String(Keys.GroupKey),
        RelativePath = (doc, ctx) => $"tags/{doc.String(Keys.GroupKey).ToLower()}/index.html",
        GroupDocumentsMetadataKey = BlogKeys.Posts,
        GroupKeyMetadataKey = BlogKeys.Tag
    }));

Pages

For pages, I chose yet another option and reimplement the whole RenderPages pipeline, because I not only wanted to change names of generated files but also change the Razor layout for the pages.

int renderPagesIndex = Pipelines.IndexOf(Blog.RenderPages);
Pipelines.Remove(Blog.RenderPages);
Pipelines.Insert(renderPagesIndex, Blog.RenderPages,
    new ModuleCollection() {
        Documents().FromPipelines(new string[] { Blog.Pages }),
        new Flatten()
    },
    new Razor().WithLayout((doc, ctx) => "/_PageLayout.cshtml"),
    new Headings(),
    new Meta(Keys.WritePath, (doc, ctx) => {
        var filePath = doc.Get<string>(Keys.RelativeFilePath);
        var preserveFilename = doc.Get<bool>("PreserveFilename");

        return preserveFilename || filePath.EndsWith("index.html") ? filePath : doc.Get<string>(Keys.RelativeFileDir) + "/" + System.IO.Path.GetFileNameWithoutExtension(filePath) + "/index.html";
    }),
    new WriteFiles(),
    new Sort((x, y) => Comparer<string>.Default.Compare(x.String(Keys.Title), y.String(Keys.Title)))
);

Result

With the described changes Wyam generates the file structure, Azure static websites need to support "nice URLs" ... as you can see on my blog. You can find the whole config.wyam file in the GitHub repository.

Once I grasped the Wyam's philosophy, the customization was a nice experience and it gave me the confidence to try Wyam on some more complex project.