Tutorial
Implement pagination, preview saved drafts, and verify webhook signatures with JavaScript and genko.me.
Use the connection from Get started to add three features needed on a real site.
Pagination
The delivery API has no page-number parameter. Send limit and offset, then calculate the page count from the returned 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 must be an integer from 1 to 100. Values outside that range return 400. If offset exceeds the total count, contents is an empty array rather than an error.
You will read the page number from the URL. Do not use a nonnumeric or nonpositive value to calculate offset.
const page = Math.max(1, Number.parseInt(searchParams.get("page") ?? "1", 10) || 1);Fetch all entries
For up to a few hundred entries fetched once during a build, use getAll(). It automatically paginates in batches of limit: 100.
const all = await genko.apis.blog.getAll({ orders: "-publishedAt" });Do not call getAll() on every request. One call may make many requests, depending on the entry count, and reach the 300 requests per minute limit. Use limit and offset for page views, and reserve getAll() for builds or scheduled jobs.
If an entry is published or deleted while you page through results with offset, positions shift and you may get duplicates or miss entries. This is unsuitable for a point-in-time backup.
Preview drafts
The dashboard's Preview action opens a configured URL template with its draftKey substituted. Pass that key to the detail request to fetch the saved draft.
import { genko } from "./lib/genko.js";
export async function getPostForDisplay(id, draftKey) { // A draft is available only when draftKey is set; otherwise, only published entries are returned. return genko.apis.blog.getOrNull(id, { draftKey });}With plain fetch, add draftKey to the query string.
const url = new URL(`https://api.genko.me/v1/your-workspace/blog/${encodeURIComponent(id)}`);if (draftKey) url.searchParams.set("draftKey", draftKey);Set the preview URL template in the dashboard's API settings (/w/your-workspace/apis/blog/settings).
https://your-site.example/blog/{id}?draftKey={key}Keep these constraints in mind:
- Preview works for a single entry or an object API only. Adding
draftKeyto a list request does not produce a draft list. - The key is 1–64 characters long. An empty string returns
400; a well-formed key without a matching draft returns404. - Do not cache preview results. They differ from the published version, and the key is in the URL.
- Set
noindex, nofollowandno-referreron preview pages. Hide share buttons and canonical URLs that point to a public page. - Unsaved edits are not included. Save in the dashboard before opening the preview.
Rebuild or revalidate with webhooks
genko.me sends a small JSON POST when content changes. It does not include the entry body; fetch the entry from the delivery API after receiving the webhook if needed.
A webhook with a secret includes x-genko-timestamp (Unix seconds) and x-genko-signature (sha256= plus the HMAC-SHA256 hex of timestamp.rawBody). The SDK rejects timestamps outside 300 seconds and compares signatures in constant time. Verify the signature against the raw request body before parsing JSON.
The SDK's verifyGenkoSignature() uses node:crypto in Node.js and crypto.subtle in Edge runtimes and similar environments.
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 ?? "(none)"}`); // Clear the cache or start a rebuild here. }
response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ received: true })); });}).listen(3000);In frameworks that receive the Web-standard Request, such as Hono, Cloudflare Workers, or Deno, await request.text() gives you the raw body.
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"),});If Express or Fastify has a JSON body parser enabled, reserializing the parsed object with JSON.stringify() will not verify: key order and whitespace may differ from the original body. Configure the framework to retain the raw body (for Express, use express.json({ verify })).
Receiver considerations
| Item | Details |
|---|---|
| Timeout | 10 seconds. There are no automatic retries |
| Duplicates | Manual resend from delivery logs can deliver the same event more than once. Make handling safe to repeat |
| Events | Saving an entry by manually clicking Publish sends entry.created or entry.updated, not entry.published |
| Unpublish | entry.updated sets entry.status to draft. Invalidate the old public page too |
| Test delivery | event is webhook.test and entry is null. Handle the missing entry |
A slow rebuild can exceed the 10-second timeout. Return 200 promptly and hand work to a queue or background job.
For Next.js ISR, the SDK's plugin nextRevalidate can revalidate pages. See Webhooks for registration, delivery logs, and troubleshooting.
Related
- Query reference: how to use
filters,q,depth, andfields - Response values: JSON for images, references, and content blocks
- Errors and limits: causes by status and rate limits
Last updated: October 14, 2026