実践
SvelteKitの+server.tsルートでページ送り・保存済み下書きのプレビュー・Webhookでの再生成を組み立てる手順。
導入の一覧・詳細に、運用で必要になるものを足します。
ページ送り
次ページの取得はサーバーが受け持ちます。+server.tsにPOSTハンドラを1つ置いてください。
export { genkoPaginationHandler as POST } from "@genko-me/svelte/server";+page.server.tsではpaginationを指定します。{ mode: "url" }はページ番号をURLに載せ、戻る操作にも対応します。既定(pagination: true)はURLを変えず一覧だけを差し替えます。
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ページ目を返します。
<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を使います。
import { fetchGenkoList, genkoPaginationAction } from "@genko-me/svelte/server";
export const actions = { genkoPage: genkoPaginationAction };アクションは{ ok: true, result }または{ ok: false, message }を返すので、それをListClientのinitialに渡します。
{#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を返します。
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 };};<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を設定します。
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と組み合わせて使ってください。
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へ変換する前の、生の本文に対して確認します。
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日