posts | links

The new blog engine

2023-05-15 | computing, blog, rust, idm

GitHub started supporting custom code for generating static pages after I started my blog in 2015, so now I have more options for a blog engine. I initially set up a Zola generator, which worked for the standard posts. Zola doesn't have any plugin support though, and I wanted to add a bookmarks page, which would use a different page generator. The links would be written down in a compact IDM document and then a JavaScript-enhanced HTML bookmark page would be generated from it. Any code dealing with IDM needs to be a custom thing written by me. The Zola workflow couldn't support this, so I ended up writing a clumsy two-phase GitHub action that generated both the Zola site and my own bespoke bookmark page.

The bookmark generator turned out to be a surprisingly simple affair using IDM deserialization, std::convert::From conversion and Askama templating. I started thinking about ditching Zola and generating the entire site from IDM using my own code.

        idm::from_str    convert::From         Template::render
 ┌──────────┐ |  ┌──────────┐ |  ┌──────────────────┐ |  ┌─────────────┐
 │ IDM text ├────► IDM data ├────►  template data   ├─╭──► static HTML │
 └──────────┘    └──────────┘    └──────────────────┘ │  └─────────────┘
                                             ┌────────┴─────────┐
                                             │ Askama templates │
                                             └──────────────────┘

An insight that helped with this was that I could print out an entire directory as an outline with file and directory names as headlines and file contents indented under the file name headline. Fixed file or directory names in this scheme correspond to struct field names in the site datatype when the whole directory is printed out as an outline and deserialized with IDM.

Using the generic directory-to-outline routine, printing the following directory tree,

site/posts/one-post.md:
  Lorem ipsum dolor sit amet,
  consectetur adipiscing elit,
site/posts/another-post.md:
  Ut enim ad minim veniam,
  quis nostrud exercitation ullamco
site/links.idm:
  https://example.com/
    :title Example bookmark

produces the following IDM outline,

posts
  one-post
    Lorem ipsum dolor sit amet,
    consectetur adipiscing elit,
  another-post
    Ut enim ad minim veniam,
    quis nostrud exercitation ullamco
links
  https://example.com/
    :title Example bookmark

which deserializes into the following Rust type (assuming suitable Blogpost and Link types),

struct Site {
  posts: IndexMap<String, Blogpost>,
  links: IndexMap<String, Link>,
}

The site directory deserializes to an initial input struct that matches the structure of the IDM data exactly. This needs to be converted into an output struct that contains derived information like lists of posts by tag and can drive Askama templates. The conversion is straightforward and is implemented with Rust's standard From trait. The HTML and Atom feed pages for the static site are then generated by rendering the Askama templates associated with the output types. The entire site generator runs mostly on type definitions and involves little actual code.

Blog posts are written in Markdown, so how does everything being in IDM work with this? IDM's indent based structure provides a convenient multi-line string escape syntax, where an entire indented region can be treated as having a different syntax up until the next dedentation. The blog post files have metadata fields in the colon-prefixed header block and the rest of the content is interpreted as Markdown text. When the deserialized input structs are converted to output structs, the Markdown text is converted to HTML with the pulldown-cmark crate.

The whole site is now being generated using this system which was first developed for just the bookmarks page. The blog generation works fine, and any new features can be easily added by extending the IDM site schema and the From-conversion methods. For example, for the project outlines post, I wanted to be able to publish a raw outline instead of a Markdown post, so I added a format field to the post metadata struct, set it to default to Markdown so none of the existing posts needed to be touched, and set it to Outline for the new post. Then I wrote a new HTML converter by hand for the outline format and I had a brand new style option for my blogposts.

The source code for the blog engine can be found in the repository of my blog.