実践
JavaScriptでgenko.meのページ送り、保存済み下書きのプレビュー、Webhookの署名検証を実装する。
導入で作った接続を使い、実際のサイトで必要になる3つを組みます。
ページ送り
配信APIにページ番号のパラメータはありません。limitとoffsetを送り、返ってきたtotalCountからページ数を計算します。
import { genko } from "./genko.js";
export const PER_PAGE = 10;
export async function getPage(page) { const { contents, totalCount } = await genko.apis.blog.list({ limit: PER_PAGE, offset: (page - 1) * PER_PAGE, orders: "-publishedAt", });
return { contents, totalCount, totalPages: Math.max(1, Math.ceil(totalCount / PER_PAGE)), hasPrevious: page > 1, hasNext: page * PER_PAGE < totalCount, };}limitは1〜100の整数です。範囲外を渡すと400になります。offsetが総件数を超えた場合はエラーではなく、contentsが空配列で返ります。
ページ番号はURLから受け取ることになります。数値でない値や0以下をoffsetの計算に通さないでください。
const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1", 10) || 1);全件をまとめて取る場合
記事数が数百件までで、ビルド時に一度だけ読むなら、getAll()が使えます。内部でlimit: 100ずつ自動でページングします。
const all = await genko.apis.blog.getAll({ orders: "-publishedAt" });リクエストのたびにgetAll()を呼ばないでください。 1回の呼び出しで記事数に応じた数のリクエストが飛び、1分300リクエストの制限に届きます。ページ表示にはlimitとoffsetを使い、getAll()はビルド時や定期処理に限ります。
なお、offsetでページを辿る途中に記事が公開・削除されると一覧の位置がずれ、重複や取りこぼしが起きます。時点を固定したバックアップの用途には使えません。
保存済み下書きのプレビュー
管理画面の「プレビュー」は、設定したURLテンプレートのdraftKeyを置き換えて開きます。受け取ったキーをそのまま詳細取得に渡すと、保存済みの下書きが返ります。
import { genko } from "./lib/genko.js";
export async function getPostForDisplay(id, draftKey) { // draftKeyを付けたときだけ下書きが対象になる。付けなければ公開中のものだけ。 return genko.apis.blog.getOrNull(id, { draftKey });}素のfetchなら、draftKeyをクエリに足すだけです。
const url = new URL(`https://api.genko.me/v1/your-workspace/blog/${encodeURIComponent(id)}`);if (draftKey) url.searchParams.set("draftKey", draftKey);管理画面のAPI設定(/w/your-workspace/apis/blog/settings)で、プレビューURLテンプレートを設定します。
https://your-site.example/blog/{id}?draftKey={key}押さえておく点です。
- 対象は単一コンテンツとオブジェクト形式だけです。一覧に
draftKeyを付けても下書き一覧にはなりません。 - キーは1〜64文字です。空文字を送ると
400、形式は正しくても一致する下書きがなければ404になります。 - プレビューの結果をキャッシュしないでください。 公開表示とは別の内容で、URLにキーが含まれます。
- プレビュー中のページには
noindex, nofollowとno-referrerを設定し、公開URLを組み立てる共有ボタンやcanonicalは出さないでください。 - 未保存の編集内容は反映されません。管理画面で保存してから開きます。
Webhookで再生成・再検証する
genko.meは、コンテンツが変わったことを小さなJSONでPOSTします。記事本文は送られてこないので、必要なら受信後に配信APIから取り直します。
シークレットを設定したWebhookはx-genko-timestamp(Unix秒)とx-genko-signature(timestamp.rawBodyのHMAC-SHA256)を送ります。SDKは300秒を超える時間差を拒否し、署名を定数時間で比較します。署名はJSONへ変換する前の、生の本文に対して検証します。
SDKのverifyGenkoSignature()が、Node.jsではnode:cryptoを、Edgeランタイムなどではcrypto.subtleを使い分けて検証します。
import { createServer } from "node:http";import { verifyGenkoSignature } from "@genko-me/sdk";
const secret = process.env.GENKO_WEBHOOK_SECRET;
createServer((request, response) => { if (request.method !== "POST" || request.url !== "/genko-webhook") { response.writeHead(404).end(); return; }
const chunks = []; request.on("data", (chunk) => chunks.push(chunk)); request.on("end", async () => { const rawBody = Buffer.concat(chunks).toString("utf8"); const valid = await verifyGenkoSignature({ secret, rawBody, signatureHeader: request.headers["x-genko-signature"], timestampHeader: request.headers["x-genko-timestamp"], }); if (!valid) { response.writeHead(401).end("Invalid signature"); return; }
const payload = JSON.parse(rawBody); if (payload.api === "blog") { console.log(`[genko] ${payload.event} ${payload.entry?.id ?? "(なし)"}`); // ここでキャッシュの破棄や再ビルドを起動する。 }
response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ received: true })); });}).listen(3000);Web標準のRequestを受け取るフレームワーク(Hono、Cloudflare Workers、Denoなど)では、await request.text()が生の本文です。
const rawBody = await request.text();const valid = await verifyGenkoSignature({ secret, rawBody, signatureHeader: request.headers.get("x-genko-signature"), timestampHeader: request.headers.get("x-genko-timestamp"),});ExpressやFastifyでJSONのボディパーサーを有効にしている場合、パース後のオブジェクトをJSON.stringify()し直したものでは検証できません。キーの順序や空白が元の本文と一致しないためです。生の本文を保持する設定(Expressならexpress.json({ verify }))を使ってください。
受信側で気をつけること
| 事項 | 内容 |
|---|---|
| タイムアウト | 10秒。自動リトライはありません |
| 重複 | 配信ログからの手動再送で同じイベントが複数回届きます。何度処理しても壊れない作りにします |
| イベント | 手動で「公開」にした保存はentry.publishedではなくentry.created/entry.updatedです |
| 非公開への変更 | entry.updatedでentry.statusがdraftになります。古い公開ページの無効化も必要です |
| テスト送信 | eventはwebhook.test、entryはnullです。entryがない場合を必ず扱ってください |
重い再生成を同期で待つと10秒を超えます。受信は即座に200を返し、処理はキューやバックグラウンドへ渡してください。
Next.jsのISRを再検証する場合は、SDKのプラグインnextRevalidateが使えます。登録・配信ログ・失敗時の調べ方はWebhookにまとめています。
関連
最終更新: 2026年10月14日