Moving on to Wyam

Published on Saturday, April 20, 2019

Background

I decided as the blog is mostly for myself to remember short snippets and information gathered during projects and reading sessions that I needed to make the blog easier to update and cheaper to run as I don’t want to fork out $10 every month for something I rarely use.

Exploring alternatives

One alternative was to completely close it down and move to a document based solution instead. Something like Dropbox or Google Drive to manage everything. However I like to have something public to display even if it’s not something huge. So I decided to check for alternatives to my hosting in Azure, which is great but a little pricy for something as simple as a personal blog.

I looked around and found Wordpress hosting as a cheap alternative but I’m not a big fan of completely relinquishing both my content and my hosting to someone else as I want some redundancy to be sure my content is kept safe.

So after some research I ended up going with Wyam which I first found through Hanselman's great blog over at hanselman.com (source).

I started looking through the documentation for Wyam and also went through a couple of alternative site generators such as Hugo, but I ended up deciding to go through with Wyam.

Wyam

Wyam is a static site generator built in C#/.NET which, in my case, starts with number of Markdown files (which are the blog posts) and a configuration to generate a static website.

Installing Wyam

Head over to wyam.io for the quick install information, super easy to install through the dotnet interface if you already have dotnet installed.

Getting started

I felt that the documentation for Wyam was a little bit sparse with working examples and how to configure everything when using a “recipe” together with some custom configuration. So after some Googling and some Github exploration I found two bloggers using Wyam as base for their websites.

These two repositories served as a good base for my new Wyam based blog.

Initializing the site

The first step is to initialize the Wyam site by running the following command, this command sets the site as a blog instance and also sets the theme to use for the blog. Css overrides may be added later to change the styling of the blog. It’s also possible to change the generation behavior in the configuration.

wyam --recipe Blog --theme CleanBlog

Configuration

After initialization of the site I the configuration is the next step to go to, The configuration of my blog can be split into three parts (all parts are located in the same config.wyam file and in the order they are displayed in this post).

The first part - Blog settings

The initial settings consists of defining the recipe to use for the site and setting up the defaul configuration for the blog.

#recipe Blog
// Customize your settings and add new ones here
Settings[Keys.Host] = "nicklasgavelin.se";
Settings[BlogKeys.Title] = "Nicklas Gavelin";
Settings[BlogKeys.Description] = "";
Settings[BlogKeys.Image] = "/images/background.jpeg";
Settings[BlogKeys.IndexPageSize] = 10;
Settings[BlogKeys.IndexPaging] = true;
Settings[BlogKeys.GenerateArchive] = false;

The image set for the “BlogKeys.Image” can be overridden in a single post to change the post image as following

Title: Some post title
Image: /images/post-background.jpg
---

The second part - Global settings

I didn’t find any way of defining custom settings that could be used in the Razor views used to add post footers, head scripts and replace other parts of the pre-defined theme templates.

So after some trial and error I ended up with a global static class definition that defines the settings I need to prevent any hard-coded values in the views.

// Custom settings that are retrievable in the views
public static class CustomSettings {
  public const string DisqusShortName = "nicklasgavelin";
  public const string DisqusIdentifierKey = "disqus_identifier";
}

The example above shows the post-footer disqus short-name setting that are used to add comments to the blog posts.

The file for the javascript code is located in the input/_PostFooter.cshtml file. This file I added manually after installing Wyam and initializing the blog.

<div id="disqus_thread"></div>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
    var disqus_shortname = '@CustomSettings.DisqusShortName';
    var disqus_identifier = '@(Model.String(CustomSettings.DisqusIdentifierKey) ?? Model.FilePath(Keys.RelativeFilePath).FileNameWithoutExtension.FullPath)';
    var disqus_title = '@Model.String(BlogKeys.Title)';
    var disqus_url = '@Context.GetLink(Model, true)';
    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
    
    (function () {
        var s = document.createElement('script'); s.async = true;
        s.type = 'text/javascript';
        s.src = '//' + disqus_shortname + '.disqus.com/count.js';
        (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
    }());
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>

The third part - Url updates

I also wanted to customize the url for each blog entry to make it easier to find out in the url when the blog post was published. The way I decided to format the url was with "year/blogpost-title".

The code below managing the blog url was found at a github issue discussion https://github.com/Wyamio/Wyam/issues/612

The “BlogPosts” pipeline is added by the Blog recipe and we will have to append the custom steps.

Pipelines["BlogPosts"].Append(
  // Custom blog url with year /yyyy/postname
  Meta(Keys.RelativeFilePath, (doc, ctx) =>
  {
    string validChars = "abcdefghijklmnopqrstuvwxyz0123456789-+.";
    DateTime? publishedDate = doc.Get<DateTime?>(BlogKeys.Published, DateTime.Now);
    string slug = doc.Get<string>("slug", ""); 
    
    string fileName;
    if(string.IsNullOrEmpty(slug))
    {
      fileName = doc.FilePath(Keys.SourceFileName).ChangeExtension("html").FullPath;
    }
    else
    {
        fileName = new FilePath(slug).AppendExtension("html").FullPath; 
    }

    // Remove any invalid characters and replace them with "-" to prevent the url from being inaccessible
    fileName = fileName.ToLower();
    var name = "";
    for (var i = 0; i < fileName.Length; i++) {
      name += validChars.Contains(fileName[i]) ? fileName[i].ToString() : "-";
    }

    var newPostsPath = ctx.DirectoryPath(BlogKeys.PostsPath).GetRelativePath((DirectoryPath)"../blog");
    return $"{newPostsPath.FullPath}/{publishedDate:yyyy}/{name}";
  }),
);

The fourth part - Custom disqus identifier

I also wanted a customized identifier for disqus to make it easier in the future if I want to change the generator for the website. I decided to use blog path generated above without the .html part. The code part below was added directly after the blog url generator Meta part.

Meta(CustomSettings.DisqusIdentifierKey, (doc, ctx) => {
  string slug = doc.Get<string>("slug", ""); 
  return new FilePath(doc.Get<string>(Keys.RelativeFilePath, slug)).ChangeExtension("").FullPath.Trim('.');
})

Custom CSS

After a bit of tinkering I decided to add a few custom sections and update the code styling that is generated by highlight.js.

Theme overrides

Theme overrides are easy to add if you only want a override.css file, but I decided to go with a .less file instead and it seems like it’s not possible to add a override.less file to the /input/assets/css/ folder as that will only generate an empty file instead of the desired file. So instead create a theme.less file in the /input/assets/css folder and that generated a complete theme.css file.

Folder tree for theme.scss

After creating the css file you can add the inclusion of the generated css file by creating a input/_Head.cshtml file containing the following. The code in the _Head.cshtml file will be added to all <head> sections of all the generated .html files.

<link rel="stylesheet" href="/assets/css/theme.css">

Custom Highlight.js theme

To create a more custom feel for the code sections in the blog posts you can select a custom highlight.js theme by downloading a theme from highlight.js github and putting it in a file called highlight.css in the input/assets/css folder.

Folder tree for highlight.js theme

Running a preview site

You can start a live preview of the site that has live reloading after updating the configuration or any post files.

wyam -p -w

Build the site

When you’ve created your posts and information and are happy with the preview results you can generate the final site by running the following command.

wyam build

The site will be generated to the output folder and these files may be uploaded to your hosting of choice.

Automatic deployment

There are several options for deployment of the generated site.

Github Pages (Free)

The first option I went with was to upload the generated output folder to Github and configuring the repository to use Github pages. This option is easy and fast, configuration, domain update and testing only took about 1-2 hours total after having started.

However as I have all my repositories and build settings setup in Azure DevOps I decided to remove the Github repository and instead move everything to Azure DevOps and look for another hosting alternative.

Azure DevOps (Free) + Netlify (Free)

A combination I found after some researching is to combine Azure DevOps with the free hosting of Netlify. Netlify exposes an API that can be used to push code from Azure DevOps to Netlify and directly publish it as a website. The free plan on Netlify also supports custom domain names and free SSL through Let’s Encrypt.

I started by committing all the code for the Wyam site to Azure DevOps as a new repository. This commit included the configuration files and the input folder. The output folder was added to the .gitignore file as this will be built in Azure DevOps pipelines and is not needed in the commit.

To simplify the building pipeline and also keep everything under source control I found a great yaml configuration for building Wyam sites in DevOps. With a few changes I ended up with the following configuration that I added to the git repository as a file in the root of the repository.

trigger:
- master
pool:
  name: Hosted Ubuntu 1604
variables:
  configuration: debug
  platform: x64
steps:
- task: DotNetCoreInstaller@0
  displayName: Install .NET Core SDK
  name: install_dotnetcore_sdk
  enabled: true
  inputs:
    packageType: 'sdk'
    version: '2.2.101'
- script: dotnet tool install -g Wyam.Tool
  displayName: Install Wyam
- script: wyam
  displayName: Build Site 
- task: ArchiveFiles@2
  displayName: Zip Site
  inputs:
    rootFolderOrFile: '$(Agent.BuildDirectory)/s/output' 
    includeRootFolder: true
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' 
    replaceExistingArchive: true
- script:  >-
      curl
      -H 'Authorization: Bearer $(netlifyAccessToken)' 
      -H 'Content-Type: application/zip'
      --data-binary '@$(Build.BuildId).zip'
      https://api.netlify.com/api/v1/sites/$(netlifySiteId)/deploys
  workingDirectory: '$(Build.ArtifactStagingDirectory)'
  displayName: 'Upload to Netlify'

The above pipeline configuration steps are:

  1. Download .NET Core 2.2.101
  2. Install Wyam.Tool on the build agent
  3. Build the website through the Wyam tool
  4. Create a .zip containing the generated site
  5. Upload the .zip file to the Netlify account

The build script needs a few parameters to work correctly, these parameters can be added to the build step in Azure DevOps through the “Variables → Pipeline variables” section in the build configuration in the Azure DevOps web interface.

The configuration variables are as following:

  • netlifyAccessToken - The access token to your Netlify account, this can be created under the “Personal access tokens” section in your Netlify profile.
  • netlifySiteId - The id of the created site you have in Netlify, this can be found if you go to the site in the Netlify configuration panel and then going to the settings section, the id will be under the section called API ID (https://app.netlify.com/sites//settings/general)

So after creating the build in the web panel for Azure DevOps I tried pushing some changes to make sure everything runs smoothly and all changes are pushed to my Netlify site.

comments powered by Disqus