AetherEchoesEngineering
Vol.042026年5月9日
Engineering#00561 min4605 view

ISR + Webhook で「公開した瞬間に反映」を作る

Rails 側の publish イベントから Next.js の /api/revalidate に Webhook を投げる。即時反映と CDN 効率を両立する設計。

SoSoraEndo2026年5月9日1 min460

なぜ ISR + Webhook なのか

ブログのようなサイトを Next.js で組むとき、選択肢は概ね 3 つある。

  • SSR(毎リクエスト server side で組む)— TTFB が長くなり SEO に不利
  • SSG(ビルド時に全 HTML 生成)— 公開即反映が難しい
  • ISR(SSG + 期限切れで再生成 + on-demand revalidate)— 両方の中間

個人ブログの規模だと ISR + on-demand revalidate が圧倒的に楽。「公開した瞬間に反映する」流れを 1 日で組める。

全体図

┌─ Admin で記事を published に ─┐
│                                │
│  Rails after_commit            │
│       │                        │
│       ▼                        │
│  Sidekiq job                   │
│       │ POST /api/revalidate   │
│       ▼                        │
│  Next.js handler               │
│       │ res.revalidate(path)   │
│       ▼                        │
│  ISR キャッシュ更新             │
└────────────────────────────────┘

Rails 側の after_commit から Sidekiq に enqueue。Sidekiq は Next.js の API route を Bearer 認証で叩く。Next 側は受けたパスを res.revalidate() で再生成する。

Rails 側

class Post < ApplicationRecord
  after_commit :enqueue_revalidate, if: :saved_change_to_status?

  private

  def enqueue_revalidate
    RevalidatePostsWorker.perform_async(slug)
  end
end

status カラムが変わった時だけ enqueue。draft の編集中は実行されない。

class RevalidatePostsWorker
  include Sidekiq::Worker

  def perform(slug)
    paths = ["/posts/#{slug}", '/', '/posts', "/categories/#{Post.find_by(slug: slug)&.category&.slug}"]
    HTTP.headers('Authorization' => "Bearer #{ENV['NEXT_REVALIDATE_SECRET']}")
        .post("#{ENV['NEXT_PUBLIC_URL']}/api/revalidate", json: { paths: paths.compact })
  end
end

Next.js 側

// pages/api/revalidate.ts
export default async function handler(req, res) {
  if (req.headers.authorization !== `Bearer ${process.env.NEXT_REVALIDATE_SECRET}`) {
    return res.status(401).json({ error: 'unauthorized' });
  }
  const paths = (req.body?.paths || []) as string[];
  await Promise.all(paths.map((p) => res.revalidate(p).catch(() => null)));
  res.json({ revalidated: paths });
}

Bearer トークンで Rails ↔ Next の通信を認証。Promise.all で複数パスを同時再生成。

詰まったところ

実装中に詰まった点を 3 つ:

  1. Bearer 認証なし で組むと CSRF / 不正リクエストの的になる。NEXT_REVALIDATE_SECRET を必ず環境変数で渡す。
  2. revalidate の失敗を握りつぶす か明示的に handle するか — 私は catch して null 返すようにした(一部失敗しても他は更新したい)。
  3. dev では効かない。ISR は production build のみ。docker compose -f docker-compose.prod.yml up で確認する。

まとめ

ISR + Webhook の組み合わせは、個人ブログ規模の Next.js + 任意 backend で 公開即反映 を実装する標準パターンになりつつある。半日で組める。組んだ後は「公開ボタンが効く」感覚が手に入って気持ちいい。

静的サイト + 動的更新の中間が、いまの個人サイトには一番合う。

Tags

Reaction

Share

X (Twitter)