genko.meDocs

実践

Nuxtを使わないVueでload-pageによるページ送りと下書きプレビューを実装し、コンテンツ更新の反映方法を選ぶ手順。

導入の一覧・詳細に、運用で必要になるものを足します。この対象にはNuxt向けの署名付きページ送りトークンやサーバールートはないので、いずれも@genko-me/sdkを直接呼ぶコードです。

ページ送り

ListClientのload-pageに、@genko-me/sdkを直接呼ぶ関数を渡します。APIキーは元からブラウザにあるので、署名付きトークンは不要です。

src/components/BlogList.vue
<script setup lang="ts">import { onMounted, ref } from "vue";import { getApiBinding } from "@genko-me/sdk";import type { GenkoListResult } from "@genko-me/sdk";import type { PageResult } from "@genko-me/vue";import { client } from "../lib/genko";
const PER_PAGE = 6;const binding = getApiBinding(client.blog);
const initial = ref<GenkoListResult<Record<string, unknown>>>();const failed = ref(false);
onMounted(async () => {  try {    initial.value = await client.blog.list({ limit: PER_PAGE, orders: "-publishedAt" });  } catch {    failed.value = true;  }});
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: "読み込みに失敗しました。" };  }}</script>
<template>  <Error v-if="failed" template />  <Loading v-else-if="!initial" template />  <ListClient v-else :initial="initial" :initial-page="1" :fields="binding.fields" :schemas="binding.schemas" href="/blog/{id}" :load-page="loadPage">    <article>      <h2><ListItem id="title" /></h2>    </article>    <template #pagination><Pagination /></template>    <template #empty><Null>公開されている記事はありません。</Null></template>  </ListClient></template>

PageResultは{ ok: true, result }または{ ok: false, message }の形です。loadPageがこれを返せば、ListClientは内部のuseListStoreで保留状態・失敗・ページ番号を管理してくれます。limitは1〜100です。ページ番号をURLに残したい場合は、pageをVue Routerのクエリから読むように変え、mode="url"相当の挙動は自分で組んでください(Nuxtのpagination-optionsのような専用のURLモードはこの対象にはありません)。

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

URLのdraftKeyを読み、getOrNullへ渡します。キーが合えば下書きが、合わなければnullになります。

src/components/BlogPost.vue
<script setup lang="ts">import { computed, onMounted, ref, watchEffect } from "vue";import { useRoute } from "vue-router";import { getApiBinding } from "@genko-me/sdk";import type { GenkoEntryMeta } from "@genko-me/sdk";import { EntryProvider, PreviewNotice, ShareButtons, ViewItem } from "@genko-me/vue";import { client } from "../lib/genko";import type { Blog } from "../lib/schema";
const route = useRoute();const binding = getApiBinding(client.blog);
const post = ref<(Blog & GenkoEntryMeta) | null>();const draftKey = computed(() => (typeof route.query.draftKey === "string" ? route.query.draftKey : undefined));const draft = ref(false);
onMounted(async () => {  try {    post.value = await client.blog.getOrNull(route.params.id as string, draftKey.value ? { draftKey: draftKey.value } : undefined);    draft.value = Boolean(draftKey.value) && post.value !== null;  } catch {    post.value = null;  }});
// SPAはサーバーからレスポンスヘッダーやHTMLの<meta>を返せないため、// プレビュー中だけdocument.headへ直接足す。watchEffect(() => {  let tag = document.querySelector('meta[name="robots"]');  if (draft.value) {    if (!tag) { tag = document.createElement("meta"); tag.setAttribute("name", "robots"); document.head.appendChild(tag); }    tag.setAttribute("content", "noindex, nofollow");  } else {    tag?.remove();  }});</script>
<template>  <p v-if="post === undefined">読み込み中…</p>  <p v-else-if="post === null">記事が見つかりません。</p>  <EntryProvider v-else :entry="post" :fields="binding.fields" :schemas="binding.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="post.title" />  </EntryProvider></template>

管理画面のAPI設定(/w/your-workspace/apis/blog/settings)で、プレビューURLをこのSPAのルートに合わせて設定します。

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

{id}と{key}は管理画面が置き換えます。SPAフォールバック(存在しないパスもindex.htmlを返す設定)になっていることを確認してください。

PreviewNoticeの「終了」はこの対象でもそのまま動きます。 ボタンは内部でwindow.location.replace()を呼び、URLからdraftKeyを外して読み直す仕組みで、フレームワークのルーターに依存しないためです。Next.js向けのViewのような自動処理はありませんが、noindexの付与とプレビュー通知は、上のコードだけで揃います。

共有リンクの抑制は自分で気をつけてください。 プレビューURLには下書きを読めるキーが含まれるため、上の例ではdraftのときだけShareButtonsを出さないようにしています。記事の「設定」でプレビューキーを再生成すると、以前のプレビュー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日

目次