Using liquid-rust with serde

I have been running Jekyll for 9 years now. I wrote about it in this post. At this point, my needs and patience have outgrown Jekyll. There is a few features that I miss and there is some annoyance that comes with running Ruby, I feel.

I have decided to write my own static site generator. I need something that I control - something that bends to my will. But because I am lazy, I will keep the content structure of Jekyll. This way I can just run my new static site generator on the exact same content that I already have. It means that I have to use markdown for my content and it means that I have to use liquid as my templating system. And I have no problem with this.

It was easy to find a markdown compiler for the project. I chose pulldown-cmark. It seems easy to use and passes all the compilations of my current markdown files. After looking for a good liquid crate, I fell upon liquid-rust. It seems to be written for cobalt, which is a static site generator written in Rust. And it was damn easy to get started on. Until I hit my first speed bump. How do I put non-trivial objects into the templating engine?

I have this piece liquid code in my html for my posts:

<section class="content">
  <ul class="listing">
    {% for post in site.posts %}
    <li>
       <a href="{{ post.url }}">{{ post.title }}</a> <span class="left">{{ post.date | date: "%B %e, %Y" }}</span>
       <hr>
    </li>
    {% endfor %}
  </ul>
</section>



Problem was just that all the examples I could find showed how to inject a very simple structure into the templating engine. The example is showed below and is showing to how put in a primitive type.

let template = liquid::ParserBuilder::with_stdlib()
  .build().unwrap()
  .parse("Liquid! -2").unwrap();

let mut globals = liquid::object!({
  "num": 4f64
});

let output = template.render(&globals).unwrap();
assert_eq!(output, "Liquid! 2".to_string());



My goal is just to inject a complex type which holds a list of post and where each post object holds url, title and date. I scoured the internet for a way of doing this and came up empty. I found the author of the crate was on the official Rust Discord and I introduced myself and my problem. And he showed me the simple solution. Use serde.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Post {
  title: String,
  url: String,
  date: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Posts {
  posts: Vec<Post>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Globals {
  site: Posts,
}



And now with complex data structure described in serde, it was as easy as creating the objects and using them as input to the templating engine:

let site = Globals {
    site: p3
};

let index = fs::read_to_string("_build/index.html").expect("Should be able to read the index file");
let template = liquid::ParserBuilder::with_stdlib()
       .build().unwrap()
       .parse(&index).unwrap();
let globals = liquid::to_object(&site).expect("Should be able to parse");
    
let output = template.render(&globals).unwrap();
println!("index: {}", output);



It took me some time to find the solution - or find the person who could give me the solution. But now that it is here, it seems so easy. And with with that, I can finalize my static site generator.

Discussion, links, and tweets

I'm a developer and CS graduate. Follow me on Twitter; you'll enjoy my tweets.

I also keep an "ask me anything" type of project in a repository on GitHub (naturally!). Feel free to ask me a question.