Adding live blog URL links to the post dashboard
It’s a small thing but I kept running into it: the /posts dashboard lists all my blog posts, and every time I want to read one on the actual blog, I have to manually navigate to it. The titles were just plain text. The natural action, clicking the title, did nothing useful.
The fix sounds obvious: make the title a link. But there’s a wrinkle. The URL pattern isn’t the same for every blog. My blogs table already had a base_url column (https://vdaluz.com), but the path between the domain and the slug, /blog/ for vdaluz.com, was hardcoded in the show page partial. That worked for one blog but would break the moment I added another blog with a different URL structure.
So I needed to make that path segment configurable.
What I built
Added a post_path_prefix column to blogs; a string like /blog that sits between base_url and the post slug. Then Post#live_url assembles the full URL:
def live_url
return nil if blog.base_url.blank? || pub_date.blank?
"#{blog.base_url.chomp('/')}#{blog.post_path_prefix}/#{slug}"
end
The pub_date gate is intentional. Drafts and posts with no publish date don’t have a live URL yet; showing a broken link is worse than no link.
The migration backfills existing rows to /blog so the current setup keeps working without any manual data fix:
reversible do |dir|
dir.up { Blog.update_all(post_path_prefix: "/blog") }
end
On the index, published post titles now link out with target="_blank" rel="noopener". Drafts fall back to the internal show page. The show page’s URL badge, which was previously hardcoded as "#{post.blog.base_url}/blog/#{post.slug}", now uses post.live_url instead, so there’s one source of truth for the URL shape.
The thing I almost missed
The issue spec said to add a live_url column to blogs. But base_url already existed and did the same job. Adding live_url would have been a redundant field with no clear owner. I scrapped that part of the spec and kept the existing column; the post_path_prefix addition was all that was actually needed.
Result
Click a published post title, land on the live post. Takes about 2 seconds less mental effort than it used to. Not glamorous, but this is the kind of thing that makes a tool feel like it was built for you.
Related reading
What happens when a job broadcasts to nobody
Closing the hero-image loop: insert-only frontmatter patching, a guard that caught real drift on its first run, and a Turbo broadcast with no listener.
A doc-drift fix that wasn't as boring as it sounded
Three audit items that each turned into something: a half-fixed claim, a quietly dead password reset, and a staging email that would have linked to production.
A 500 that was hiding inside a mounted engine's isolated routes
The jobs dashboard returned a 500 instead of a login page: bare route helpers resolve against the engine, not the app. One line, plus its dormant twin.
You might also find useful
Proton Drive
Encrypted cloud storage from the team behind Proton Mail.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreProton Mail
End-to-end encrypted email with zero-access architecture.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn moreProton VPN
Commercial VPN with NetShield filtering and a kill switch.
As a Proton Partner, I earn from qualifying purchases of Proton's privacy and security services (Pass, Mail, VPN, Drive).
Learn more