Skip to content
Development

Building blog-manager, part 1: the Blog model and a UI that doesn't look like a scaffold

By Victor Da Luz
rails blog-manager dev-log active-record-encryption

I’m building a Rails app to manage my blogs. The goal: one place to configure each blog’s GitHub repo, Medium token, and LinkedIn credentials, then automate the cross-posting later. I have two blogs now and the workflow is a mess of bookmarked tabs and tokens I keep forgetting. This fixes that.

This is part 1: the Blog model and the UI.

The Blog model: the credentials, not the CRUD

The CRUD is the boring part. The interesting part is the credentials. Each blog gets its own GitHub personal access token, a Medium integration token, and a LinkedIn OAuth token. None of those can sit in plaintext in SQLite.

My first instinct was attr_encrypted, which is what most tutorials reach for. Then I remembered Rails 7 shipped Active Record Encryption and I haven’t actually used it yet. The API is about as small as it gets:

class Blog < ApplicationRecord
  encrypts :github_token, :medium_token, :linkedin_access_token

  validates :name, presence: true
  validates :github_repo_url, presence: true
end

That’s the whole thing. No gem, no paired ciphertext columns, no IV columns to manage by hand. Rails does all of it, and the keys live in config/credentials.yml.enc under active_record_encryption, which is where they belong.

The fixtures gotcha that broke every test

I added the encrypted columns to blogs.yml and the whole suite immediately started throwing ActiveRecord::Encryption::Errors::Decryption on every test. The fixture loader bypasses model callbacks, so it writes raw strings into columns where Rails expects ciphertext, then blows up the moment anything reads them back.

The fix was to pull the encrypted columns out of the fixture file and use Blog.create! in the tests that actually need a token. I left a comment explaining why, so I don’t quietly undo it in six months.

A column for a feature I haven’t built yet

The LinkedIn token has a 60-day expiry, so I’m adding an expires_at column now even though the OAuth flow doesn’t exist yet. Cheaper than a migration later. The index view uses it to show “expired” in red and “expires soon” in amber when a token is within a week of dying.

A UI I’d actually use

The model took an afternoon. The UI took longer, because Rails scaffolds are functional and look like 2012 Bootstrap, and I want something I’ll enjoy opening every day.

I landed on a dark sidebar (#111110), a warm cream main area (#f5f3ee) roughly the color of old paper, and a terracotta accent (#c85e32) on the logo mark. For type I pulled Bricolage Grotesque for headings, which has a slightly rough editorial character that reads as intentional, DM Sans for body text, and JetBrains Mono for token fields. All from Google Fonts, nothing installed locally.

Each integration gets its own card in the form with a small icon: a mono field for the GitHub token, a yellow warning on the LinkedIn section about that 60-day expiry. In the table, credential status is a single colored dot, green for set, red for expired, amber for expiring soon, a dash for missing. The login page is full dark with a cream card floating in the middle and a terracotta sign-in button. If I’m the only user, it can still look good.

Two small things that cost me time

button_to in Rails 8 wraps the button in its own <form>. So a CSS class on the button doesn’t touch the form wrapper, and the default form display quietly broke my table cell alignment. The fix is form: { style: "display:inline;" } (or form_class:). Obvious once you know, annoying the first time.

The other one was bin/dev. Its shebang is #!/usr/bin/env sh, and that shell doesn’t source ~/.zshrc, so my asdf shims aren’t on $PATH. The old bin/dev tried to gem install foreman against the system Ruby 2.6 and failed. I fixed the symptom by adding foreman to the Gemfile and calling bundle exec foreman, and the root cause by exporting the shims path directly in ~/.zshrc, since asdf 0.19 dropped the old asdf.sh.

What’s next

Part 1 is a model and a UI. The actual point of the app is the next piece: pulling posts from the GitHub repo and showing them, which is where the syndication logic starts. That’s part 2.

Related reading

Meta

Starting a dev log

Why Imperfect Systems is keeping a public dev log, and how the blog is wired up under the hood.

Read