A simple file-based blog engine in Clojure

11 months ago

When I built this blog I was thinking of doing what I previously have done with great success, which is to write the front-end in whatever language I want and use WordPress as an API for content delivery. However in a quest to simplify my set-up, I've decided to move away from a VPS and go with Heroku, and so using whatever I want for front-end, and WordPress as an API, quickly gets complicated.

Which is why I decided to ditch WordPress, and write my own static blog engine, in my current favorite language, Clojure. What I needed was a very simple and straight forward way of having meta data in a file that contains Markdown, and be usable for more use cases, other than just blogging. if you're longing for something similar, here's how do to it.

First things first

We need a file structure in which your data resides. Let's say that your data is in a root directory of your project, where your project.clj file is and let's say that the directory name is something obvious like, data. Now let's say in that directory you have another directory where you keep your blog posts, let's name that also something obvious, like, posts.

Now let's create a file in the posts directory that looks like this:

--
date: 2019-01-01
title: Hello, World
--

Markdown content follows.

As you can see, the content between the three dashes are meta data, or more correctly, YAML. After that is our content, which can be absolutely anything. Now, let's name this file hello-world.md, because for our intents and purposes this is a Markdown file, but you can name it whatever you want, in whatever format you want. Once you've done this, you should have a file at the location data/posts/hello-world.md.

Parsing the file

Before we actually get to the blog part, we need to extract the information in that file. That means the YAML data and the content. For that, let's create a function:

(defn- parse-file [path-to-file]
  (let [content (slurp path-to-file)
        yaml (yaml/parse-string (second (re-find #"(?is)\--(.*?)\--" content)))
        entry {:entry (second (re-find #"(?is)(?:.*?--){2}(.*)" content))}]
    (conj yaml entry)))

This file is dependent on the owainlewis/yaml Clojure library, so be sure to get it and include it in the namespace where we do all of this.

As you can see, in the above function, on the second line of code, we use slurp to get the contents of the file. On the third line of the above code, we use a little help from a regular expression to get all the YAML data, which we then parse with the YAML Clojure library and on the fourth line of the above code we use yet a little more help from a regular expression to get everything in that file after the YAML data, which is the Markdown, or HTML, or whatever you wish, for the content.

Then, with the above function, we can return our result as a map that will look something like this:

{:date "2019-01-01"
 :title "Hello, World"
 :entry "Markdown content follows."}

The above parse function, however, is a private function (like the defn- gives away), because we won't be actually using this for our blog (we could, but we will wrap this function with another one later instead).

The blog bits

Since what we want is to have a blog, we want to display a list of blog posts. To do so, we create another function, such as this:

(defn- get-file-names-from-path [path]
  (remove (fn [x]
            (nil? (re-find #"\." x)))
          (for [file (file-seq (clojure.java.io/file path))]
            (.getName file))))

What this essentially does is it goes through an entire directory, makes sure that it will skip anything it finds that does not have a dot in it, because we don't want directories, but instead actual files.

Now that we have all the file names in a directory, we'd want to parse them with the above function we created for parsing files, to extract the juicy YAML meta data and content bits out of them. To do so, we create yet another function:

(defn- parse-files [file-names path]
  (loop [xs file-names
         result []]
    (if xs
      (let [x (first xs)]
        (recur (next xs) (conj result (parse-file (str path "/" x)))))
      result)))

This goes through all the file names we give it, in the path we give it, then parses them with our own parsing function and returns the result. But this is still not enough to have a simple blog, we want to sort the posts by a YAML meta data key, like, uhm .. for example, date, too .. right? And then we probably also want to order it by either ascending or descending order, right? To do these two things, we create a function:

(defn- sort-and-order-parsed-files [parsed-files sort-by-key order-by-desc?]
  (let [sorted-files (sort-by sort-by-key parsed-files)]
    (if order-by-desc?
      (reverse sorted-files)
      sorted-files)))

This sorts given parsed files by the given key and orders them by the given order. Now let's put it all together into a simple function that we will be using to get all our blog data out of all of our blog posts:

(defn get-all-from [path sort-by-key order-by-desc?]
  (let [file-names (get-file-names-from-path path)
        parsed-files (parse-files file-names path)
        result (sort-and-order-parsed-files parsed-files sort-by-key order-by-desc?)]
    result))

This returns a vector containing the maps of all the files in a given path, sorted by a given YAML meta data key and ordered by ascending or descending order. And to get just a single file, we can just wrap our parse function to achieve it:

(defn get-from [path-to-file]
  (parse-file path-to-file))

Using all of this

As a result of the above code, you can now get all the blog posts from data/posts directory, sorted by date, in a descending order by writing the following bit of code:

(get-all-from "data/posts" :date true)

And to get just a single file's content, by writing this bit of code:

(get-from "data/posts/hello-world.md")

To conclude

This all makes up a blog engine. In fact, it's the very same blog engine I use for this site. Now to put this all together into an actual website is a subject for another post, which I may or may not write, but the point of this is that you would be able to use whatever web framework you want to put together a simple static file based blog engine written in Clojure.