genko.meDocs

実践

JavaScriptでgenko.meのページ送り、保存済み下書きのプレビュー、Webhookの署名検証を実装する。

導入で作った接続を使い、実際のサイトで必要になる3つを組みます。

ページ送り

配信APIにページ番号のパラメータはありません。limitとoffsetを送り、返ってきたtotalCountからページ数を計算します。

lib/pagination.js
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の計算に通さないでください。

js
const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1", 10) || 1);

全件をまとめて取る場合

記事数が数百件までで、ビルド時に一度だけ読むなら、getAll()が使えます。内部でlimit: 100ずつ自動でページングします。

js
const all = await genko.apis.blog.getAll({ orders: "-publishedAt" });

リクエストのたびにgetAll()を呼ばないでください。 1回の呼び出しで記事数に応じた数のリクエストが飛び、1分300リクエストの制限に届きます。ページ表示にはlimitとoffsetを使い、getAll()はビルド時や定期処理に限ります。

なお、offsetでページを辿る途中に記事が公開・削除されると一覧の位置がずれ、重複や取りこぼしが起きます。時点を固定したバックアップの用途には使えません。

保存済み下書きのプレビュー

管理画面の「プレビュー」は、設定したURLテンプレートのdraftKeyを置き換えて開きます。受け取ったキーをそのまま詳細取得に渡すと、保存済みの下書きが返ります。

preview.js
import { genko } from "./lib/genko.js";
export async function getPostForDisplay(id, draftKey) {  // draftKeyを付けたときだけ下書きが対象になる。付けなければ公開中のものだけ。  return genko.apis.blog.getOrNull(id, { draftKey });}

素のfetchなら、draftKeyをクエリに足すだけです。

js
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テンプレートを設定します。

text
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を使い分けて検証します。

webhook.js
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()が生の本文です。

js
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日

目次