Tutorial
Build pagination, saved draft previews, and webhook handling with Nuxt server routes.
Add production features to the list and detail pages from Get started.
Pagination
The server fetches the next page. Add one POST route under server/.
import { loadGenkoPage } from "@genko-me/vue/server";
export default defineEventHandler(async event => { const { token, page } = await readBody<{ token: string; page: number }>(event); return loadGenkoPage(token, page);});Set pagination in server/api/blog.get.ts. { mode: "url" } puts the page number in the URL and supports browser Back. The default (pagination: true) replaces the list without changing the URL.
import { fetchGenkoList } from "@genko-me/vue/server";import { client } from "~/lib/client";
export default defineEventHandler(event => fetchGenkoList({ api: client.blog, fields: ["title", "publishedAt"], href: "/blog/{id}", limit: 6, orders: "-publishedAt", pagination: { mode: "url" }, searchParams: getQuery(event) as Record<string, string | string[] | undefined>,}));<script setup lang="ts">import { createPageLoader } from "@genko-me/vue";
const route = useRoute();const { data } = await useFetch("/api/blog", { query: route.query });</script>
<template> <main> <ListClient v-if="data?.ok" :initial="data.initial" :initial-page="data.initialPage" :fields="data.fields" :schemas="data.schemas" href="/blog/{id}" :pagination-options="{ mode: 'url' }" :load-page="createPageLoader({ url: '/api/genko-page', token: data.paginationToken! })" > <article> <h2><ListItem id="title" /></h2> </article> <template #pagination><Pagination mode="url" /></template> <template #empty><Null>No published entries yet.</Null></template> </ListClient> <Error v-else template /> </main></template>Without { query: route.query } on useFetch, opening /blog?page=3 directly returns page one from the server. /api/blog and /blog are separate URLs; forward the page's query explicitly. Then direct navigation, reload, and Back all SSR the correct page.
Set mode and param on ListClient's pagination-options, not on Pagination. React reads <Pagination mode="url" /> from JSX, but Vue cannot inspect template children. Pagination accepts only previous-label, next-label, class, and style. Match fetchGenkoList's pagination to ListClient's pagination-options.
The url in createPageLoader points to the server/api/genko-page.post.ts route created above. Pagination renders nothing when the list has only one page.
About pagination tokens
fetchGenkoList issues a signed paginationToken on the server and sends it to the browser with page data. Next.js can bind arguments to a Server Action; Nuxt uses your API route to exchange the token instead.
- The token contains no API key. It identifies only the fetch target and filters at issuance.
- It expires after one hour. Reload if it expires or the key changes.
- It can fetch only later pages of the list already shown.
- Pagination does not work with a client configured with custom
fetch; that produces a configuration error. - Match the pagination client's API key to the server's
GENKO_API_KEY. - Signing and verification use
node:cryptodirectly. Choose a Node.js-compatible Nitro preset such asnode-server.
Preview drafts
Pass searchParams to fetchGenkoEntry so it reads draftKey from the URL and fetches the draft without caching. It returns draft: true.
import { fetchGenkoEntry } from "@genko-me/vue/server";import { client } from "~/lib/client";
export default defineEventHandler(event => fetchGenkoEntry({ api: client.blog, id: getRouterParam(event, "id"), fields: ["title", "body"], searchParams: getQuery(event) as Record<string, string | string[] | undefined>,}));<script setup lang="ts">const route = useRoute();const { data } = await useFetch(`/api/blog/${route.params.id}`, { query: route.query });const draft = computed(() => data.value?.ok && data.value.draft);
useHead({ meta: draft.value ? [{ name: "robots", content: "noindex, nofollow" }] : [],});</script>
<template> <main> <EntryProvider v-if="data?.ok && data.entry" :entry="data.entry" :fields="data.fields" :schemas="data.schemas"> <PreviewNotice v-if="draft" /> <h1><ViewItem id="title" /></h1> <ViewItem id="body" /> <ShareButtons v-if="!draft" :url="`https://your-site.example${route.fullPath}`" :title="data.entry.title as string" /> </EntryProvider> <p v-else>No entry found.</p> </main></template>Set a preview URL in the dashboard API settings (/w/your-workspace/apis/blog/settings).
https://your-site.example/blog/{id}?draftKey={key}Save an entry and open Preview to see the draft on the same detail page. No extra Route Handler or proxy is needed. The PreviewNotice exit button removes draftKey from the URL and reloads.
An invalid key returns entry: null, not the published version. The preview URL contains a key that can read the draft, so hide share buttons and canonical URLs during preview. Regenerating the key in an entry's Settings invalidates older preview URLs.
Call useHead with meta after data resolves, on the line following await useFetch. Top-level await in <script setup> lets the server include the meta tag correctly.
Revalidate with webhooks
Nuxt has no equivalent of Next.js revalidatePath. The action depends on how you serve the page.
| Delivery method | What to do after saving an entry |
|---|---|
| Server-rendered on every request (default) | Nothing. fetchGenkoList and fetchGenkoEntry fetch fresh data each time; they have no cache option. |
Built with nuxt generate | Point the dashboard webhook at your hosting provider's deploy hook. No receiver code is needed. |
| Cached by a CDN or reverse proxy | Add a receiver that calls its purge API. |
React's cache option is specific to Next.js and unavailable in @genko-me/vue/server. To cache, use Nuxt features such as a Nitro routeRules cache on /api/blog. Draft previews always fetch fresh data regardless.
Write a webhook receiver
If you need to trigger a purge or rebuild, receive the webhook under server/. Verify the signature against the raw body before parsing JSON.
import { verifyGenkoSignature } from "@genko-me/sdk";
export default defineEventHandler(async event => { const rawBody = await readRawBody(event, "utf-8") ?? ""; const valid = await verifyGenkoSignature({ secret: process.env.GENKO_WEBHOOK_SECRET!, rawBody, signatureHeader: getHeader(event, "x-genko-signature"), timestampHeader: getHeader(event, "x-genko-timestamp"), }); if (!valid) throw createError({ statusCode: 401, statusMessage: "Invalid signature" });
const payload = JSON.parse(rawBody) as { event: string; api: string; entry: { id: string } | null }; // Purge the cache or trigger a rebuild here. return { received: true, event: payload.event };});readRawBody provides the raw string before JSON parsing. Using only readBody loses the exact bytes required for signature verification.
Deletion notifications and test notifications with entry: null can arrive. The timeout is 10 seconds and there are no automatic retries. See Webhooks for events, payloads, and delivery limits.
Next steps
Last updated: October 14, 2026