Tutorial
Build static pagination in Astro, render draft previews on request, and reflect updates with webhooks.
Add the features needed to operate the list and detail pages from Get started. Astro has no dedicated Pagination or View components, so you build these pieces yourself.
Pagination
The total count is known at build time, so generate a static route for each page number. Calculate the number of pages in getStaticPaths() and build that many HTML pages.
---import { client } from "../../../lib/genko";
const PER_PAGE = 6;
export async function getStaticPaths() { const perPage = 6; const { totalCount } = await client.blog.list({ limit: 1 }); const lastPage = Math.max(1, Math.ceil(totalCount / perPage)); return Array.from({ length: lastPage }, (_, i) => ({ params: { page: String(i + 1) } }));}
const page = Number(Astro.params.page);const { contents, totalCount } = await client.blog.list({ limit: PER_PAGE, offset: (page - 1) * PER_PAGE, orders: "-publishedAt",});const lastPage = Math.max(1, Math.ceil(totalCount / PER_PAGE));---<html lang="en"> <body> <main> <h1>Blog ({page} / {lastPage})</h1> <ul> {contents.map((post) => <li><a href={`/blog/${post.id}`}>{post.title}</a></li>)} </ul> <nav aria-label="Pagination"> {page > 1 && <a href={`/blog/page/${page - 1}`}>Previous</a>} {page < lastPage && <a href={`/blog/page/${page + 1}`}>Next</a>} </nav> </main> </body></html>getStaticPaths() has its own execution scope. Referencing PER_PAGE from outside the function inside it causes a PER_PAGE is not defined runtime error during the build. Define needed values inside the function too, as perPage is above. The page rendering code can still use the outer PER_PAGE.
These previous and next links lead to static HTML returned by the server. No client JavaScript is needed. Rebuild after adding or deleting entries because the page count changes (you can automate this with a webhook below).
Preview drafts
A draft's draftKey is unknown at build time, so render only this page on request. First add an adapter. This example uses @astrojs/node; choose the adapter for your host if it differs.
npm install @astrojs/nodeimport { defineConfig } from "astro/config";import react from "@astrojs/react";import node from "@astrojs/node";
export default defineConfig({ integrations: [react()], adapter: node({ mode: "standalone" }),});Set prerender to false at the top of the detail page and read draftKey from the query string.
---export const prerender = false;
import { client } from "../../lib/genko";import { ShareButtons } from "@genko-me/react/share-buttons";
const { id } = Astro.params;const draftKey = Astro.url.searchParams.get("draftKey") ?? undefined;const post = await client.blog.getOrNull(id!, draftKey ? { draftKey } : undefined);
if (!post) Astro.response.status = 404;---<html lang="en"> <body> <main> {post ? ( <> <h1>{post.title}</h1> <div set:html={post.body} /> <ShareButtons client:load url={`https://your-site.example/blog/${post.id}`} title={post.title} /> {draftKey && <p data-preview>Previewing draft</p>} </> ) : ( <p>Entry not found.</p> )} </main> </body></html>Removing getStaticPaths() from [id].astro means the whole route, including published entries, now renders on each request instead of at build time. The switch applies per file, so the list and pagination remain static. It still sends no JavaScript: prerender controls when server rendering happens, not how much JavaScript reaches the browser.
Set the preview URL template for this page in the dashboard's API settings (/w/your-workspace/apis/blog/settings).
https://your-site.example/blog/{id}?draftKey={key}Read the API key from process.env, not import.meta.env. In Astro v6 and later, import.meta.env embeds the value at build time. A value from .env stays fixed even if you later change only the host's environment variable. The Node adapter reads process.env at request time, which lets you rotate the key after deployment or vary it by environment. That is why lib/genko.ts in Get started uses process.env from the start. For adapters without process.env, such as Cloudflare, follow that adapter's documentation.
Rebuild or revalidate with webhooks
Most of this site is static. After you publish or update an entry, the old HTML remains live until a rebuild. Set the dashboard webhook destination to your host's deploy hook to rebuild automatically when an entry is saved. No receiver code is needed.
The draft-preview page ([id].astro with prerender = false) queries the delivery API on every request, so it shows the latest saved draft without rebuilding. It needs no revalidation.
Only if you cache delivery API responses in a CDN or reverse proxy do you need a small API route to clear that cache. Use verifyGenkoSignature from @genko-me/sdk to verify the webhook.
export const prerender = false;import type { APIRoute } from "astro";import { verifyGenkoSignature } from "@genko-me/sdk";
export const POST: APIRoute = async ({ request }) => { const rawBody = await request.text(); const valid = await verifyGenkoSignature({ secret: process.env.GENKO_WEBHOOK_SECRET!, rawBody, signatureHeader: request.headers.get("x-genko-signature"), timestampHeader: request.headers.get("x-genko-timestamp"), }); if (!valid) return new Response(null, { status: 401 });
// Call the CDN or reverse proxy cache-purge API here. return new Response(null, { status: 204 });};This route also needs prerender = false and therefore the adapter from the previous section. See Webhooks for event types, payloads, and delivery limits.
Next steps
Last updated: October 14, 2026