genko.meDocs

実践

SvelteKitを使わないSvelteのSPAでgenko.meのページ送りと下書きプレビューを実装し、コンテンツ更新の反映方法を選ぶ。

導入の一覧・詳細に、運用で必要になるものを足します。@genko-me/svelteの表示部品は使えますが、取得はアプリ側で行うため、ページ送りと下書きプレビューの「いつ何を取得するか」は自分で書きます。

ページ送り

ListClientのloadPageには、ページ番号を受け取ってPageResultを返す関数を渡します。SvelteKitではcreateGenkoPageLoaderがサーバーの署名付きトークンを介してこれを行いますが、この対象にはサーバーがないので、client.blog.list()を直接呼ぶ関数を自分で用意します。

src/components/BlogList.svelte
<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が返ります。

src/components/BlogPost.svelte
<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のルートに合わせて設定します。

text
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など、リクエストの生の本文を読める環境が必要です。ブラウザだけで完結するこの対象には置けません)。
ts
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日

目次