増分SSG — 変更検知APIで差分だけ再ビルド

中級 約15分 最終更新 2026-10-14

投稿が数千件あるサイトを、更新のたびに全ページ再生成するのは時間がかかります。変更検知API(cms.changes)を使うと、前回ビルド以降に変更があった公開コンテンツ(新規・更新・公開・予約公開の到来・非公開・削除)だけを特定して、変更分だけ再生成できます。Webhook / GitHub Actionsが「ビルドを起動する」のに対し、変更検知APIは「どのページを再生成するかを決める」補完関係です。

前提:APIキーとサイトコードを取得済みで、クイックスタートまで完了していること。SDK @sharedan/cms-sdk を利用します。

1. 仕組み(差分と最終ビルド日時)

サイト単位の「最終ビルド日時」はCMS側が保持します。cms.changes.fetch() はその日時以降に更新された公開コンテンツの差分を返します。ポイントは3つです。

  • 冪等:fetch() は最終ビルド日時を進めません。ビルドが成功したときだけ complete() で前進させます(失敗時は次回に同じ差分を再取得)。
  • action:各変更は upsert(現在公開中=再生成)か delete(非公開・削除・未公開=出力から除去。元々無ければ無視)のどちらかです。
  • full_resync:true のときはサイト全体を再生成します(changes に現在公開中の全コンテンツが upsert で並びます。詳細は §4)。
GET /api/v1/changes(レスポンス例)
{
  "as_of": "2026-10-14T12:34:56+09:00",
  "full_resync": false,
  "changes": [
    { "type": "post", "post_type": "news", "key": "summer-campaign",
      "action": "upsert", "updated_at": "2026-10-14T10:00:00+09:00" },
    { "type": "post", "post_type": "news", "key": "old-notice",
      "action": "delete", "updated_at": "2026-10-14T11:20:00+09:00" }
  ]
}
type意味(c.key の中身)
post 投稿(c.post_type が種別slug、c.key が投稿slug)
faq FAQ(c.key は ID の文字列)
content 固定コンテンツ(c.key は key)
category カテゴリ(c.key は slug)
form フォーム定義(c.key は key)

2. 差分を取得して変更分だけ生成

差分を取得し、upsert は該当ページを再生成、delete は出力から除去します。投稿詳細ページ(多数)だけを増分にし、トップや一覧ページは毎回再生成する構成が扱いやすいです(一覧はFAQ・固定コンテンツ・カテゴリの変更も反映するため)。

build-changes.ts
import { ShareDanCmsClient } from '@sharedan/cms-sdk';

const cms = new ShareDanCmsClient({
  baseUrl: process.env.CMS_API_ORIGIN,
  apiKey: process.env.CMS_API_KEY,
  siteCode: process.env.CMS_SITE_CODE,
});

// 1. 前回ビルド以降の差分を取得する(最終ビルド日時はCMSが保持。fetch では進めない=冪等)
const feed = await cms.changes.fetch();

if (feed.full_resync) {
  // 初回/カテゴリ・タグの変更/手動ビルド時は true。サイト全体を再生成する
  await buildEverything();
} else {
  for (const c of feed.changes) {
    if (c.type === 'post' && c.action === 'upsert') {
      // 現在公開中=再生成(post は c.post_type が種別slug、c.key が投稿slug)
      const post = await cms.posts.get(c.post_type, c.key);
      await writePage(post);
    } else if (c.type === 'post' && c.action === 'delete') {
      // 非公開・削除・未公開=出力から除去(元々無ければ無視)
      await removePage('news', c.key);
    }
    // faq / content / category / form も type + action で同様に扱う
  }
}
ヒント:静的サイトのデプロイで既存ファイルを上書き(ミラー)する場合、delete の分は明示的に削除する必要があります(アップロードだけでは古いページが残るため)。

3. ビルド完了を通知する

ビルドとデプロイが成功したあとにだけ、取得した as_of を complete() に渡して最終ビルド日時を前進させます。これで次回の fetch() はこの時刻以降の差分だけを返します。

build-changes.ts(続き)
// 2. ビルド/デプロイが成功したら、取得した as_of で最終ビルド日時を前進させる。
//    失敗時は呼ばない=次回に同じ差分を再取得できる(取りこぼさない・冪等)。
await cms.changes.complete(feed.as_of);
失敗時は呼ばないこと:ビルドやデプロイが失敗したときに complete() を呼ぶと、その差分を「反映済み」とみなして次回から取りこぼします。成功時のみ呼び出してください。

4. 全体ビルドになる変更

次の場合は full_resync: true になり、サイト全体を再生成します。

  • 初回(最終ビルド日時が未設定)。
  • カテゴリ・タグ名の変更:カテゴリ名・タグ名は多数の投稿ページに波及するため、個別差分ではなく全体ビルドになります。
  • サイト設定画面からの手動ビルド:管理画面で手動ビルドした場合も安全側で全体ビルドになります。

一方、フォーム定義の変更は局所的なため全体ビルドにはならず、type: "form" の差分として返ります。また、予約投稿が公開時刻に到来したとき(予約go-live)も、upsert の差分として現れます。

動くサンプル:Astro 静的サイトでの増分ビルドの実装例(差分計画 → 変更ページのみ生成 → 削除ページの除去 → 完了通知)は、Webhook / GitHub Actions でビルドを起動と組み合わせるのが定番です。まずトリガーでCIを起動し、その中で cms.changes を使ってどのページを再生成するかを決めます。
FEEDBACK

このページはお役に立ちましたか?

改善要望・不具合・ドキュメントの分かりにくい点など、お気軽にお寄せください。いただいた内容は今後の改善に役立てます。

返信をご希望の場合のみご記入ください。

スパム対策(レートリミット・ハニーポット・送信速度チェック)はサーバ側で自動適用されます。