
Anyone familiar with my blog will know that I like to write about incense. A reader wrote to me some time ago asking about what sticks I’ve been enjoying lately, and it occurred to me that it might be a nice thing to have a “now listening” type feature on my website, so that fellow incense heads could get a sense of the types of incense I like. After all, while I write plenty of incense reviews, they represent only a small percentage of the sticks, cones, powders, woods, and resins I’m burning or heating from day to day. (If you’re here for my incense content, feel free to skip this one and head to /now-burning to see the new feature!)
The issue of ergonomics #
While it would have been simple enough for me to build a microblogging feature into my Eleventy website, the trouble was wanting to use it after it was built. Unlike using a CMS such as WordPress to make a website, I knew of no nice interface for Eleventy, or for that matter any SSG, that would help me create a post and publish it online without opening an IDE and using the command line. Instead, the process looks something like this:
I don’t necessarily want to feel like a
hackerman
every time I decide to make a tiny status update. Also, I just
noticed that I totally screwed up the frontmatter for that post.
As big of a nerd as I am, I’m just not going to want to do that multiple times a day for what amounts to a status post. This lead me to scour the internet looking for a solution: something that I could run on my own desktop or laptop that could build my site locally and push changes to my website, hosted the old fashioned way: as a bunch of text files sitting on a server accessible via SFTP. No needless complexity like running Eleventy on the server, or using a host like Netlify. Surely there’d be something, right? Surely, the realm of SSGs can’t be without at least one nice, local user interface that people can use without being a web developer?
An attempt to fix the problem #
In the end, I did find one answer to the problem: Publii. Publii seems to be made predominantly with end-users in mind, however. It’s not just a local CMS, it’s an SSG in its own right, which does me no good as I can’t make it work with my website. So after coming up with nothing I could use, I gave the idea a rest for a while until I had the epiphany that I could solve the problem with a simple script using KDE’s KDialog to provide a rudimentary UI. So that’s what I did.
The idea was simple: a wizard-like experience that guides the user through the creation of a microblog / status post. Post types and the data they collect should be customized by the user via a JSON configuration file. After the post data is collected from the user, the script should execute a user-defined build command as well as a user-defined command to sync the static files to the server.
Building “Poaster” #
For some reason, I decided to write my script in Ruby, a language for which I once completed a course before promptly forgetting everything I knew about it. I would have had a much easier time using JavaScript and Node, which I am much more familiar with and have successfully used for similar purposes. Why I did not is anyone’s guess. All this to say: please do not make (too much) fun of my shitty little script, which I have dubbed “Poaster.”
I started with the JSON configuration file, /Poaster/config/config.json:
{ "buildCommand": "npx @11ty/eleventy", "postTypes": [ { "name": "Now Burning", "postUnitName": "incense", "contentEnabled": true, "frontMatter": [ { "name": "title" }, { "name": "manufacturer" }, { "name": "date" }, { "name": "time" } ], "postDirectory": "/post/output/dir" } ], "uploadCommand": "rsync -av --del /local/path/to/site/output [email protected]:/remote/path/to/public/site/files", "siteDirectory": "/local/path/to/site/repo" }Here, the user can specify as many post types as they like, each with their own output directory. Each post type can also collect as many pieces of frontmatter as the user cares to specify.
The first thing the script needed to do was ask the user which post type they want to create, so I referenced the KDialog tutorial and wrote a method to handle that /Poaster/lib/spawn_radio_list.rb:
def spawn_radio_list(title, text, options_arr) command = %(kdialog --title "#{title}" --radiolist "#{text}") options_arr.each_with_index do |option, i| command += %( #{i} "#{option}" off) end `#{command}` endI wrote a few more methods in /Poaster/lib to spawn toast notifications, input boxes, create directories if they don’t exist, and write files: /Poaster/lib/spawn_toast.rb:
def spawn_toast(title, text, seconds) `kdialog --title "#{title}" --passivepopup "#{text}" #{seconds}` end/Poaster/lib/spawn_input_box.rb:
def spawn_input_box(title, text) `kdialog --title "#{title}" --inputbox "#{text}"` end/Poaster/lib/ensure_dir_exists.rb:
def ensure_dir_exists(directory_path) unless Dir.exist?(directory_path) FileUtils.mkdir_p(directory_path) spawn_toast 'Directory Created', %(Poaster created #{directory_path}.), 10 end end/Poaster/lib/write_file.rb:
def write_file(directory, name, extension, content) post_file = File.new(%(#{directory}/#{name}.#{extension}), 'w+') post_file.syswrite(content) post_file.close endAll I had to do then was tie it all together in /Poaster/poaster.rb:
require 'json' require 'fileutils' require './lib/spawn_input_box' require './lib/spawn_radio_list' require './lib/spawn_toast' require './lib/ensure_dir_exists' require './lib/write_file' config_data = JSON.parse(File.read('./config/config.json')) dialog_title_prefix = 'Poaster' post_types_arr = [] config_data['postTypes'].each do |type| post_types_arr.push(type['name']) end post_type = config_data['postTypes'][Integer(spawn_radio_list(dialog_title_prefix, 'Select a post type:', post_types_arr))] post_unit = post_type['postUnitName'] frontmatter = [] post_type['frontMatter'].each do |item| frontmatter.push({ item['name'] => spawn_input_box(%(#{dialog_title_prefix} - Enter Frontmatter'), %(Enter #{post_unit} #{item['name']}:)) }) end post_content = spawn_input_box %(#{dialog_title_prefix} - Enter Content), %(Enter #{post_unit} content:) post_directory = post_type['postDirectory'] ensure_dir_exists(post_directory) post = %(---\n) post_id = '' frontmatter.each_with_index do |item, i| post += %(#{item.keys[0]}: #{item[item.keys[0]]}) post_id += %(#{item[item.keys[0]].chomp}#{i == frontmatter.length - 1 ? '' : '_'}) end post += %(---\n#{post_content}) post_file_name = %(#{post_type['name']}_#{post_id.chomp}) post_extension = 'md' write_file post_directory, post_file_name, post_extension, post spawn_toast 'File Created', %(Poaster created #{post_file_name}#{post_extension} at #{post_directory}.), 10 `cd #{config_data['siteDirectory']} && #{config_data['buildCommand']} && #{config_data['uploadCommand']}`Burning now #
There is a lot that this script should do that it doesn’t, but for now, it’s still a handy wee utility for SSG users on GNU/Linux systems running KDE who want to make creating quick status-type posts a little less painful. Just make sure KDialog is installed (as well as Ruby, naturally), clone the repo, create /Poaster/config/config.json to meet your needs using the example as a reference and you’re off to the races! I’ve even made a silly little toaster icon using assets from some of the KDE MimeType icons that you can use if you want to make a .desktop file so that you can click an icon on your app launcher to start the script.
Isn’t that nice?
My poaster.desktop file looks something like this:
[Desktop Entry] Exec=/path/to/poaster.rb GenericName[en_US]=Create a post with Poaster. GenericName=Create a post with Poaster. Icon=/path/to/poaster_icon.svg Name=Poaster NoDisplay=false Path=/path/to/repo/ StartupNotify=true Terminal=false Type=ApplicationHere’s the script in action:
To build the new “now burning” incense microblog feature, I created two new pages. /now-burning shows the latest entry:
--- layout: layouts/base.njk title: "Nathan Upchurch | Now Burning: What incense I'm burning at the moment." structuredData: none postlistHeaderText: "What I've been burning:" --- {% set burning = collections.nowBurning | last %} <h1>Now Burning:</h1> <article class="post microblog-post"> <img class="microblog-icon" src="/img/censer.svg"> <div class="microblog-status"> <h2 class="">{{ burning.data.title }}{% if burning.data.manufacturer %}, {{ burning.data.manufacturer }}{% endif %}, {{ burning.date | niceDate }}, {{ burning.data.time }}</h2> {% if burning.content %} <div class="microblog-comment"> {{ burning.content | safe }} </div> {% endif %} </div> </article> <a href="/once-burned/"> <button type="button">Previous Entries »</button> </a>…and /once-burned shows past entries:
--- layout: layouts/base.njk title: "Nathan Upchurch | Once Burned: Incense I've burning in the past." structuredData: none --- {% set burning = collections.nowBurning | last %} <h1>Previous “Now Burning” Entries:</h1> {% set postsCount = collections.nowBurning | removeMostRecent | length %} {% if postsCount > 0 %} {% set postslist = collections.nowBurning | removeMostRecent %} {% set showPostListHeader = false %} {% include "incenseList.njk" %} {% else %} <p>Nothing’s here yet!</p> {% endif %} <a href="/now-burning/"> <button type="button">Latest »</button> </a>…using a post-listing include built specifically for microblogging:
<section class="postlist microblog-list"> {% if postlistHeaderText %}<h2>{{ postlistHeaderText }}</h2>{% endif %} <div class="postlist-item-container"> {% for post in postslist | reverse %} <article class="postlist-item"> <div class="post-copy"> <h3> {% if post.data.title %}{{ post.data.title | safe }}{% else %}?{% endif %}{% if post.data.manufacturer %}, {{ post.data.manufacturer | safe }}{% endif %} </h3> <div class="post-metadata"> <div class="post-metadata-copy"> <p> <time datetime="{{ post.date | htmlDateString }}">{{ post.date | niceDate }}{% if post.data.time %}—{{ post.data.time }}{% endif %}</time> </p> </div> </div> {% if post.content %} <div class="microblog-comment"> {{ post.content | safe }} </div> {% endif %} </div> </article> <hr> {% endfor %} </div> </section>And that’s about it! There’s a lot to do to make the script a little less fragile, such as passing along build / upload error messages, allowing for data validation via regex, et cetera. I’m sure I’ll get to it at some point. If Poaster is useful to you, however, and you’d like to submit a patch to improve it, please do let me know.
.png)

