実践
SvelteKitを使わないSvelteのSPAでgenko.meのページ送りと下書きプレビューを実装し、コンテンツ更新の反映方法を選ぶ。
導入の一覧・詳細に、運用で必要になるものを足します。@genko-me/svelteの表示部品は使えますが、取得はアプリ側で行うため、ページ送りと下書きプレビューの「いつ何を取得するか」は自分で書きます。
ページ送り
ListClientのloadPageには、ページ番号を受け取ってPageResultを返す関数を渡します。SvelteKitではcreateGenkoPageLoaderがサーバーの署名付きトークンを介してこれを行いますが、この対象にはサーバーがないので、client.blog.list()を直接呼ぶ関数を自分で用意します。
<script lang="ts"> import { onMount } from "svelte"; import { getApiBinding } from "@genko-me/sdk"; import { ListClient, ListItem, Pagination, Error, Null, Loading } from "@genko-me/svelte"; import type { ListClientProps, PageResult } from "@genko-me/svelte"; import { client } from "../lib/genko";
const PER_PAGE = 10; let listProps = $state<Omit<ListClientProps, "children"> | undefined>(undefined); let failed = $state(false);
async function loadPage(page: number): Promise<PageResult> { try { const result = await client.blog.list({ limit: PER_PAGE, offset: (page - 1) * PER_PAGE, orders: "-publishedAt" }); return { ok: true, result }; } catch { return { ok: false, message: "読み込みに失敗しました。" }; } }
onMount(async () => { const initialPage = Number(new URLSearchParams(window.location.search).get("page") ?? 1) || 1; try { const binding = getApiBinding(client.blog); const initial = await client.blog.list({ limit: PER_PAGE, offset: (initialPage - 1) * PER_PAGE, orders: "-publishedAt" }); listProps = { initial, initialPage, fields: binding.fields, schemas: binding.schemas, href: "/blog/{id}", loadPage, paginationOptions: { mode: "url" }, }; } catch { failed = true; } });</script>
<h1>ブログ</h1>
{#if failed} <Error template />{:else if !listProps} <Loading template />{:else} <ListClient {...listProps}> {#snippet children()}<h2><ListItem id="title" /></h2>{/snippet} {#snippet pagination()}<Pagination />{/snippet} {#snippet empty()}<Null template />{/snippet} </ListClient>{/if}Pagination・URLとの連動・aria-liveでの読み上げはListClientが持っているロジックで、SvelteKitと同じものです。paginationOptions={{ mode: "url" }}にするとwindow.history.pushStateでURLのpageを書き換え、戻る操作にも対応します。既定(paginationOptionsを省略)はURLを変えず一覧だけを差し替えます。
ここがReact(Viteの非Next構成)と違う点です。 ReactにはPaginationに相当する部品がないため、ボタンとページ番号の計算をすべて自分で書く必要がありますが、Svelteの表示部品はSvelteKit専用ではないので、この対象でもそのまま使えます。
limitは1〜100です。100件を超える一覧を一度に扱う場合はgetAll()が自動でページングします。詳しくは並び替え・絞り込み・検索を参照してください。
保存済み下書きのプレビュー
URLのdraftKeyを読み、getOrNullの第2引数に渡します。キーが合えば下書きが、合わなければ404扱いになりnullが返ります。
<script lang="ts"> import { getApiBinding } from "@genko-me/sdk"; import { EntryProvider, ViewItem, PreviewNotice, ShareButtons, Error } from "@genko-me/svelte"; import type { EntryProviderProps } from "@genko-me/svelte"; import { client } from "../lib/genko";
let { id }: { id: string } = $props();
const draftKey = new URLSearchParams(window.location.search).get("draftKey") ?? undefined;
let entryProps = $state<Omit<EntryProviderProps, "children"> | undefined>(undefined); let notFound = $state(false); let failed = $state(false);
$effect(() => { entryProps = undefined; notFound = false; failed = false; const binding = getApiBinding(client.blog); client.blog .getOrNull(id, draftKey ? { draftKey } : undefined) .then((entry) => { if (!entry) { notFound = true; return; } entryProps = { entry, fields: binding.fields, schemas: binding.schemas }; }) .catch(() => { failed = true; }); });</script>
<svelte:head> {#if draftKey} <meta name="robots" content="noindex, nofollow" /> <meta name="referrer" content="no-referrer" /> {/if}</svelte:head>
{#if failed} <Error template />{:else if notFound} <p>記事が見つかりません。</p>{:else if entryProps} {#if draftKey}<PreviewNotice />{/if} <EntryProvider {...entryProps}> {#snippet children()} <article> <h1><ViewItem id="title" /></h1> <ViewItem id="body" /> </article> {/snippet} </EntryProvider> {#if !draftKey} <ShareButtons url={`https://your-site.example/blog/${id}`} title="記事タイトル" /> {/if}{/if}<svelte:head>はSvelteKit専用の機能ではありません。SvelteのコンパイラがマウントのタイミングでDOMの<head>へ直接挿入するので、SvelteKitを使わないこの対象でも動きます。ただしSSRではないため、検索エンジンのクローラーがJavaScriptを実行しない場合はnoindexが間に合わない可能性があります。下書きプレビューは本来クローラーに見せない前提の画面なので、実用上は許容範囲ですが、確実に隠したいならプレビューURLをrobots.txtで塞ぐか、認証の裏に置いてください。
PreviewNoticeの「終了」ボタンはwindow.location.replaceでURLからdraftKeyを外し、ページを丸ごと再読み込みします。SPAルーターによるソフトな遷移ではありません。
管理画面のAPI設定(/w/your-workspace/apis/blog/settings)で、プレビューURLテンプレートをSPAのルートに合わせて設定します。
https://your-site.example/blog/{id}?draftKey={key}{id}と{key}は管理画面が置き換えます。詳細ページを直接開ける設定(SPAフォールバック)になっていることを確認してください。プレビューURLには下書きを読めるキーが含まれるため、プレビュー中はシェアボタンやcanonical URLを出さないでください。 記事の「設定」でプレビューキーを再生成すると、以前のプレビューURLは無効になります。
Webhookで再生成・再検証する
この対象では、再検証するキャッシュがありません。 SPAは表示のたびに配信APIから取得するので、記事を公開すればそのまま次の表示に反映されます。Webhookの受信処理も不要です。
Webhookが要るのは次の場合です。
- ビルド時にコンテンツを埋め込んでいる場合(静的生成やプリレンダリング)。管理画面のWebhookの通知先をホスティング先のデプロイフックにすると、記事を保存したときに再ビルドが動きます。受信側のコードは書きません。
- CDNやリバースプロキシでAPIの応答をキャッシュしている場合。そのキャッシュのパージAPIを叩く小さなサーバー処理を用意します。署名の検証には
@genko-me/sdkのverifyGenkoSignatureが使えます(Node.jsなど、リクエストの生の本文を読める環境が必要です。ブラウザだけで完結するこの対象には置けません)。
import { verifyGenkoSignature } from "@genko-me/sdk";
const rawBody = await request.text();const valid = await verifyGenkoSignature({ secret: process.env.GENKO_WEBHOOK_SECRET!, rawBody, signatureHeader: request.headers.get("x-genko-signature"), timestampHeader: request.headers.get("x-genko-timestamp"),});イベントの種類、ペイロード、配送の制限はWebhookにまとめています。
次に読む
最終更新: 2026年10月14日