Commit Redux

A software development blog in commit-sized retrospectives

List all posts

Hello, world!

Wednesday May 07 2025 • 12:47 AM

This is the very first post in my new Sinatra app for blogging :)

Making a Sinatra Blog

Wednesday May 07 2025 • 01:58 AM

I’ve spent an inordinate amount of time devising blog engines in so many tools, from something pre-built like Jekyll to my own solutions with custom Ruby scripts and even a full Rails app.

Now I’m testing this little Sinatra app made just for me. Here’s the first post ever from its web interface :-)

Classless CSS

Wednesday May 07 2025 • 02:36 AM

I started using the Sakura CSS theme to give the blog a nicer look, then I tried a few more classless themes like cosmoCSS and a few others from CSSBed.com. I settled on Water.css and things are looking clean. I think this is good enough for now. I’ve only developed a few parts on it, namely seeing all posts and creating new ones via a regular form. I haven’t added editing or deleting functionality yet.

🪐cosmoCSS

CSSBed

Water.css

ActionView in Sinatra, and this blog's name

Wednesday May 07 2025 • 03:05 AM

The blog’s main writing space is a textarea element. When rendering posts, I’m outputting things without line breaks. Last night I solved this same problem over on Rails using the simple_format helper from the ActionView library.

To make the helper available in Sinatra, I imported the ActionView gem and activated the Sinatra helper block to make it and other helpers like truncate() available in my ERB templates.

I’m not entirely sure it’s worth it to bring the whole library since the goal is to keep a minimal footprint, but it also doesn’t make sense to reinvent the wheel for such simple things. Maybe I’ll come back to this.

Also, I like the name Commit Redux for this blog, but (My) Last Commit is also pretty good.

Updating (editing) Posts

Wednesday May 07 2025 • 03:15 AM

For editing posts, I’m using the PATCH HTTP method, and I’m still playing around with the method in the Sinatra app to make sure it works well.

Here’s what I got so far:


patch '/posts/:id' do
  @post = Post.find_by(id: params[:id])
  if @post.update(params[:post])
    redirect "/posts/#{@post.id}"
  else
    status 422
    erb :'posts/edit'
  end
end

I looked over the code on the Rails app from last night to find the hidden input which changes the form’s method from POST to PATCH, so I added the same to the Sinatra form. I also fixed the action in the edit form template (I had left it pointing to the /posts URL instead of /posts/:id).

In the new post method, I had used @post.create(params) and I assumed that would work for updating posts too, but that was wrong.

While looking over the Rails code, I realized the form sets parameters using a nested format, like post[title] and post[content]. I updated the ERB template for both forms to follow this convention for the named parameters.

In summary, implementing the UPDATE part of CRUD from “scratch” in Sinatra required me to:

  • Create a GET path method and to render the edit template
  • Create a PATCH path method for /post/:id to update singular posts by ID
  • Update the HTML form and ensure parameters are nested

I also added a paragraph to indicate if and when a blog post has been updated.

New post with nested params

Wednesday May 07 2025 • 04:34 AM

This is a quick post to ensure I can still create posts on the web UI.

Deleting Posts

Wednesday May 07 2025 • 05:20 AM

I’ve added a controls bar above the blog posts with a link to edit and a form that looks like a button to delete posts.

Those are the CRUD operations for the Post model in this little blog engine. Naturally the styling is a little wonky but I feel proud about having implemented what Rails does with a rails generate scaffold Post title:string content:text on my own from scratch.

It’s certainly helped me understand forms better. And there’s something kind of cool about having this cute little web interface for blog posts.

Screenshot of web interface

Displaying Timezones Correctly

Wednesday May 07 2025 • 05:34 AM

To show the timezone I wrote this monstrosity of a line within the ERB template for posts:

@post.created_at.in_time_zone("Pacific Time (US & Canada)").strftime("%A %B %d %Y • %I:%M %p")

I’m not sure how to configure timezones globally the way I can set environment variables in Heroku after deployment or even in Rails config files, but this works so far.

Update: I was using single quotes and double quotes in the line above… oops

Adding Markdown Support

Wednesday May 07 2025 • 05:41 AM

Adding Markdown support was pretty easy. I added the kramdown gem and added this line to the show ERB template.

sanitize Kramdown::Document.new(@post.content).to_html

Water.css takes care of the styling and it looks pretty nice. For images and code elements in the previous posts I wrote the HTML by hand, but this will make it much more convenient to write in the future.

Day 1 Redux — Starting a Sinatra App

Wednesday May 07 2025 • 06:05 AM

Screenshot-2025-05-07-at-6-16-16-AM.png

I’ve spent about 4 hours total working on this blog and I’m excited about where it’s at right now. The goal is to eventually deploy it on my very own VPS to get costs much lower than what I’m spending on Heroku, especially so I can leverage the usage of sqlite3 instead of paying almost $15 per month for a Posgresql instance at a tier I’m nowhere near using fully per app. That’s just too much. I was paying about $20 on Squarespace.

I appreciate all the services Rails comes bundled in, but I find navigating the framework somewhat overwhelming. Obviously I’ll get more comfortable with it, but I’m only able to appreciate the utility of the myriad of libraries it ships with by understanding how difficult it is to implement their functionality on my own.

So far I’m enjoying working with Sinatra a lot, the size of the app is manageable at this point and I can see myself getting this blog engine in tip-top shape with some Stimulus magic for interactivity next.

P.S. The whole Commit Redux format is kind of cool, right? Making these posts so short makes me want to write even more. And there’s something good about having titles, turns out titles are pretty important. It keeps things from devolving into the craziness of Twitter, Mastodon and the like.

Rackup

Thursday May 08 2025 • 02:05 AM

For starting Sinatra apps, I’d gotten into the habit of defining a custom task called :serve in my Rakefile, but in recent experiments with ActiveRecord, this task would consistently break.

I’m not well versed in Ruby processes yet, but I believe running the app.rb file directly from the terminal is different from invoking the Rake task because two new processes are started with the latter approach —one for Rake and the other for starting the app itself.

I’ve added rackup and the puma server to my project as I look towards deployment in my own VPS like I mentioned yesterday.

Hetzner

Thursday May 08 2025 • 03:13 AM

I created an account on Hetzner. They had me verify my ID and send them pictures of both sides of my driver’s license plus my credit card. I got verified right away. People keep raving about how great they are, so we’ll be checking out their services.

I purchased the lowest cost server with the following configuration:

  • Location: Nuremberg (eu-central)
  • Image: Ubuntu 24.04
  • Type: Shared vCPU x86 — CX22, 2VCPUs, 4GB RAM, 40GB SSD, 20TB Traffic ($3.99)
  • Networking: Public IPv4, IPv6 ($0.60)

Final price: $4.59 / month

I named the server Mojave.

After a few seconds my server was created and ready to go. I received an email from Hetzner with further instructions.

SSH into Mojave

Thursday May 08 2025 • 04:06 AM

I was able to log in to my server. First thing I was prompted to do was update my server password.

Next I generated an SSH key:

$ ssh-keygen -t ed25519 -C "<email@domain.com>" -f ~/.ssh/id_hetzner

Since I had created the server already, I had to add the contents of the .pub file to the root server:

$ echo "keyfile_content" >> /root/.ssh/authorized_keys

Now I can SSH into Mojave.

Dokku

Thursday May 08 2025 • 05:13 AM

I’ve read great things about Dokku, a project billing itself as:

An open source PAAS alternative to Heroku

I’m starting with this after reading a bit about nginx because it seems much easier to manage. I toyed with the idea of installing all my necessary dependencies locally and running a simple static site from Mojave but I think this approach will be a bit easier.

I followed the following instructions from their documentation.

Mojave

Download installation script

$ wget -NP . https://dokku.com/bootstrap.sh

Run the installer

$ sudo DOKKU_TAG=v0.35.18 bash bootstrap.sh

Configure your server domain

$ dokku domains:set-global bucareli.co

and your ssh key to the dokku user

$ PUBLIC_KEY="your-public-key-contents-here" $ echo "$PUBLIC_KEY" | dokku ssh-keys:add admin

create your first app and you’re off!

$ dokku apps:create test-app

To get the custom domain working, I went to Porkbun and created an A record pointing to my server’s public IP Address.

Local

In my local project directory I ran this command:

$ git remote add dokku dokku@your-vps-ip:test-app

Lastly I ran

$ git push dokku main

This prompted me to enter the password for dokku, which meant my SSH key wasn’t configured successfully.

Configuring Git to use the right SSH key

Thursday May 08 2025 • 05:33 AM

Every time I attempted deploying with git push dokku main I was prompted for Dokku’s password on my VPS, meaning Git was using the wrong SSH key.

I created a ~/.ssh/config file to fix that.


Host dokku-server
  HostName IP_ADDRESS
  User dokku
  IdentityFile ~/.ssh/id_hetzner

Deployment of a test app

Thursday May 08 2025 • 05:36 AM

With my SSH keys configured, I ran git push dokku main from my local repo.

I checked http://bucareli.co and that showed a “Welcome to nginx” screen, but the Sinatra app itself wasn’t running. I’ll go over my code and make sure I’m not missing anything important and try again.

The app is running now! 🥳

I needed to add an A record in Porkbun for the subdomain. It’s being served over http though, so I’ll have to fix that. Also, I can access from my web browser with no problem, but curl displays a “Could not resolve host” error message… weird.

Nevermind, it’s all good now! Probably took a bit to propagate.

Day 2 Redux — Deploying a test app to Mojave, my Hetzner VPS

Thursday May 08 2025 • 06:25 AM

Last night I started this blog from scratch, manually writing the code for each element as I needed it. There’s really not much overhead at this point, not even an authentication system. I plan on deploying to production once I’ve done that and also after enabling https on the little test app I deployed today.

It took me around 4 hours to get Dokku set up on Mojave, but this included signing up for Hetzner in the first place and purchasing the server. It’s a bit more than what I’m paying over on Porkbun right now, but having the flexibility of creating pretty much anything I want is hard to beat. As I learn more I’ll move my personal sites over to this VPS. Now this would be fun to test against 65,000 visitors, which Porkbun and Jekyll handled easily with my last blog post about Vercel’s predatory business model.

Anyway, I’m proud of the work done today and I’m glad I’m taking notes along the way. There’s much left to learn.

In summary, today I:

  • bought a Hetzner VPS
  • installed Dokku
  • configured SSH keys to access my VPS and deploy dokku apps
  • deployed a Sinatra test app over HTTP

P.S. While Dokku was installing I read about Paul Ford’s tilde.club blog post and it’s gotta be one of my favorite posts of all time. I think it’d be cool to start one of those, or at least join one. We’ll see.

This blog in chronological order

Thursday May 08 2025 • 06:45 AM

I created the route for posts index where all posts are rendered in chronological order, so I can read the entire blog in one page.

I also made the delete button red because earlier today I accidentally deleted a short post thinking it was an edit button, so I don’t want that to happen again lol

Learning more about SSH Keys

Thursday May 08 2025 • 09:35 PM

I stayed up late last night reading about the Tildeverse1. I watched James Tomasino’s introduction video on YouTube and read over Paul Ford’s original blog post about tilde.club2.

Access to these public unix servers is granted via SSH keys. I have many old SSH keys on my machine, most I presume are no longer being used, but I’m afraid of breaking my Github or something.

I’m slowly learning about the ~.ssh/config file (which I used last night for dokku) and earlier today I set up Mojave as its own host, meaning I can login as root with the very simple ssh mojave command.

I took some time to go over access logs in my VPS. I was surprised to see the huge amount of failed login attempts from random bots all over the place. I need to disable password access on root but I’m taking my time to do so to avoid locking myself out lol.

Adding users to Mojave

Thursday May 08 2025 • 10:43 PM

I found this helpful video from CJ over at the Syntax podcast where he goes over some basics about self-hosting and VPS safety measures.

I dusted off my Tab S7+ and reused the SSH keys I had generated last year for GitHub access to login to Mojave.

First, as root I created a new user: $ adduser nyoki. At the prompt for a new password I provided a different one from the root user and left the default info blank.

Adding SSH key access for nyoki from Tab S7+

Over on Café Quito, I created the .ssh directory in the new user’s home directory (/home/nyoki).

Just for fun, I ran $ cat ~/.ssh/id_ed25518.pub on the tablet and proceeded to manually type the entire SSH key into the /home/nyoki/.ssh/authorized_keys file.

This worked wonderfully. Like magic, typing ssh nyoki@IP_ADDRESS on the tablet logged me in without prompting for a password.

Cool.

Adding a new SSH key to access Mojave as nyoki from Café Quito

I’ll be generating a new set of SSH keys for Café Quito to login as nyoki.

First I ran $ ssh-keygen -t ed25519 -a 100 (command taken from tilde.team) and manually wrote the filename and the comment, but the alternative $ ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_custom -C "" does it all in one line.

I added the public key to nyoki’s /home/nyoki/.ssh/authorized_keys file. To login by simply typing ssh nyoki@mojave from my terminal, I edited my local ~.ssh/config file:


Host mojave
  Hostname ID_ADDRESS
  User nyoki
  IdentityFile ~/.ssh/id_ed25519_mojave

That did the trick.

Signing up for Tilde.team

Thursday May 08 2025 • 11:41 PM

Now that I feel a bit more familiar with managing SSH keys, I sent a request to sign up for Tilde.team. I hope they get back to me soon, I’m excited to play around in a public unix server. I can’t believe I never got around to do it during the pandemic, which is when I began to get interested in them in the first place after discovering Gopher and the Gemini protocol.

Let's Encrypt with Dokku

Friday May 09 2025 • 12:43 AM

To enable access over HTTPS Dokku provides a Let’s Encrypt plugin.

First, I installed the plugin by running sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

Then it was a matter of setting an email for the app (last command) and running

$ dokku letsencrypt:enable test-app

And that was it! The test app is available via HTTPS at https://test-app.bucareli.co

Letsencrypt commands

Additional commands:
    letsencrypt:active                      Verify if letsencrypt is active for an app
    letsencrypt:auto-renew []               Auto-renew app if renewal is necessary
    letsencrypt:cleanup                     Remove stale certificate directories for app
    letsencrypt:cron-job [--add --remove]        Add or remove a cron job that periodically calls auto-renew.
    letsencrypt:disable                     Disable letsencrypt for an app
    letsencrypt:enable                      Enable or renew letsencrypt for an app
    letsencrypt:help                             Display letsencrypt help
    letsencrypt:list                             List letsencrypt-secured apps with certificate expiry times
    letsencrypt:revoke                      Revoke letsencrypt certificate for app
    letsencrypt:set   ()   Set or clear a letsencrypt property for an app

Removing PasswordAuthentication in Mojave

Friday May 09 2025 • 01:53 AM

I wasn’t aware of how often bots try to brute force their way in to web applications until I spun up my own VPS. By running the tail -f /var/log/auth.log command I can see current (failed) attempts to log in happening in real time.

I’ve been managing my server through the nyoki user (which has admin privileges), and the more I read about managing a server it becomes clear that having root access available by password authentication is dangerous.

Disabling Password Authentication

In the /etc/ssh/ssdh_config I updated this line:

57: PasswordAuthentication no 

Next, I had to edit this other file $ sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf and also disable the password authentication there.

Now I can only sign in to Mojave with an SSH key. So cool 😎

Dokku Logs

Friday May 09 2025 • 01:55 AM

With the Sinatra test app deployed, realtime logs are visible by running $ dokku logs -t app-name.

Deploying a Sinatra App to Dokku with Custom Domain

Friday May 09 2025 • 03:46 AM

First I create the dokku app with $ dokku apps:create app-name.

As long as there’s a git repository for the given project, all I gotta do is add the dokku remote according to my SSH config and push to the main branch.

$ git remote add dokku dokku@dokku-server:app-name

$ git push dokku main

Setting a subdomain

For now I’m deploying to a subdomain, so in Porkbun I’m adding an A Record with the Host as the app name and the Answer/Value equal to Mojave’s public IP Address.

SSL Certificate

To make the app accessible via HTTPS, first I set the e-mail associated with the registration:

dokku letsencrypt:set app-name email email@address.com

Next I enable LetsEncrypt on the app:

$ dokku letsencrypt:enable app-name

That’s it!

Setting a Regular Domain

I’m transferring an app from Heroku to my VPS but the process is the same. In Porkbun, I set the A Record to the Mojave IP Address. Then I set the CNAME Record to the desired domain.

Dokku Domain

On the Dokku side, I add a new domain with this command: $ dokku domains:add app-name domain.com

HTTPS

Lastly, I run $ dokku letsencrypt:enable app-name and that’s it!

That’s one production app migrated from Heroku to my VPS :-)

Deploying a Jekyll build to Mojave

Friday May 09 2025 • 07:44 AM

I’m using the nginx server because Dokku installed it and I didn’t want to have to learn yet another tool like Apache.

Directory for storing sites

The first thing I did was create a directory for each the new static site.

sudo mkdir -p /var/www/test-blog.example.org/html

User Permissions

Next I gave my user permission to modify the directory with sudo chown -R $USER:$USER /var/www/test-blog.example.org (I have myself permissions one directory above)

Build Process

I built the site locally with my custom build rake task (which is nothing more than JEKYLL_ENV=production bundle exec jekyll build).

Sync Process

Next I uploaded the files via rsync with rsync -avz --delete _site/ user@host:/var/www/test-blog.example.org/html

I had Gemini 2.5 Flash summarize the command above in T3.chat:

this command is designed to efficiently and accurately synchronize the contents of the local _site/ directory to the remote /var/www/sites/example.org directory, preserving file attributes, compressing data during transfer, and deleting any files on the remote side that are no longer in the local source.

Nginx Server Blocks

DigitalOcean has a nice explanation article about setting up Nginx server blocks.

When using the Nginx web server, server blocks (similar to virtual hosts in Apache) can be used to encapsulate configuration details and host more than one domain on a single server.

sites-available directory

Since I want this Jekyll test blog to be available at test-blog.example.org, I copied the default configuration file sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/test-blog.example.com and updated the contents.

/etc/nginx/sites-available/test-blog.example.org file and added an nginx config:

server {
    listen 80;
    server_name test-blog.example.org;

    root /var/www/sites/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enabling nginx blocks

After saving the changes above I enabled the site by creating a symlink between my config file and the sites-enabled directory.

$ sudo ln -s /etc/nginx/sites-available/test-blog.bucareli.co /etc/nginx/sites-enabled

Test and Restart Nginx

Next I run a test and restart nginx with

$ sudo nginx -t && sudo systemctl reload nginx

Porkbun domain

As long as the A Records are pointed to the IP Address of the server, it should work.

HTTPS

Since Dokku is no longer handling SSL, it’s gotta be done manually.

First I installed certbot and some other dependencies.

sudo apt update
sudo apt install certbot python3-certbot-nginx

Next I run sudo certbot --nginx -d test-blog.example.org and I’m prompted for my e-mail.

Screenshot-2025-05-09-at-7-39-25-AM.png

I said yes to the EFF.org newsletter because I liked how simple this whole process was. Done!

Day 3 Redux — Jekyll Sites

Friday May 09 2025 • 07:59 AM

I spent more time than I should have trying to make dokku work for Jekyll, and that was a mistake because it’s a fundamental misunderstanding of both tools. Jekyll is a static site generator, whereas dokku is a platform as a service for running web apps.

All I really needed to do was sync the static files generated by Jekyll with my VPS. I wasn’t sure how to approach the nginx configurations but they aren’t as daunting as I expected.

Summary of Day 3 of Commit Redux:

  • Created my user on the VPS so I’m no longer root for anything
  • Completed a full transfer of powRSS from Heroku to Mojave 🥳
  • Spent a lot of time trying to make Dokku and Jekyll play along
  • Deployed a test Jekyll blog by configuring nginx to serve static files

I’m happy with this progress! The big one remaining is Rails. Once I can get that migrated to my own VPS, I won’t really have to spend so much on Heroku for small hobby apps. I’ll leave that for the Atelier’s enterprise projects.

Up next is working more on the backend for this blog to get it deployed!!

Prepping Mojave for a sqlite App

Friday May 09 2025 • 03:42 PM

Update I ended up discarding the sqlite idea for production since dokku has a hard time accessing the host filesystem and the database file is read-only.

First I’ll create a directory on the host for the database. Last night I read a bit about the Linux filesystem hierarchy standard1 and the /var/ directory, which is where I’ll be saving the sqlite file.

$ mkdir -p /var/lib/dokku/data/storage/commit-redux

Next I create the app

$ dokku apps:create commit-redux

That way I can mount the persistent storage from the host (Mojave) to the dokku container

$ dokku storage:mount commit-redux /var/lib/dokku/data/storage/commit-redux:/app/db

Update /config/environment.rb

Over in the app, I updated my code to connect to the production database when deployed to production.

db_path = if ENV['RACK_ENV'] == 'production'
  'db/production.sqlite3'
else
  'db/development.sqlite3'
end

set :database, { adapter: 'sqlite3', database: db_path }

Connecting Repo to Dokku Remote

I had the syntax wrong so this took me a bit to fix but the right command is:

$ git remote add dokku dokku@dokku-server:commit-redux

Add database files to .gitignore

I was just about to git push to dokku but that would’ve also pushed the database files itself, so I modified the .gitignore file and now we’re good.

Push to dokku

Now I’m pushing to dokku hoping it works out ✌️

Update: Ha, I had some issues with not specifying the ruby version in my Gemfile but after adding it it was good to go.

Porkbun

Now I’ll add the Porkbun A Records to point to the domain name. I’m actually gonna go a bit off script here and add it to my personal website’s domain as a subdomain at commit-redux.enocc.com

Configure domain in dokku

Since dokku was using my global domain host, I had to update it.

$ dokku domains:set commit-redux commit-redux.enocc.com

SSL

Then I set the SSL (email first) then enable LetsEncrypt.

Deployment

I deployed but visiting the website shows me an internal server error. I suspect it’s because I haven’t run database migrations on production, so I run $ dokku run commit-redux rake db:migrate and I’m shown this error:

SQLite3::CantOpenException: unable to open database file (SQLite3::CantOpenException)

Looks like I haven’t created the production database file. I’ll sync the development file to Mojave by syncing from Café Quito my user directory, since I do have write permissions there, and then over on the host I transfer the file to /var/lib/dokku/data/storage/commit-redux.

After updating permissions with

sudo chown -R dokku:dokku /var/lib/dokku/data/storage/commit-redux
sudo chmod -R u+rwX /var/lib/dokku/data/storage/commit-redux

I was now looking at a different error: ActiveRecord::StatementInvalid: SQLite3::ReadOnlyException: attempt to write a readonly database: (ActiveRecord::StatementInvalid)

  1. https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html 

Commit Redux with Postgresql in Production

Friday May 09 2025 • 04:06 PM

I think trying to get sqlite3 working with dokku is more trouble than it’s worth because the container has tons of permission issues with the persistent storage. Time to use postgresql in production.

Postgresql dokku plugin

$ dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres

Create Postgres db service in dokku

$ dokku postgres:create commit-redux-db

Link service to app

$ dokku postgres:link commit-redux-db commit-redux

This showed an error because I hadn’t added the pg gem to the Gemfile. I also had to fix my environment.rb file.

if ENV['RACK_ENV'] == 'production'
  set :database, ENV['DATABASE_URL']
else
  set :database, { adapter: 'sqlite3', database: 'db/development.sqlite3' }
end

So with this changed, I ran $ dokku postgres:link commit-redux-db commit-redux again, but it said the db was already linked.

I ran the migration with $ dokku run commit-redux rake db:migrate

Which showed:

rake aborted!
Errno::EACCES: Permission denied @ rb_sysopen - db/schema.rb (Errno::EACCES)

It seems like this was due to how I had configured my environment.rb file, so I fixed it with:

db_config = if ENV['RACK_ENV'] == 'production'
  { adapter: 'postgresql', url: ENV['DATABASE_URL'] }
else
  { adapter: 'sqlite3', database: 'db/development.sqlite3' }
end

set :database, db_config

Now let’s hope migrations work 🤞

$ dokku run commit-redux rake db:migrate

and… same issue :-(

Restarting Commit Redux Deployment

Friday May 09 2025 • 04:25 PM

I deleted the production.sqlite3 file in the /var/ directory, deleted the dokku container and began again. I updated my .gitignore to ignore the db files and pushed to dokku.

Running $ dokku run commit-redux rake db:migrate worked like a charm this time around.

Just gotta update the domain again and run the LetsEncrypt script.

$ dokku domains:set commit-redux commit-redux.enocc.com

$ dokku letsencrypt:enable commit-redux

And it’s live!! 🥳

New rake task to populate seeds.rb

Saturday May 10 2025 • 10:52 PM

[main 8956963] New rake task to populate seeds.rb
   3 files changed, 242 insertions(+)
   create mode 100644 db/seeds.rb
   create mode 100644 lib/tasks/export_seeds.rake

Every post written in Commit Redux so far has been stored in the development sqlite database. While this is okay for local development, yesterday I spent way too much time trying to make new dokku containers connect with persistent storage on the host computer, and even when it did the database was read-only.

On the other hand, deploying with dokku and using postgresql for production makes things infinitely simpler.

Now the challenge is to take all the blog posts I’ve been writing here and populate the production database with them. To do this I’ll be writing a Rake task.

Creating a new task

I’ll follow the Rails directory structure and create the file /lib/tasks/export_seeds.rake.


# lib/tasks/export_seeds.rake
namespace :db do
  desc "Export data from development database to seeds.rb format"
  task export_seeds: :environment do
    File.open("db/seeds.rb", "w") do |file|
      file.puts "# Auto-generated seed data"
      
      Post.find_each do |post|
        file.puts <<~RUBY
          Post.create!(
            title: #{post.title.inspect},
            content: #{post.content.inspect},
            created_at: #{post.created_at.utc.iso8601.inspect},
            updated_at: #{post.updated_at.utc.iso8601.inspect}
          )
        RUBY
      end

    end
    puts "✅ Seed data written to db/seeds.rb"
  end
end

Update config/environment.rb

Next I update my environment file to import the new rake task.


require 'sinatra/activerecord/rake'
require './config/environment'
import './lib/tasks/export_seeds.rake'

The seeds.rb file

With elements in this format in the seeds.rb file I can push this file to production and run dokku run commit-redux rake db:seed to populate the blog with the words you’re reading right now 😎


# Auto-generated seed data
Post.create!(
  title: "Hello, world!",
  content: "This is the very first post in my new Sinatra app for blogging :)",
  created_at: "2025-05-07T07:47:38Z",
  updated_at: "2025-05-07T07:47:38Z"
)

And now this blog has all the posts I wrote during development this past week, and new posts can continue being written directly on the site here.

Update: I inadvertently created a bug by also dumping the ID data. Totally unnecessary, so I removed that column from the export task.

Bugfix: Resetting Production DB Post ID Sequence

Sunday May 11 2025 • 06:21 AM

After seeding the production database, I came across an interesting bug. When creating new posts, the ID would start at 1. Since the previous posts already had those IDs, and these were automatically incremented with each new post being seeded, creating new posts resulted in an error.

ActiveRecord::RecordNotUnique - PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "posts_pkey":

DETAIL:  Key (id)=(5) already exists.

This led me into a bit of a side quest to investigate running particular SQL queries within dokku.

Listing dokku services

First I listed my dokku services to remind myself of the name of the Postgresql service.

$ dokku postgres:list

=====> Postgres services
commit-redux-db

Connecting to the database

Next I connected to the database with $ dokku postgres:connect commit-redux-db to run the query:

commit_redux_db=# SELECT setval(pg_get_serial_sequence('posts', 'id'), MAX(id)) FROM posts;

That fixed things ✌️

User Authentication

Sunday May 11 2025 • 06:34 AM

I’ll keep authentication simple.

I looked over the Rails 8 authentication generator to study some of the classes it creates (User, Sessions, Current) and concerns. I liked the authenticated? method so for now it’s the only one I’ll borrow.

Sessions in Sinatra

The Sinatra documentation has a good writeup on setting cookies via the session hash. In development, I assigned the output of ruby -e "require 'securerandom'; puts SecureRandom.hex(64)" to an environment variable called SESSION_SECRET to sign session data.

Next I updated app.rb.


#app.rb
enable :sessions

set :session_secret, ENV.fetch('SESSION_SECRET')

use Rack::Session::Cookie, secret: settings.session_secret

With sessions in place I created new routes for authentication and an ERB template. I borrowed that authenticated? helper method along with require_authentication and set up buttons for managing these posts behind a simple conditional statement.

Environment Variables

To set the environment variable in dokku, I ran:

$ dokku config:set commit-redux SESSION_SECRET=$(openssl rand -hex 64)

The line above set the variable and restarted my dokku container. To verify it had been set I ran $ dokku config commit-redux which listed all my environment variables.

I deployed those changes and now I’ve got very simple authentication 🌱

Day 4 and 5 Redux — Deploying a Sinatra App with a Postgresql Database

Sunday May 11 2025 • 07:22 AM

I thought I was going to use sqlite for my production database too, but after failing to get it working with dokku I opted for postgres.

To get Commit Redux online, I

  • installed the postgres db service in dokku
  • linked the service to the app
  • created a seeds.rb file with a custom rake task
  • ran migrations
  • set domain and enabled lets encrypt
  • reset the production db id sequence with a custom SQL query
  • added user authentication

Website Redesign

Monday May 12 2025 • 05:41 AM

Last Thursday while researching SSH keys and the tildeverse I signed up for an account at the tilde.team public unix server. They haven’t gotten back to me yet and there’s a decent chance they won’t, since usage of these pubnixes has largely fallen out of favor, which probably means the sysadmins don’t spend as much time receiving SSH key requests or onboarding new members.

On their site they have a list of users with links to their personal websites. Most of them are blank or haven’t been updated in years, but I found a couple of blogs that were using a tool called BashBlog which allows people to write using their text editor of choice and publish to their websites from the command line. I looked over the project’s GitHub repository and the initial announcement by Carles Fellonosa written back in September of 2011 and I really liked the design.

BashBlog

I had been using Water.css as a drop-in stylesheet while I took care of the backend development and the site looked like this:

Early Design

Starting last night I took the time to recreate the BashBlog design, add a dark mode, and make it responsive. I’m pretty happy with how it turned out. It looks old for sure, it reminds me of the early 2000s blogs hosted on Wordpress and Blogger. I know I’ll update the design as time goes on but for now I think it’ll be fun to write here.

Here are screenshots of the home screen and the article view!

Home

Article View

Working with ENV variables

Monday May 12 2025 • 10:10 AM

While working on redesigning this site’s UI I ended up finding other aspects to refactor. One of the reasons I made Commit Redux is to control this bad habit since each of these posts corresponds to a specific commit, otherwise I end up going on programming tangents to resolve unrelated albeit important things, but the time I spend on those adds up.

For a brief window of time during earlier development, I was handling authentication by comparing the form parameters submitted to the server with a hard-coded string in a conditional statement. Yeah, yeah, absolutely awful, but now I’m older and wiser and I’ll do it properly with environment variables.

First, I’ll be adding the .env file to my .gitignore and then creating it.

Sidenote: TextMate doesn’t show dotfiles by default, but focusing on the file browser panel and doing ⌥⌘i will show them.

Screenshot-2025-05-12-at-11-04-12-AM.png

Next, I’m installing the dotenv gem and requiring it at the top of my app.rb file (require 'dotenv/load') so I can load environment variables from a .env file in development instead of creating them in my ~/.zshrc config, which I’d done before to set the SESSION_SECRET variable 🤦🏻‍♂️.

Setting ENV variables in Dokku

To set environment variables for the Dokku container the syntax looks like this:

$ dokku config:set app-name VAR1="value1" VAR2="value2"

Executing the command will restart the container.

Lastly, I replaced the strings from the conditional with the environment variables and deployed these changes, so now authentication is just a tiny bit more secure :-)

Building an Atom (RSS) feed

Tuesday May 13 2025 • 06:39 AM

Like the BashBlog announcement post I shared a couple of days ago says, “a blog’s magic is in the RSS feed”, so this commit was dedicated to building it!

First I’m installing the builder gem that ships with Rails since it provides all the necessary builder objects for generating XML markup and data structures.

Next, I’m creating a route at /feed.xml (the way Jekyll does it) which will return the generated Atom/XML file. Within this block I define an instance variable for the posts I want to include in the feed, set the content_type header to application/atom+xml, and call the rendering method with the :atom template as its only argument.


get '/feed.xml' do
  @posts = Post.order(created_at: :desc).limit(20)
  content_type 'application/atom+xml'
  builder :atom
end

Atom Feed Template


xml.instruct! :xml, version: "1.0", encoding: "UTF-8"

xml.feed xmlns: "http://www.w3.org/2005/Atom" do
  xml.id         "https://commit-redux.enocc.com"
  xml.title      "Commit Redux"
  xml.updated    (@posts.first&.updated_at || Time.now).iso8601
  xml.link       href: "https://commit-redux.enocc.com/feed.xml", rel: "self"
  xml.link       href: "https://commit-redux.enocc.com"

  @posts.each do |post|
    xml.entry do
      xml.title     post.title
      xml.id        "https://commit-redux.enocc.com/posts/#{post.id}"
      xml.link      href: "https://commit-redux.enocc.com/posts/#{post.id}"
      xml.updated   post.updated_at.iso8601
      xml.published post.created_at.iso8601
      xml.summary   summarize_post_content(post.content, 200)
      xml.content   post.content, type: "html"
    end
  end

end

For the xml.summary object I had issues with the built-in truncate method (something about being unable to fetch an integer?!) so I opted for defining my own little helper called summarize_post_content which grabs the first 200 characters of a post’s content and appends an ellipsis.

To make sure the feed is detected automatically by RSS clients, I’ll add a link tag to the layout template’s head element.

I use Vivaldi as my main web browser and it comes with an RSS previewer, so that’s what I used to check the Atom/RSS feed had been constructed successfully. A couple of days ago I wrote about migrating PowRSS from Heroku to Mojave, so I also checked this site’s RSS feed over on the app and everything seems to be working 🥳

The last step will be adding an RSS icon somewhere on the site’s header or footer, but that’s beyond the scope of this commit :-)

My Writing Process

Thursday May 15 2025 • 08:36 AM

I often think of the William Gibson interview for the Paris Review in which he says that he starts off every writing day by reading his drafts from the very beginning until reaching what he finished writing the night before, and that’s where he picks up.

Interviewer: What’s your writing schedule like?

Gibson: When I’m writing a book I get up at seven. I check my e-mail and do Internet ablutions, as we do these days. I have a cup of coffee. Three days a week, I go to Pilates and am back by ten or eleven. Then I sit down and try to write. If absolutely nothing is happening, I’ll give myself permission to mow the lawn. But, generally, just sitting down and really trying is enough to get it started. I break for lunch, come back, and do it some more. And then, usually, a nap. Naps are essential to my process. Not dreams, but that state adjacent to sleep, the mind on waking.

Interviewer: And your schedule is steady the whole way through?

Gibson: As I move through the book it becomes more demanding. At the beginning, I have a five-day workweek, and each day is roughly ten to five, with a break for lunch and a nap. At the very end, it’s a seven-day week, and it could be a twelve-hour day. Toward the end of a book, the state of composition feels like a complex, chemically altered state that will go away if I don’t continue to give it what it needs. What it needs is simply to write all the time. Downtime other than simply sleeping becomes problematic. I’m always glad to see the back of that.

Interviewer: Do you revise?

Gibson: Every day, when I sit down with the manuscript, I start at page one and go through the whole thing, revising freely.

Interviewer: You revise the whole manuscript every day?

Gibson: I do, though that might consist of only a few small changes. I’ve done that since my earliest attempts at short stories. It would be really frustrating for me not to be able to do that. I would feel as though I were flying blind. The beginnings of my books are rewritten many times. The endings are only a draft or three, and then they’re done. But I can scan the manuscript very quickly, much more quickly than I could ever read anyone else’s prose.

My Writing Processes

I’ve tested out multiple writing setups over the years, each one different from the next depending on the kind of writing I need.

When writing notes and academic papers, I’ve always used the notes app in my phone and Microsoft Word. For notes, the phone is convenient but formatting is a pain. For papers and long documents, drafting with Word has been good enough as I mainly need a table of contents, sections and footnotes, but longer documents would be difficult to edit because the program would slow down. I used Word for my editing process for two reasons. The main reason was to send the document to my Kindle, where I could comfortably read and edit long texts without staring at a bright screen or having to print the pages. Sharing the Word document to the Kindle via e-mail was easy and convenient since Amazon takes care of the formatting. The second reason of course is because Word is the de facto standard in academia and publishing, and submissions often asked for Word documents.

For personal writing this process was terrible. Most of my online writing is technical, more often than not including blocks of code, links, footnotes, citations and images. Word is simply not the tool for this (try adding syntax highlighting for your favorite programming language), especially if the end product will be hosted on my own websites anyway. For this type of writing, I have enjoyed using Markdown and Jekyll, but this approach isn’t perfect either. The truth is there is no such thing as a perfect writing process, since every writer has to develop their own process over time. I am sharing mine here because I enjoy reading about people’s writing processes and I’ve learned a trick or two this way.

Jekyll

Jekyll is a fantastic tool, and static sites are underrated. I could write a whole entry about this topic alone. My issue with Jekyll is entirely personal and it’s due to what I would characterize as a lack of discipline, or perhaps a lack of separation of concerns.

When I write a document within my Jekyll setup, I start a new Markdown file in my text editor, running a local server so I can preview the formatting as it will appear online. Part of the fun in using Jekyll is to be in full control of the site’s appearance. I’ve spent hours sometimes updating styles, choosing fonts, modifying spacing, tweaking line heights, swapping syntax highlighting themes… and I love doing this. I find great joy in writing for the web because I am in control of my words and their appearance. I know I can use a theme designed by someone else and simply focus on writing, but I wouldn’t have as much fun.

The problem starts with having my entire site’s files right next to me. It’s so easy to get distracted with changing things when all I have to do is open a file, update a line or two, and check out the new changes in my browser window. I can’t avoid this if I’m writing within a Jekyll site, since my documents are stored in the same directory as my program files. When I’m ready to publish what I’ve written, I inevitably open the command line and manage my changes with git, so the line between writing and programming gets blurred every time.

Separation of Concerns

When I started Commit Redux that was exactly the problem I sought to fix.

If I want to change something about the site, I need to go out of my way and search for the project directory in my files, open up my text editor, add my updates, commit my changes to git, deploy these changes to the server, wait for my Dokku container to restart, and then I can see the final output.

Here’s an example of what I mean. I’m currently editing this post, and I’m noticing the “Update” button below my text area isn’t green like the “New Post” or “Share” button because I forgot to add the CSS class. I’ll get to doing that later, but I’m not going to stop writing right now because those files are out of my way at the moment. This separation of concerns keeps me from getting distracted. I have to make the conscious choice to stop writing and begin coding, whereas with Jekyll it all happens in the same space.

Commit Redux Editor

This has worked out well for the type of writing I want to share here, namely write-ups in which I go over what I’ve been programming, limited in scope to the last commit I made so the posts don’t get too long. It’s the same issue I have with Jekyll, which I wrote about this last Monday:

While working on redesigning this site’s UI I ended up finding other aspects to refactor. One of the reasons I made Commit Redux is to control this bad habit since each of these posts corresponds to a specific commit, otherwise I end up going on programming tangents to resolve unrelated albeit important things, but the time I spend on those adds up.

Having identified this recurring issue in my own writing process, now I can share a new approach I’ve adopted, especially for personal notes and long documents where editing is more important than drafting.

Pandoc

At one point in the interview, Gibson talks about printing his manuscript at every stage.

Interviewer: How much do you write in a typical day?

Gibson: I don’t know. I used to make printouts at every stage, just to be comforted by the physical fact of the pile of manuscript. It was seldom more than five manuscript pages. I was still doing that with Pattern Recognition, out of nervousness that all the computers would die and take my book with them. I was printing it out and sending it to first readers by fax, usually beginning with the first page. I’m still sending my output to readers every day. But I’ve learned to just let it live in the hard drive, and once I’d quit printing out the daily output, I lost track.

With Pandoc, I’ve found a happy medium between writing within the confines of my text editor and seeing my writing in a more final form. Like I mentioned earlier, I’ve mostly relied on Microsoft Word for papers and long documents. A considerable appeal to this approach when it comes to editing is getting a feel for the document as it will appear once it’s printed. I also like making notes on the page margins (or on the Kindle) and letting the project mature over time while I gradually incorporate my changes.

Pandoc Example

I can’t overstate the convenience of Markdown when it comes to formatting, I much prefer it to Word, but I don’t want to be responsible for formatting my document when I’m deep in the process of writing a paper or a story. That’s where Pandoc comes in.

Pandoc is an open source universal document converter created by John MacFarlane, a philosophy professor at UC Berkeley. The program accepts various filetypes for input and output. I’ll be using Markdown for input and PDF for output, but other formats include Word, ODT, HTML, EPUB, LaTeX, and many others.

Per Pandoc’s documentation:

To produce a PDF, specify an output file with a .pdf extension:

$ pandoc test.txt -o test.pdf

By default, pandoc will use LaTeX to create the PDF, which requires that a LaTeX engine be installed.

My preferred text editor for Markdown is GNU nano. I mainly use it for its paragraph justification feature, where Ctrl J keeps line length uniform while respecting long lines (usually for links or footnotes). I also love its interface. My personal website is even modeled after it!

To keep long documents manageable, I’ve been breaking them up into manageable sections and using pandoc to combine them into one PDF. Here is a sample file structure for a typical project:

.
├── cover.md
├── section1.md
├── section2.md
└── section3.md

I use the cover page file to set the front matter (pandoc only reads front matter from one file) and add a \newpagedirective. For everything else, I let the command line and pandoc take care of everything.

The contents of my cover.md file:

---
title: Hello, world!
author: Fulan Ito
date: Thu 15 May 2025
---

\newpage

The other files are just Markdown. No front matter, nothing other than the text itself. Since Pandoc can accept multiple documents for input, I sort in whatever order makes sense for the project (alphabetical, last modified, creation, etc) and output a list after passing cover.md as the first argument.

$ pandoc cover.md section*.md -o example.pdf

That’s all there is to it!

Pseudodrafts with Stimulus.js

Thursday May 15 2025 • 01:36 PM

In my last commit, I added the first bit of JavaScript to this blog, following the principle of progressive enhancement.

Andy Bell explains it like this:

Progressive enhancement is a design and development principle where we build in layers which automatically turn themselves on based on the browser’s capabilities. Enhancement layers are treated as off by default, resulting in a solid baseline experience that is designed to work for everyone.

We do this with a declarative approach which is already baked in to how the browser deals with HTML and CSS. For JavaScript — which is imperative — we only use it as an experience enhancer, rather than a requirement, which means only loading when the core elements of the page — HTML and CSS — are already providing a great user experience.

I’m using JavaScript to store the contents of the editor in localStorage when I’m drafting a new post. This way refreshing the page or navigating away won’t wipe the contents of a post before I publish. To do this I’m using Stimulus.js.

Stimulus is a great library for handling JavaScript interactions, or, as the team at 37signals puts it, for “sprinkling” interactivity throughout HTML. It enhances static HTML by providing a set of annotations (HTML element attributes) like data-controller and data-action which connect JavaScript objects to those elements.

From the Stimulus documentation:

These JavaScript objects are called controllers, and Stimulus continuously monitors the page waiting for HTML data-controller attributes to appear. For each attribute, Stimulus looks at the attribute’s value to find a corresponding controller class, creates a new instance of that class, and connects it to the element.

You can think of it this way: just like the class attribute is a bridge connecting HTML to CSS, Stimulus’s data-controller attribute is a bridge connecting HTML to JavaScript.

With Stimulus, I’ll be able to slowly (progressively) add features to the blog. For now, this works out as a very primitive implementation of a drafts feature. Later on, it would be nice to have a preview for the rendered Markdown.

The Jolly Tea Pot and Monospaced Fonts

Monday May 19 2025 • 10:20 AM

In my last commit, I updated the font family in the Commit Redux editor to a monospace font.

[main 239d98c] Updated textarea font family to monospace
 1 file changed, 3 insertions(+), 1 deletion(-)

Yesterday I redesigned my personal website once more, inspired by the design of The Jolly Tea Pot, a blog from Nicolas Magand about the web and software. I’ve also been looking through sites on the 512KB Club website, which is how I found JTP in the first place.

In a blog post called Why I like monospace fonts he talks about writing his drafts with monospaced fonts. I hadn’t realized that I’ve done the same for a long time, even using the same SF Mono font!

This quote particularly resonated with me:

A draft set in a monospaced font feels like it can be further edited and chewed on furthermore, like a raw material, like code. It feels like a work in progress.

I relate to that feeling wholeheartedly, which is why I’ve changed the font family within the Commit Redux editor. It makes drafting easier, and now the editor is a bit more similar to my usual setup with nano.

Updating my Jekyll Blogging Rakefile

Friday May 23 2025 • 10:22 AM

To make the management of posts easier on my Jekyll sites, I’ve been maintaining a custom Rakefile. Today I updated it with two new tasks:

  • Listing all posts
  • Listing recent posts

I have also namespaced tasks (e.g., rake posts:all shows all posts). As I wrote in the My Writing Process post, one of the things that made Jekyll difficult to work with was the management of text files, especially when using a text editor like TextMate, which exposed post files and site files alike.

For a bit now I’ve been using nano as my text editor for blog posts on my main site, but editing older files was tedious, especially with my file hierarchy _posts/YYYY/MONTH and each file name being YEAR/MONTH/YYYY-MM-DD-some-title.md.

Now my rake tasks look like this:

$ rake -T

rake build         # Build the site with JEKYLL_ENV=production
rake posts:all     # List all blog posts
rake posts:new     # Create a new post with front matter
rake posts:recent  # List recent blog posts
rake serve         # Serve site locally

For example, running $ rake posts:recent shows something like this:

Recent Posts:

01. | _posts/2025/mayo/2025-05-23-discovery-tools-for-independent-websites.md
02. | _posts/2025/mayo/2025-05-20-back-on-gemini.md
03. | _posts/2025/mayo/2025-05-15-my-writing-process.md
04. | _posts/2025/mayo/2025-05-03-pings.md
05. | _posts/2025/abril/2025-04-05-fetching-favicons.md

Now it’s as easy as copying a path and running $nano [filename] to write / edit via the command line.

Tasks

To namespace the tasks, Rake has a namespace keyword.

namespace :posts do 

end

I added two constants in my Rakefile for the posts directory and the array of all post files.

POSTS_DIR = "_posts"
POST_FILES = Dir.glob("#{POSTS_DIR}/**/*.md").sort_by { |f| File.mtime(f) }.reverse

The Dir.glob method returns an array of the entry names selected by the argument. I set it to the _posts directory along with the /**/*.md selector, which matches all files in the current directory and all subdirectories. This is necessary because prior to 2025 my posts weren’t organized by month.

The block I’m passing to sort_by uses the File::mtime method to sort files by modification time and returns them in reverse order (most recently modified first).

Finally, I listed each post and formatted the output with a string formatter since I needed the index to be padded with a zero for single digits.

POST_FILES.each_with_index do |file, index|
  puts "%02d. | %s" % [index + 1, file]
end

That was it! 🌱

Colorizing Terminal Output

Friday May 23 2025 • 12:00 PM

Jekyll has support for drafts in the form of a dedicated _drafts directory. The files in this directory won’t have a date in the filename, and to see these drafts when running a local server, the Jekyll process has to be launched with the drafts flag.

$ jekyll serve --drafts

I’ve gotten into the habit of “drafting” by adding a published: false property directly in a post’s front matter. This saves time by not having to rename files or move them between directories.

I modified my Rakefile to output the status of each post.

Rakefile output

Most of the update is contained within a new method called get_post_status(file) which looks like this:

def get_post_status(file)
    content = File.read(file)

    if content =~ /\A---\s*\n(.*?)\n---\s*\n/m
      front_matter = YAML.safe_load($1, permitted_classes: [Time])
      status_text = front_matter["published"] == false ? "unpublished" : "published"
      padded = status_text.ljust(11)

      return status_text == "published" ? "\e[0;32m#{padded}\e[0m" : "\e[0;33m#{padded}\e[0m"
    else
      "unknown".ljust(11)
    end
  end

RegEx

\A---\s*\n(.*?)\n---\s*\n
A RegEx for capturing Jekyll Front Matter

The RegEx above (courtesy of an LLM) is designed to match the formatting of a Jekyll front matter block.

The \A anchors the match to the very start of the string. The first ---\s*\n matches the opening YAML delimiter (---) possibly followed by spaces and a newline. The core of the expression, (.*?)\n, is a non-greedy match that captures everything up to the first newline before the closing delimiter, allowing it to extract the contents of the front matter. The final part, ---\s*\n, matches the closing YAML delimiter (again allowing optional spaces) followed by a newline. The m modifier at the end enables multiline mode so that . in (.*?) can match newline characters as well.

ANSI Color Codes

I found this GitHub Gist with ANSI Color Codes. Adding these to the published / unpublished strings changed the space padding around the post status, so I refactored the method to pad the string prior to adding the color code.

Output Ruby String Format

For the final output, I added another item to the format string.

POST_FILES.each_with_index do |file, index|
  post_status = get_post_status(file) # published, unpublished
  puts "%02d. | %s | %s" % [index + 1, post_status, file]
end