genko.meDocs

実践

Nuxtのserver/ルートでページ送り・保存済み下書きのプレビュー・Webhookでの再生成を組み立てる手順。

導入の一覧・詳細に、運用で必要になるものを足します。

ページ送り

次ページの取得はサーバーが受け持ちます。server/にPOSTルートを1つ置いてください。

server/api/genko-page.post.ts
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を変えず一覧だけを差し替えます。

server/api/blog.get.ts
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>,}));
app/pages/blog/index.vue
<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を返します。

server/api/blog/[id].get.ts
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>,}));
app/pages/blog/[id].vue
<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を設定します。

text
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へ変換する前の、生の本文に対して確認します。

server/api/genko-webhook.post.ts
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日

目次