コメント欄をメールアドレス入力必須にした。スパム対策としてはあまり意味がないかもだが、今日の荒んだインターネットでは「ブログ主にメールアドレスを知られてもいいからコメントしたい」と思う人にコメントしてもらうくらいでちょうどいいのかもしれない。なお、メールアドレスは公開されません。
カテゴリーとタグでの検索、 ChatGPT によるタグ生成

ふと思い立ってカテゴリーで検索できるようにしたら便利だろうと改修してみた。 Tantiny には facet_query というものがあって、カテゴリー名などは検索インデックスの型を facet にしておくと、カテゴリーだけを検索対象にできる。実装してみたところ便利。カテゴリー内の記事一覧ページを表示する機能はあるが、本文やコメントなども読み込むため遅い。タイトルだけ一覧でがっと欲しいときにカテゴリー検索は便利。
同様にタグでも検索できるようにしてみた。こちらは term_query を使っている。本文の検索は検索キーワードを形態素解析してトークナイズされた文字列で検索しているが、タグ名での検索は exact match をするようにしている。なので過不足あればヒットしない。完全一致で検索したいときに便利。
ただ、これまで記事にタグを設定してこなかったのでタグ未設定の記事が多い。ということで ChatGPT に記事の内容を読ませてタグを自動生成してもらうようにした。ちょっとへんちくりんなタグを設定することもあるが、文脈を読んでタグを設定してくれる。本文中にキーワードはないけど記事の内容に合致するタグを選んできたりもする。賢い。
ちなみに今回 ChatGPT のモデルを gpt-4o から gpt-5-mini に変更した。 GPT 5 系に移行するためには Chat API から Responses API への移行が必要だった。最初それがわからず難儀したが、 Responses API の方がパラメーターがシンプルで使いやすい。例えば JSON のフォーマットを指定したいとき、 Chat API だと JSON Schema で定義を書いて渡す必要があったが、 Response API であればプロンプト中に「こういうフォーマットでくれ」と書けばそのフォーマットで返してくれる。すごい。
require 'openai'
class EntryTagGenerator
MODEL = 'gpt-5-mini'
PROMPT = <<~EOF
添付のブログ記事に関して、文章の内容を反映したタグを付与してください。ただし、以下の条件を遵守してください。
1. ブログ全体のタグの一覧は「 %s 」です。ふさわしいものがあればそこから利用してください。なければ新しいタグを考えて設定してください。
2. タグの数は最大で 5 個までとします。
3. すでにタグが付与されている場合は、現在のタグを加味して 5 個までタグを付与してください。現在のタグが内容にふさわしくなければ削除しても構いません。
4. タグは極力一語で構成されるようにしてください。固有名詞の場合はその限りではありません。
5. 略語は避けてください。固有名詞の場合はその限りではありません。
6. 「減量ダイエット」のような意味が重複する二つの単語で構成されるタグは付与しないでください。固有名詞の場合はその限りではありません。
7. タグは半角スペースを含んでも問題ありません。"GoogleAnalytics" ではなく "Google Analytics" でよいです。
8. レスポンスは JSON フォーマットで、以下のような形式にしてください。
\```json
{
"tags": ["tag1", "tag2"...]
}
\```
# ブログ記事 #
## タイトル ##
%s
## 本文
%s
## すでに付与済みのタグ
%s
EOF
attr_reader :response
def initialize(title, body, tags)
@title = title
@body = body
@tags = tags
@client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
end
def generate
@response ||= begin
request = @client.responses.create(
parameters: {
model: MODEL,
input: input,
reasoning: { effort: 'minimal' }
}
)
JSON.parse(request.dig("output", 1, "content", 0, "text").strip)
rescue => e
puts "タグ生成エラー: #{e.message}"
nil
end
end
def input
PROMPT % [Tag.joins(:entries).pluck(:name), @title, @body, @tags]
end
end要約用のフィールドを用意して ChatGPT に生成させた要約を入力できるようにした
OpenAI の API を利用してブログの本文の要約を自動生成する機能をつけてみた。ブログの編集画面に要約欄を追加し、チェックボックスにチェックが入っていれば OpenAI の API にリクエストを投げて記事本文の要約を生成して保存するようにした。

これも ChatGPT に設計を依頼してレビュー&手直ししながらやった。めっちゃ便利。要約生成君のコードはこんな感じ。
require 'openai'
class EntrySummarizer
MODEL = 'gpt-4o-mini'
def initialize(content)
@content = content
@client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
end
def summarize
response = @client.chat(
parameters: {
model: MODEL,
messages: [
{ role: "system", content: "以下のブログ記事を著者になりきって、日本語で簡潔に要約してください。文章の長さは200文字以内で、受動態表現と「ですます」調を避けて下さい。" },
{ role: "user", content: @content }
],
temperature: 0.3,
max_tokens: 150
}
)
response.dig("choices", 0, "message", "content").strip
rescue => e
puts "要約生成エラー: #{e.message}"
nil
end
endog:image を動的に生成
OGP の og:image を動的に生成する機能をブログに実装していた( 1 年半も前)。
記事本文中に画像がある記事であれば og:image は本文中に含まれる最初の画像を og:image として設定するようにしている。画像がない文章だけの記事の場合はこれまでサイトのロゴを og:image として表示していた。それだと金太郎飴っぽくなってしまうので、はてなブログとか Qiita とかがやってるみたいに、タイトルとサイトロゴを使って動的に og:image を生成して表示することにした。
- mini_magick をインストール
- 日本語表示用に NotoSansJP-ExtraBold.ttf をダウンロード
- タイトルを載せるための背景画像を作成(画像ソフトで作成)
- 動的に画像を生成するコードを記述
こだわりポイントとしては、日本語のタイトルの折り返し位置をいい感じにするために形態素解析して、ちょうどいい折り返し位置を決定するような処理を実装した。この辺のコードは結構頑張ってる。
def nm
@nm ||= Natto::MeCab.new(
userdic: File.expand_path('lib/tokenizer/userdic.dic'),
node_format: "%M\t%H\n",
unk_format: "%M\t%H\n"
)
end
def prepare_text(text:)
splitted_text = nm.enum_parse(text).map(&:feature)
row_length = 0
result = []
do_loop = true
while do_loop do
splitted_text.each.with_index(1) do |item, i|
result[row_length] ||= ''
if (result[row_length].length + item.length) > INDENTION_COUNT
row_length += 1
result[row_length] = ''
end
result[row_length] += item
do_loop = false if splitted_text.length == i
end
do_loop = false if ROW_LIMIT - 1 > row_length
end
result.each {|item| item.gsub!(/EOS\n\z/, '') }
if result[-1].length == 1
result[-2] += result[-1]
result.pop
end
result.map(&:strip).join("\n").gsub(/"/, '\"').chomp
end結果はこんな感じになる。

フォトギャラリーの実装

写真でふりかえるやつはフォトギャラリーとして実装した。
- PhotoSwipe を使えるように組み込む
- PhotoSwipe が期待するフォーマットで本文の HTML を書き出しつつ写真を S3 にアップロードする Rake タスクを作成
- PhotoSwipe の Dynamic Caption プラグインを組み込む
- ↑の Rake タスクから exiftool を呼び出し、 Exif データを HTML 内に Caption として埋め込む
- ↑の Rake タスクに緯度経度から住所を取得する機能を追加( Nominatim の API を利用)
ChatGPT に聞きながらやったらサクッとできてしまった。 AI 、ちょっとやる気がある人がその気になればサクッとプログラムを作れてすごい。 Rails で 10 分で Twitter もどきを作れる〜みたいなのがどんな種類のプログラムでもできる感じ。すごい世の中になった。
フォトギャラリーの機能が面白くて、むかし Flickr の存在を知ったときに猿のように画像をアップロードしてた頃を思い出した。あの頃はウェブ上に写真の Exif 情報が表示されるだけで面白かった。カメラの機種名とかレンズとか、焦点距離とか絞り値とか。自分のブログでそういうのをサクッと表示できてうれしい。 2025 年はもっと写真を撮ってブログにアップしていきたい。こういう発言、ウェブ縄文時代( 2000 年代)っぽい。
Flickr に関してはダメなサービスになってしまったし、悪口のような記事も過去に書いたけれど、 Flickr があるおかげで写真を撮ってアップロードするのが楽しかった側面はあったなぁと思う。 Flickr を使わなくなってから全く一眼カメラを使わなくなってしまった。
フォトギャラリーを作ったので、今後はまめにカメラを引っ張り出して写真を撮りたい。
写真で毎月をふりかえることにした
これから毎月写真でふりかえってみることにする。
SNS に写真を上げるだけではなく、自分で見え方まで考えて写真を整理してみたくなった。 Flickr に写真を上げてアルバムを作っていたようなことを自分のブログでやる感じ。
ギャラリーのライブラリは Medium Zoom だと足りなかったので PhotoSwipe に移行してみた。
過去の任意の日付の人気記事を確認できるようにした
人気記事を確認できるようにしたという記事を以前書いている。もう 7 年も前のことのようだ。
調べればこういうログ集約・集計系のサービスはあるようだったが、個人ブログなのでなるべくお金はかけたくない。
しばらくの間は集計後のログを捨てていたが、 2 年半前からログローテートするタイミングで日付ごとのファイルを書き出し保存するようにしていた。日付ごとのログファイルは Amazon の S3 にアップロードしてさくらの VPS サーバーからは待避させている。現状、 2022 年の 6 月 13 日のログから S3 上にデータが残っているので、その日以後の日ごとの人気記事を確認できるようにした。こんな感じ。
日付の選択は最初 React のライブラリを使って作ろうかと思ったが、そういや最近のブラウザーは <input type="date" /> とすると結構ちゃんとしたカレンダー UI と日付選択の機能を提供してくれるので楽をすることにした。
max と min を指定すると選べない日付は非活性にされるし、無理に選択すると form を submit したときにバリデーションしてくれる。
ブログの閲覧者からしたらいつどの記事が読まれていたかなんて興味ないだろうけど、著者である自分には興味深い情報になる。セルフホストのブログだからできる機能だ。