| @ブログ

On This Day

日記系のアプリ( Day One とか)やランニング系のアプリにある "On This Day" 的な機能をこのブログのトップページに付けてみた。今日を含む前後 3 日間ずつの合計 7 日分の範囲に収まる過去年の記事を取得してきてランダム表示する。動作検証していたら「 21 年前」みたいな表示が出てきたりしてぎょっとした。去年は 20 周年だったみたいだ。なにかやればよかった。 30 周年になるときは祝賀会でもやろう。

| @ブログ

メッセージ機能

著者である自分に非公開でメッセージを送れる機能を付けた。今日のブログのコメント欄は死んでいて使う人なんてほぼいない(スパマーだけ)が、非公開ならスパマーはコメントする意味がないし、第三者には見られたくないけど著者にだけ何か言いたい人はいるかもしれないという考えのもとに実装してみた。加えて公開コメントに関しては親コメントと子コメントの概念を入れて返信っぽく見せられるようにした。

なおコメントもメッセージもメールアドレスの入力が必須です。メッセージの場合、公開されないので送信者が内容を後からたどれるように控えのメールを送る親切設計。罵詈雑言を僕に浴びせかけたい人はご利用ください。

| @ブログ

ポータルシット問いかけ君を右下の吹き出しからいつでも呼び出して語りかけられるようにした。

Dify の公式 iframe 埋め込み方式では、 Safari + ATOK の組み合わせで日本語の変換を確定しただけでフォームが送信されてしまい不便だった。今回は F/E を自前実装(と言っても Claude 任せ)したので Safari + ATOK で日本語変換確定しただけでフォームが送信される問題は解消された。

リンクを貼ったときに OGP を展開するようにしてみたけど、これにより自サイトの OGP 参照時に HTTP リクエストが発生して呼び出し先のページの OGP 読み込み君も動いてしまい、場合によっては何重にも OGP 読み込み君が動いてしまうという仕様バグがあることが判明した。これによって puma のプロセスを占有して 504 エラーが多発するようになったので、自サイトの OGP 読み込み時は HTTP 通信を経由せず DB から直接 OGP カードを生成するようにした。

Dify の機能も活用して会話オープナー(最初のデフォルト質問)やフォローアップ(次の質問提案)をやるようにしてみた。

会話オープナー フォローアップ
バブルボタン呼び出し版のポータルシット問いかけ君

その他の機能と同様にこの機能もほぼ自分専用の機能なのだが、非常に面白い機能が追加できたと感じていて満足している。

| @ブログ

Amazon Product Advertising API が終了して Creators API へと移行となるようだ。

Claude Code にガチャガチャしてもらいながら、 Creators API に対応するコードを用意したが、 Amazon からは 403 Forbidden ステータスが返る。

{
  "message": "Your account does not currently meet the eligibility requirements.",
  "reason": "AssociateNotEligible",
  "type": "AccessDeniedException"
}

Creators API の FAQ を見ると以下のように記載がある。

参加資格

クリエイターズAPIの対象者は誰ですか?

承認されたクリエイターアカウントが必要です。

クリエイターズAPIでPA APIにアクセスする資格があるかどうかはどうすればわかりますか?

Creators API を通じて PA API にアクセスするには、承認されたクリエイターアカウントに加えて、過去 30 日間に対象となる売り上げが 10 件以上ある必要があります。

この件について書いてある記事もあった。

30 日間で 10 件は結構きびしい。よく売れる商品を紹介したらこのくらい買ってもらえることもあるが、このブログでは基本的に月 2 ~ 3 件くらいしか売れていないので、弱小個人ブログではもう Amazon のアフィリエイトはできなくなると思った方が良さそうだ。

以前、別の記事で以下のように書いたことがある。

Google としてはどこの馬の骨とも付かない個人がやってる泡沫ブログにトラフィックを流してインターネットの果実を分け与えるつもりはないのだろう

Amazon も同様の方針に舵を切るようだ。

インターネットで個人が不労所得を得るのがどんどん難しくなってきている。ある意味正しい方向に向かっているのかもしれないが、なんだか夢のない話だなぁと思ってしまう。途上国の若者がブログを書いてアフィリエイト収入で暮らしているみたいなストーリーが成り立たなくなってきてる。これだと暇な情報技術がある若者はフィッシングサイトを作って儲けるとかしかない。

| @ブログ

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

| @ブログ

カテゴリー検索

ふと思い立ってカテゴリーで検索できるようにしたら便利だろうと改修してみた。 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

| @ブログ

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
end