genko.meDocs

実践

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

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

ページ送り

次ページの取得はサーバーが受け持ちます。+server.tsにPOSTハンドラを1つ置いてください。

src/routes/api/genko/page/+server.ts
export { genkoPaginationHandler as POST } from "@genko-me/svelte/server";

+page.server.tsではpaginationを指定します。{ mode: "url" }はページ番号をURLに載せ、戻る操作にも対応します。既定(pagination: true)はURLを変えず一覧だけを差し替えます。

src/routes/blog/+page.server.ts
import { fetchGenkoList } from "@genko-me/svelte/server";import { client } from "$lib/client";import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ url }) => ({  list: await fetchGenkoList({    api: client.blog,    fields: ["title", "publishedAt"],    href: "/blog/{id}",    limit: 10,    orders: "-publishedAt",    pagination: { mode: "url" },    searchParams: url.searchParams,  }),});

searchParamsを渡すと、/blog?page=3を直接開いたときにサーバーが3ページ目を返します。

src/routes/blog/+page.svelte
<script lang="ts">  import {    ListClient, ListItem, Pagination, Error, Null, Loading, createGenkoPageLoader,  } from "@genko-me/svelte";
  let { data } = $props();</script>
{#if data.list.status === "ok"}  <ListClient    {...data.list.data}    href={'/blog/{id}'}    paginationOptions={{ mode: 'url' }}    loadPage={createGenkoPageLoader({ token: data.list.data.paginationToken! })}  >    {#snippet children()}      <article>        <h2><ListItem id="title" /></h2>        <time><ListItem id="publishedAt" format="yyyy/MM/dd" timeZone="Asia/Tokyo" /></time>      </article>    {/snippet}    {#snippet pagination()}<Pagination />{/snippet}    {#snippet loading()}<Loading template />{/snippet}    {#snippet error()}<Error template />{/snippet}    {#snippet empty()}<Null>公開されている記事はありません。</Null>{/snippet}  </ListClient>{:else}  <Error template />{/if}

modeとparamはListClientに渡します。Paginationではありません。 Reactは<Pagination mode="url" />のpropをJSXから読み取りますが、Svelteのsnippetは中身を走査できないためです。Paginationが受け取るのはpreviousLabel・nextLabel・class・styleだけです。fetchGenkoListのpaginationとListClientのpaginationOptionsには同じ値を指定してください。

createGenkoPageLoaderのendpointは既定で/api/genko/pageです。別のパスに置いた場合は指定します。1ページしかないときはPaginationが何も描きません。

トークンについて

paginationTokenはfetchGenkoListがサーバーで発行する署名付きのスコープで、ページのデータとしてブラウザへ渡ります。Next.jsはServer Actionに引数を束ねられますが、SvelteKitに同じ仕組みはないためです。

  • APIキーは含まれません。 表しているのは、発行時の取得先と絞り込み条件だけです。
  • 有効期限は1時間です。切れた場合やキーを交換した場合は、ページを再読み込みしてください。
  • 取得できるのは「そのページが既に表示している一覧の続き」に限られます。
  • カスタムfetchやwrapRequestを設定したクライアントでは、ページ送りは使えません。設定エラーになります。
  • ページ送りを使うクライアントのAPIキーは、サーバーのGENKO_API_KEYと一致させてください。

JavaScriptを使わない導線

Paginationはボタンなので、JavaScriptが動かない環境では切り替わりません。フォームで送る場合はgenkoPaginationActionを使います。

src/routes/blog/+page.server.ts
import { fetchGenkoList, genkoPaginationAction } from "@genko-me/svelte/server";
export const actions = { genkoPage: genkoPaginationAction };

アクションは{ ok: true, result }または{ ok: false, message }を返すので、それをListClientのinitialに渡します。

src/routes/blog/+page.svelte
{#if data.list.status === "ok"}  {@const initial = form?.ok ? form.result : data.list.data.initial}  {@const current = Math.floor(initial.offset / initial.limit) + 1}  {@const lastPage = Math.max(1, Math.ceil(initial.totalCount / initial.limit))}
  <ListClient {...data.list.data} {initial} initialPage={current}>    {#snippet children()}<h2><ListItem id="title" /></h2>{/snippet}    {#snippet empty()}<Null template />{/snippet}  </ListClient>
  <form method="POST" action="?/genkoPage">    <input type="hidden" name="token" value={data.list.data.paginationToken} />    <button type="submit" name="page" value={current - 1} disabled={current <= 1}>前へ</button>    <span>{current} / {lastPage}</span>    <button type="submit" name="page" value={current + 1} disabled={current >= lastPage}>次へ</button>  </form>{:else}  <Error template />{/if}

{@const}を{#if}の直下に置くと、status === "ok"で絞り込んだ型のまま計算できます。offsetを指定していない場合、現在のページはoffset / limit + 1で求まります。

mode: "url"と併用して、リンク(<a href="?page=2">)で切り替える方法もあります。この場合はloadが毎回サーバーで走るため、トークンもアクションも不要です。

保存済み下書きのプレビュー

fetchGenkoEntryにsearchParamsを渡すと、URLのdraftKeyを読んで下書きを取得します。キャッシュはせず、draft: trueを返します。

src/routes/blog/[id]/+page.server.ts
import { error } from "@sveltejs/kit";import { fetchGenkoEntry } from "@genko-me/svelte/server";import { client } from "$lib/client";import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params, url }) => {  const entry = await fetchGenkoEntry({    api: client.blog,    id: params.id,    fields: ["title", "body"],    searchParams: url.searchParams,  });  if (entry.status === "empty") error(404, "記事が見つかりません。");  return { entry };};
src/routes/blog/[id]/+page.svelte
<script lang="ts">  import { EntryProvider, ViewItem, PreviewNotice, ShareButtons, Error } from "@genko-me/svelte";  import { page } from "$app/state";
  let { data } = $props();
  const draft = $derived(data.entry.status === "ok" && data.entry.data.draft);</script>
<svelte:head>  {#if draft}    <meta name="robots" content="noindex, nofollow" />    <meta name="referrer" content="no-referrer" />  {/if}</svelte:head>
{#if data.entry.status === "ok"}  {#if draft}<PreviewNotice />{/if}  <EntryProvider {...data.entry.data}>    {#snippet children()}      <article>        <h1><ViewItem id="title" /></h1>        <ViewItem id="body" />      </article>    {/snippet}  </EntryProvider>  {#if !draft}    <ShareButtons url={page.url.href} title="記事タイトル" />  {/if}{:else}  <Error template />{/if}

<svelte:head>は{#if}の中に置けません。 コンポーネントの直下に置き、中で{#if}を使ってください。ブロックの中に書くと<svelte:head> tags cannot be inside elements or blocksでコンパイルが失敗します。

管理画面のAPI設定(/w/your-workspace/apis/blog/settings)で、プレビューURLを設定します。

text
https://your-site.example/blog/{id}?draftKey={key}

保存してから「プレビュー」を開くと、同じ詳細ページで下書きが表示されます。追加のRoute Handlerやプロキシは不要です。PreviewNoticeの「終了」を押すと、URLからdraftKeyを外して読み直します。

キーが合わない場合は、公開版に切り替わるのではなく404になります。fetchGenkoEntryはstatus: "empty"を返します。プレビューURLには下書きを読めるキーが含まれるため、プレビュー中はシェアボタンやcanonical URLを出さないでください。記事の「設定」でプレビューキーを再生成すると、以前のプレビューURLは無効になります。

Webhookで再生成・再検証する

SvelteKitには、Next.jsのrevalidatePathに当たる仕組みがありません。 何をするかは、ページをどう配信しているかで変わります。

配信方法記事を保存したときにすること
サーバーで毎回描画(既定)何もしません。fetchGenkoList・fetchGenkoEntryは既定でcache: false、毎回最新を取得します
export const prerender = trueでビルド時に埋め込む管理画面のWebhookの通知先をホスティング先のデプロイフックにします。受信側のコードは書きません
CDNやリバースプロキシでキャッシュしているそのキャッシュのパージAPIを叩く受信処理を用意します

cacheに秒数を指定した場合は、no-storeを付けずに取得します。SvelteKitにはnext: { revalidate }に当たる指定がないため、loadのsetHeadersと組み合わせて使ってください。

src/routes/blog/+page.server.ts
export const load: PageServerLoad = async ({ setHeaders }) => {  const list = await fetchGenkoList({ api: client.blog, fields: ["title"], limit: 10, cache: 60 });  setHeaders({ "cache-control": "public, max-age=0, s-maxage=60" });  return { list };};

下書きのプレビューは、cacheの指定にかかわらず常に最新を取得します。

受信処理を書く

パージや再ビルドのトリガーが必要な場合は、+server.tsで受け取ります。署名はJSONへ変換する前の、生の本文に対して確認します。

src/routes/api/genko/webhook/+server.ts
import { json, text } from "@sveltejs/kit";import { verifyGenkoSignature } from "@genko-me/sdk";import { env } from "$env/dynamic/private";import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {  const secret = env.GENKO_WEBHOOK_SECRET;  if (!secret) return text("Webhook secret is not configured", { status: 500 });
  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 (!valid) return text("Invalid signature", { status: 401 });
  const payload = JSON.parse(rawBody) as { event: string; api: string; entry: { id: string } | null };  // ここでキャッシュのパージや再ビルドのトリガーを行います。  return json({ received: true, event: payload.event });};

削除通知や、entryがnullのテスト通知も届きます。タイムアウトは10秒で自動リトライはありません。イベントの種類、ペイロード、配送の制限はWebhookにまとめています。

次に読む

最終更新: 2026年10月14日

目次