実践
Nuxtのserver/ルートでページ送り・保存済み下書きのプレビュー・Webhookでの再生成を組み立てる手順。
導入の一覧・詳細に、運用で必要になるものを足します。
ページ送り
次ページの取得はサーバーが受け持ちます。server/にPOSTルートを1つ置いてください。
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);});server/api/blog.get.tsではpaginationを指定します。{ mode: "url" }はページ番号をURLに載せ、戻る操作にも対応します。既定(pagination: true)は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>公開されている記事はありません。</Null></template> </ListClient> <Error v-else template /> </main></template>useFetchに{ query: route.query }を渡さないと、/blog?page=3を直接開いてもサーバーは1ページ目を返します。 /api/blogは/blogとは別のURLなので、ページ自身のクエリを明示的に転送する必要があります。渡していれば、直接開いたページ・リロード・戻る操作のいずれでも、サーバーが該当ページをSSRで返します。
modeとparamはListClientのpagination-optionsに渡します。Paginationのmodeではありません。 Reactは<Pagination mode="url" />のpropをJSXから読み取りますが、Vueのテンプレートは中身を走査できないためです。Paginationが受け取るのはprevious-label・next-label・class・styleだけです。fetchGenkoListのpaginationとListClientのpagination-optionsには同じ値を指定してください。
createPageLoaderのurlは、上で作ったserver/api/genko-page.post.tsのパスです。1ページしかないときはPaginationが何も描きません。
トークンについて
paginationTokenはfetchGenkoListがサーバーで発行する署名付きのスコープで、ページのデータとしてブラウザへ渡ります。Next.jsはServer Actionに引数を束ねられますが、Nuxtに同じ仕組みはないため、自分のAPIルート経由でトークンをやり取りする形になります。
- APIキーは含まれません。 表しているのは、発行時の取得先と絞り込み条件だけです。
- 有効期限は1時間です。切れた場合やキーを交換した場合は、ページを再読み込みしてください。
- 取得できるのは「そのページが既に表示している一覧の続き」に限られます。
- カスタム
fetchを設定したクライアントでは、ページ送りは使えません。設定エラーになります。 - ページ送りを使うクライアントのAPIキーは、サーバーの
GENKO_API_KEYと一致させてください。 - 署名・検証は
node:cryptoを直接使います。Nitroのプリセットはnode-serverなど、Node.js互換のランタイムにしてください。
保存済み下書きのプレビュー
fetchGenkoEntryにsearchParamsを渡すと、URLのdraftKeyを読んで下書きを取得します。キャッシュはせず、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>記事がありません。</p> </main></template>管理画面のAPI設定(/w/your-workspace/apis/blog/settings)で、プレビューURLを設定します。
https://your-site.example/blog/{id}?draftKey={key}保存してから「プレビュー」を開くと、同じ詳細ページで下書きが表示されます。追加のRoute Handlerやプロキシは不要です。PreviewNoticeの「終了」を押すと、URLからdraftKeyを外して読み直します。
キーが合わない場合は、公開版に切り替わるのではなくentry: nullになります。 プレビューURLには下書きを読めるキーが含まれるため、プレビュー中はシェアボタンやcanonical URLを出さないでください。記事の「設定」でプレビューキーを再生成すると、以前のプレビューURLは無効になります。
useHeadのmetaは、dataが確定した後(await useFetchの次の行)で呼んでいます。<script setup>のトップレベルawaitの後なら、サーバー側で正しく<meta>に反映されます。
Webhookで再生成・再検証する
Nuxtには、Next.jsのrevalidatePathに当たる仕組みがありません。 何をするかは、ページをどう配信しているかで変わります。
| 配信方法 | 記事を保存したときにすること |
|---|---|
| サーバーで毎回描画(既定) | 何もしません。fetchGenkoList・fetchGenkoEntryは毎回最新を取得します(キャッシュを指定するオプションはありません) |
nuxt generateでビルド時に埋め込む | 管理画面のWebhookの通知先をホスティング先のデプロイフックにします。受信側のコードは書きません |
| CDNやリバースプロキシでキャッシュしている | そのキャッシュのパージAPIを叩く受信処理を用意します |
React版のcacheオプションはNext.js固有のため@genko-me/vue/serverにはありません。キャッシュしたい場合は、Nitroのルートルール(routeRules)で/api/blogにcacheを指定するなど、Nuxt側の仕組みを使ってください。下書きのプレビューは、その設定にかかわらず常に最新を取得します。
受信処理を書く
パージや再ビルドのトリガーが必要な場合は、server/で受け取ります。署名は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 }; // ここでキャッシュのパージや再ビルドのトリガーを行います。 return { received: true, event: payload.event };});readRawBodyは本文をJSONへ変換する前の生の文字列で受け取ります。readBodyだけを使うと、署名の検証対象になる正確な文字列が失われます。
削除通知や、entryがnullのテスト通知も届きます。タイムアウトは10秒で自動リトライはありません。イベントの種類、ペイロード、配送の制限はWebhookにまとめています。
次に読む
最終更新: 2026年10月14日