はじめに
前編の記事では、Docker Compose を使って Rails 8 + MySQL の環境を整え、さらに importmap / Turbo / Stimulus / Tailwind を導入してモダンな開発基盤を作りました。
後編となる今回は、いよいよ Shlink API と連携して URL 短縮 UI を実装します。
ただし普通に「フォーム送信 → 結果ページへ遷移」といった従来型の動きではなく、Rails 7 以降で標準になった Turbo を活用し、ページ全体をリロードせずに部分更新する“ほぼSPA”風のUIを作っていきます。
最終的には:
- トップページにURL入力フォーム
- 送信すると Turbo で部分的に差し替え → 短縮URL表示
- クリックでコピー(Stimulus)
- 「もう一度短縮する」でフォームに戻る
という、軽快なUIをRailsで実現します。
Shlink API の準備
Shlink 側で API キーを取得しておきましょう。
Rails 側では .env
に設定を置きます。
SHLINK_BASE_URL=https://shlink.example.com
SHLINK_API_KEY=your_api_key_here
.gitignore
に .env
を追加しておくのを忘れずに。
サービスクラス
API呼び出し用のクライアントを用意します。
app/services/shlink_client.rb
require "faraday"
require "json"
class ShlinkClient
def initialize
@base_url = ENV.fetch("SHLINK_BASE_URL")
@api_key = ENV.fetch("SHLINK_API_KEY")
end
def shorten(long_url)
conn = Faraday.new(url: @base_url) do |f|
f.request :json
f.response :json
end
res = conn.post("/rest/v3/short-urls") do |req|
req.headers["X-Api-Key"] = @api_key
req.body = { longUrl: long_url }
end
res.success? ? res.body["shortUrl"] : nil
end
end
コントローラ(Turbo Stream 対応)
app/controllers/short_urls_controller.rb
class ShortUrlsController < ApplicationController
def new
end
def create
client = ShlinkClient.new
@short_url = client.shorten(params[:long_url])
respond_to do |format|
if @short_url
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"result",
partial: "short_urls/result",
locals: { short_url: @short_url }
)
end
format.html { redirect_to root_path, notice: "短縮しました" }
else
@error = "URLの短縮に失敗しました"
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"result",
partial: "short_urls/form",
locals: { error: @error, long_url: params[:long_url] }
)
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
end
ビュー(Turbo Frame とパーシャル)
new.html.erb
<div class="max-w-xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-4">URL短縮</h1>
<%= turbo_frame_tag "result" do %>
<%= render "form", error: nil, long_url: nil %>
<% end %>
</div>
_form.html.erb
<% if defined?(error) && error.present? %>
<div class="mb-4 rounded bg-red-50 text-red-700 p-3"><%= error %></div>
<% end %>
<%= form_with url: short_urls_path, class: "space-y-4" do |f| %>
<div>
<%= f.label :long_url, "長いURLを入力してください", class: "block mb-1" %>
<%= f.text_field :long_url,
value: (defined?(long_url) ? long_url : nil),
class: "border p-2 w-full rounded",
placeholder: "https://example.com/very/long?query=params" %>
</div>
<div>
<%= f.submit "短縮する", class: "bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700" %>
</div>
<% end %>
_result.html.erb
<div class="rounded border p-4 bg-white shadow-sm">
<div class="text-sm text-gray-500 mb-1">短縮URL</div>
<div class="flex items-center gap-2">
<a href="<%= short_url %>" target="_blank" class="text-blue-600 underline break-all"><%= short_url %></a>
<button
type="button"
class="text-xs px-2 py-1 rounded border hover:bg-gray-50"
data-controller="clipboard"
data-clipboard-text-value="<%= short_url %>"
data-action="click->clipboard#copy">
コピー
</button>
</div>
<div class="mt-4">
<%= link_to "別のURLを短縮する", "#", data: { action: "click->result#reset" }, class: "text-gray-600 underline" %>
</div>
</div>
Stimulus(コピー機能とリセット)
clipboard_controller.js
// app/javascript/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { text: String }
async copy() {
try {
await navigator.clipboard.writeText(this.textValue)
this.element.textContent = "コピー済み"
setTimeout(() => (this.element.textContent = "コピー"), 1200)
} catch (e) {
this.element.textContent = "コピー失敗"
}
}
}
result_controller.js
// app/javascript/controllers/result_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
async reset(event) {
event.preventDefault()
const response = await fetch("/", { headers: { "Turbo-Frame": "result" } })
const html = await response.text()
const frame = document.getElementById("result")
frame.innerHTML = html
}
}
Stimulus コントローラの生成コマンド:
docker compose run --rm web bin/rails generate stimulus clipboard
docker compose run --rm web bin/rails generate stimulus result
動作確認
docker compose up
- http://localhost:3000 を開く
- 長いURLを入力 → 「短縮する」クリック
- Turbo Frame 内だけが差し替わり、短縮URLが表示される
- 「コピー」でURLをクリップボードへ
- 「別のURLを短縮する」でフォームに戻る
全体リロードが発生しないので、SPAのような体験になります。
まとめ
これで 前後編が完結しました。
- 前編では Docker Compose × Rails 8 × MySQL 環境構築
- 後編では Shlink API を呼び出して Turbo/Stimulus でSPA風のUIを実装
これにより、Rails 8 の入門として環境構築からモダンUIまでひと通り体験できます。
さらに発展させるなら:
- 短縮URLの一覧と統計(クリック数表示)
- QRコード生成
- Tailwind で見た目を整える
などを追加すれば、本格的な Shlinkダッシュボード になります。
👉 完成版コードはこちら
https://github.com/enjoydarts/shlink-ui-rails
さらにこのプロジェクトは、その後 Claude Code を活用した「バイブコーディング」で機能を拡張し、認証・ダッシュボード・QRコード生成などを備えた本格的なアプリに進化しました。
その詳細は別記事でまとめますので、興味のある方はぜひご覧ください。
コメント