Blogging using org-mode and Ruby: Fixing it up




As explained on the previous post, I messed up the dependency management process for my blogging tool. I also found a way to simplify it to the minimum (allegedly).

On this post I'll explain what I messed up and show the final result (at least for the time being).

The bug

First I wrote a post and published it and everything was perfect in the world. I was excited and a bit proud about myself. So I went ahead and wrote a follow up (yes, I'm talking about the current series of posts). But when I went to check that the second post was correctly published, I found out that it's contents were the same as the first one, which was obviously wrong!

The problem presented itself on the current lines:

  post post_name => html_files do |t|
    # ...
    content = File.read(t.source)
    # ...
  end

A post task doesn't actually depend on the entire list of html files on the export directory, only on the one that shares it's name with. Also, by passing the list as a dependency, File.read(t.source) was returning (apparently) the content for the first file in that list, which messed everything up.

The solution

First, let me show you the final Rakefile for the project (excluding imports):

  POSTS_DIR="./posts"
  FileUtils.mkdir_p POSTS_DIR

  org_files = FileList.new("#{POSTS_DIR}/*.org").exclude(/\/WIP[_-]/i)
  post_names = org_files.ext("").sub("#{POSTS_DIR}/", "")

  task :default => post_names

  post_names.each do |post_name|
    post post_name => "#{POSTS_DIR}/#{post_name}.org" do |t|
      org_post = OrgPost.new(t.source)

      options = {
        post_status: "publish",
        post_content: org_post.html,
        post_title: org_post.title || t.post_title,
        post_name: t.name,
        terms_names: {
          category:  org_post.categories
        }
      }

      if t.post_exist?
        puts "Updating post #{t.name} ...".green
        WpClient.editPost(post_id: t.post_id, content: options)
      else
        puts "Creating new post #{t.name} ...".green
        WpClient.newPost(content: options)
      end
    end
  end

As you can see, the file is simpler than the one we had on the previous entry. So lets explain each part separately now.

Directories

The only directory we actually need for the current version is the posts one. There are no HTML exports now (you'll see why in a moment).

  POSTS_DIR="./posts"
  FileUtils.mkdir_p POSTS_DIR

This also removed some fiddling with file names, which is nice.

Files and post names

By not having an exports dir, we can also avoid having a list of HTML files.

The only files we want to keep track of are the post sources. And that's just for parsing the post names.

  org_files = FileList.new("#{POSTS_DIR}/*.org").exclude(/\/WIP[_-]/i)
  post_names = org_files.ext("").sub("#{POSTS_DIR}/", "")

We could've shortened the previous code into a single line,

  post_names = FileList.new("#{POSTS_DIR}/*.org").exclude(/\/WIP[_-]/i).ext("").sub("#{POSTS_DIR}/", "")

although I like seeing both parts that process.

The default task

This one didn't change. We want our default task to try and "process" all the potential posts.

  task :default => post_names

Org to HTML

All the tasks and rules regarding converting an org file into HTML and saving the result to disk disappeared. Remember that we're not reading the exports from disk any more.

A side note here:

Converting to HTML and putting it on a disk file was an unneeded extra step result of iterating over a process I was (sort of) copying from someone else. By adding my own constraints without thinking on the consequences, I introduced a bug, an extra step and a burden for future features to work around.

But I'm actually glad this happened because it taught to think things twice and be present for my decisions.

Post dependencies

As explained at the beginning of the article, here was the major screw-up.

A post only depends on the file with the same name, not the list of all the possible files.

So the first thing to do was determine the name of that file.

  post_names.each do |post_name|
    post post_name => "#{POSTS_DIR}/#{post_name}.org" do |t|
      # ...
    end
  end

In the snippet above, you may've noticed that the extension for the file in this case is .org instead of .html. This choice was made in order to force the current post to depend on it's source file instead of the rendered one (that won't actually exist anymore).

The second thing to notice is that now we're just interpolating the folder and the post name instead of using the file list. This is way simpler than before (IMHO) and easier to understand as well.

Creating and updating

For creating and updating posts we use the same strategy as before:

First we create an OrgPost

  org_post = OrgPost.new(t.source)

Then we set the default options to pass to the corresponding method

  options = {
    # ...
  }

And finally we invoke that method

  if t.post_exist?
    puts "Updating post #{t.name} ...".green
    WpClient.editPost(post_id: t.post_id, content: options)
  else
    puts "Creating new post #{t.name} ...".green
    WpClient.newPost(content: options)
  end

The only difference now is that for the post_content option, we call org_post.html instead of reading the file from disk.

  options = {
    # ...
    post_content: org_post.html,
    # ...
  }

The final task ended up looking like this:

  post_names.each do |post_name|
    post post_name => "#{POSTS_DIR}/#{post_name}.org" do |t|
      org_post = OrgPost.new(t.source)

      options = {
        post_status: "publish",
        post_content: org_post.html,
        post_title: org_post.title || t.post_title,
        post_name: t.name,
        terms_names: {
          category:  org_post.categories
        }
      }

      if t.post_exist?
        puts "Updating post #{t.name} ...".green
        WpClient.editPost(post_id: t.post_id, content: options)
      else
        puts "Creating new post #{t.name} ...".green
        WpClient.newPost(content: options)
      end
    end
  end

Conclusion

My small blogging tool is not finished, it's a work in process. But just by dedicating a couple of extra hours across three days I ended up with something pretty functional and learned a ton!

I highly encourage you to play the way I did it, it'll enrich your life.

I'm also extremely happy that I've been able to start writing again.

See you on the next article.

Saluti.