diff --git a/_includes/incenseList.njk b/_includes/incenseList.njk
index 98a2532..3dea597 100644
--- a/_includes/incenseList.njk
+++ b/_includes/incenseList.njk
@@ -2,7 +2,7 @@
{% if postlistHeaderText %}
{{ postlistHeaderText }}
{% endif %}
{% for post in postslist | reverse %}
-
+
diff --git a/content/blog/Solving-SSG-Microblogging-Ergonomics-with-KDialog-for-Incense-Posting.md b/content/blog/Solving-SSG-Microblogging-Ergonomics-with-KDialog-for-Incense-Posting.md
new file mode 100644
index 0000000..db5f9f8
--- /dev/null
+++ b/content/blog/Solving-SSG-Microblogging-Ergonomics-with-KDialog-for-Incense-Posting.md
@@ -0,0 +1,286 @@
+---
+title: "Poaster: Solving SSG Microblogging Ergonomics with Ruby and KDialog"
+description: "Trying to make it a little less brutal to make small, frequent posts on SSGs."
+date: 2025-06-08
+tags:
+ - Site Updates
+ - Eleventy
+ - KDE
+synopsis: "Trying to make it a little less brutal to make small, frequent posts on SSGs."
+imageURL: "/img/poaster/poaster_icon.svg"
+imageAlt: "An icon featuring a red toaster with the Ruby diamond on it popping out a sheet with the Markdown logo on it as though it were toast."
+mastodon_id: "114650833104413858"
+---
+
+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](/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](https://www.11ty.dev/) website, the trouble was *wanting to use it* after it was built. Unlike using a {{ "CMS" | abbr("Content Management System") | safe }} such as WordPress to make a website, I knew of no nice interface for Eleventy, or for that matter any {{ "SSG" | abbr("Static Site Generator") | safe }}, that would help me create a post and publish it online without opening an {{ "IDE" | abbr("Integrated Development Environment") | safe }}[^1] and using the command line. Instead, the process looks something like this:
+
+[ every time I decide to make a tiny status update. Also, I just noticed that I totally screwed up the frontmatter for that post.")](/img/poaster/ergonomics_fail.webp)
+
+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" | abbr("SSH File Transfer Protocol") | safe }}. No needless complexity like running Eleventy on the server, or using a host like Netlify.[^2] 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](https://getpublii.com/). Publii seems to be made predominantly with end-users in mind, however. It's not just a local[^3] CMS, it's an SSG in its own right, which does me no good as I can't make it work with my website[^4]. 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](https://invent.kde.org/utilities/kdialog) to provide a rudimentary UI. So that's what I did.
+
+The idea was simple: a [wizard](https://en.wikipedia.org/wiki/Wizard_(software))-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`:
+
+```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
+username@my.server:/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](https://develop.kde.org/docs/administration/kdialog/) and wrote a method to handle that `/Poaster/lib/spawn_radio_list.rb`:
+``` ruby
+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}`
+end
+```
+I 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`:
+``` ruby
+def spawn_toast(title, text, seconds)
+ `kdialog --title "#{title}" --passivepopup "#{text}" #{seconds}`
+end
+```
+`/Poaster/lib/spawn_input_box.rb`:
+``` ruby
+def spawn_input_box(title, text)
+ `kdialog --title "#{title}" --inputbox "#{text}"`
+end
+```
+`/Poaster/lib/ensure_dir_exists.rb`:
+``` ruby
+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`:
+``` ruby
+def write_file(directory, name, extension, content)
+ post_file = File.new(%(#{directory}/#{name}.#{extension}), 'w+')
+ post_file.syswrite(content)
+ post_file.close
+end
+```
+All I had to do then was tie it all together in `/Poaster/poaster.rb`:
+``` ruby
+#!/usr/bin/env ruby
+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'
+
+# Populate types_arr with post types
+post_types_arr = []
+config_data['postTypes'].each do |type|
+ post_types_arr.push(type['name'])
+end
+
+# Display post list dialog to user
+post_type = config_data['postTypes'][Integer(spawn_radio_list(dialog_title_prefix, 'Select a post type:', post_types_arr))]
+
+# Set the word we will use to refer to the post
+post_unit = post_type['postUnitName']
+
+# Collect frontmatter from user
+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
+
+# Collect post content from user
+post_content = spawn_input_box %(#{dialog_title_prefix} - Enter Content), %(Enter #{post_unit} content:)
+
+# Make sure the output folder exists
+post_directory = post_type['postDirectory']
+ensure_dir_exists(post_directory)
+
+# Create post string
+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})
+
+# Write post string to file and notify user
+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
+
+# Run build and upload commands
+`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](https://upchur.ch/gitea/n_u/Poaster), 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.
+
+[](/img/poaster/app-menu.webp)
+
+My `poaster.desktop` file looks something like this:
+``` bash
+[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=Application
+```
+Here's the script in action:
+The ease! The convenience!
+
+To build the new "now burning" incense microblog feature, I created two new pages. [/now-burning](/now-burning) shows the latest entry:
+
+``` html
+---
+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:"
+---
+{% raw %}{% set burning = collections.nowBurning | last %}
+
+
+
+
+
+
+```
+…and [/once-burned](/once-burned) shows past entries:
+``` html
+---
+layout: layouts/base.njk
+title: "Nathan Upchurch | Once Burned: Incense I've burning in the past."
+structuredData: none
+---
+{% raw %}{% set burning = collections.nowBurning | last %}
+
+
Previous “Now Burning” Entries:
+{% set postsCount = collections.nowBurning | removeMostRecent | length %}
+{% if postsCount > 0 %}
+{% set postslist = collections.nowBurning | removeMostRecent %}
+{% set showPostListHeader = false %}
+{% include "incenseList.njk" %}
+{% else %}
+
Nothing’s here yet!
+{% endif %}{% endraw %}
+
+
+
+```
+…using a post-listing include built specifically for microblogging:
+
+``` html
+
+ {% raw %}{% if postlistHeaderText %}
+
+```
+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](../../me/).
+
+[^1]: Yes, I am aware that [Kate](https://kate-editor.org/) isn't *technically*
+an IDE.
+
+[^2]: At risk of sounding crabbit and behind the times, I don't know why web
+development has to be so damned complicated these days. Like, an entire fancy
+for-profit infrastructural platform that exists just to host static websites?
+It seems nuts to me.
+
+[^3]: Thank christ. Why does everything need to run in the cloud when we
+already have computers at home?
+
+[^4]: I did however use it to very quickly set up a nice looking blog site for
+my partner.
+
diff --git a/content/blog/incense-cigarettes-reviewing-boy-vienna-11-11.md b/content/blog/incense-cigarettes-reviewing-boy-vienna-11-11.md
index 32b0da6..3fbda38 100644
--- a/content/blog/incense-cigarettes-reviewing-boy-vienna-11-11.md
+++ b/content/blog/incense-cigarettes-reviewing-boy-vienna-11-11.md
@@ -6,8 +6,8 @@ tags:
- Incense
- Incense Review
imageURL: /img/boy_vienna_11_11/boy_vienna_11_11_incense_cigarette_sticks_2.webp
-imageAlt: ""
-synopsis: "What appears to be a pack of cigarettes labeled 11:11. There is also a card featuring the brand name Boy Vienna and a temporary tattoo featuring an image of a lipstick-print and the brand name."
+imageAlt: "What appears to be a pack of cigarettes labeled 11:11. There is also a card featuring the brand name Boy Vienna and a temporary tattoo featuring an image of a lipstick-print and the brand name."
+synopsis: "Taking a look at Boy Vienna's viral cigarette incense sticks."
mastodon_id: "114462578542598320"
---
[Boy Vienna](https://boyvienna.com/) is a brand from fashion designer and multi-media artist [Afaf Fi Seyam](https://www.instagram.com/zeopatra) that has been receiving attention on [TikTok](https://www.tiktok.com/@boyvienna/video/7366977382508514603) and [Instagram](https://www.instagram.com/zeopatra/reel/DAyIy2Lv0RQ/) for its incense cigarettes. I knew I was going to have to try these sticks the minute they found their way onto my screen—it would seem that [everyone else felt the same way](https://www.instagram.com/zeopatra/p/DJHP0a3NnlI/), as when I made my way to the web store most of Boy Vienna's incense varieties were sold out. For 35 {{ "USD" | abbr("United States Dollars") | safe }}, I was able to snag a box of the 11:11 variety, listed as containing a blend of sage, lavender, and rosemary.
diff --git a/content/now-burning.njk b/content/now-burning.njk
index 655f29f..8287ced 100644
--- a/content/now-burning.njk
+++ b/content/now-burning.njk
@@ -10,7 +10,7 @@ postlistHeaderText: "What I've been burning:"