Adding Pexels image search to my blog manager
Medium wants a header image. My blog manager needed a way to pick one without leaving the app.
I’ve been building a Rails app that helps me syndicate posts from my Astro blog to Medium and LinkedIn. The cross-posting flow was mostly done; I could scan a GitHub repo for markdown files, render them as HTML, and push them to Medium as drafts. But there was one missing piece: Medium highlights a header image at the top of every post, and my app had no way to select one.
The obvious choice was Pexels. They have a clean API, a generous free tier, and an attribution requirement that’s easy to satisfy with a <figcaption>.
What I built
The Pexels section lives on the post detail page. There’s a search form pre-filled with the post title, a grid of results that loads without a full page reload, and a “Select” button per image. Once you pick one, the post card updates in place and the Medium import button activates.
The Turbo Frame approach made this simple. The search form targets a turbo_frame_tag "pexels_results", so the response replaces only that region of the page:
<%= form_with url: search_pexels_post_path(post), method: :get,
data: { turbo_frame: "pexels_results" } do |f| %>
<%= f.text_field :query, value: post.title.presence || post.slug %>
<%= f.submit "Search" %>
<% end %>
<%= turbo_frame_tag "pexels_results" %>
On the controller side, search_pexels renders the partial directly into the frame. Fast, no page transition, no scroll reset.
The Pexels auth quirk
Every other API I’ve integrated uses Authorization: Bearer {token}. Pexels doesn’t. Their auth header is just the raw API key:
req["Authorization"] = @api_key # correct
req["Authorization"] = "Bearer #{@api_key}" # wrong - 401
It’s documented, but easy to miss when copying from another client.
Where to store the API key
Per-blog vs. global: I have one Pexels account, so per-blog would mean entering the same key multiple times. I added a singleton AppSetting model instead:
class AppSetting < ApplicationRecord
encrypts :pexels_api_key
def self.current
first_or_create!
end
end
Active Record Encryption (already used for GitHub and Medium tokens) handles at-rest encryption. The settings page at /settings gives me a UI to paste the key without touching credentials.yml.enc.
Gating Medium import
The Medium import button now requires a Pexels image to be selected. The check lives in both the view (disabled tooltip) and the controller (redirect with alert); view-only gates get bypassed with a direct POST.
When DraftCreator runs
DraftCreator prepends the selected image as a <figure> before the post HTML body. The <figcaption> includes photographer attribution, satisfying Pexels’ requirements. Medium renders it at the top of the draft.
One test gotcha
minitest/mock was removed in Minitest 6.0.6; Minitest::Mock no longer exists. I dropped the stub-based controller test and relied on unit tests for Pexels::Client directly.
What’s next
The search_pexels action still reads from Rails.application.credentials; wiring it to AppSetting.current.pexels_api_key is the obvious one-liner follow-on. After that: LinkedIn cross-posting, which has the token but not the job.
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
AdGuard for iOS
System-wide ad and tracker blocking on iOS, no separate DNS server required.
As an AdGuard affiliate, I earn from qualifying purchases.
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 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 more