# NuxtでヘッドレスCMSを使う：useFetch・SSR・下書きプレビュー

公開日: 2026-10-20 | 更新日: 2026-10-20 | URL: https://blog.genko.me/nuxt-headless-cms

> Nuxt 4でヘッドレスCMSを使う方法を、genkoのSDKとuseFetchで解説します。APIキーをサーバー側で管理し、記事一覧と詳細をSSRで表示する手順から、プリレンダリングと下書きプレビューのキャッシュ対策まで確認できます。

NuxtでヘッドレスCMSを使うには、`server/api`でCMSへ接続し、ページから`useFetch()`で自分のAPIを呼び出す構成にします。APIキーをサーバー側に置きながらSSRで記事を表示でき、更新頻度に応じて公開ページをプリレンダリングする方法も選べます。

本記事はgenko.meの運営側が執筆しています。2026年10月20日時点のNuxt 4向けに、genkoのSDKを直接使う最小構成を紹介します。表示部品を使いたい場合は[Nuxt導入ガイド](https://docs.genko.me/frameworks/nuxt/getting-started)のVue連携も選べます。

## SSRと静的生成はどう選ぶ？

| 方法 | 選ぶ目安 |
| --- | --- |
| SSR | リクエスト時に取得 |
| 静的生成 | 更新後のビルド待ちを許容 |

プレビューにはサーバーが必要です。

## Nuxt 4からgenkoへ接続する準備

1. 既存のNuxt 4プロジェクトを用意します。
2. [genko.me](https://genko.me/)でリスト形式のAPI `blog`を作ります。
3. `title`をテキスト、`body`をリッチテキストにして、記事を保存・公開します。
4. `blog`を読み取れるAPIキーを発行します。
5. 実行環境の`NUXT_GENKO_API_KEY`へキーを設定します。

```bash
npm install @genko-me/sdk
```

初期設定は[クイックスタート](https://docs.genko.me/getting-started)を参照してください。Vue部品の署名付きページ送りは使いません。採用時は専用ガイドの`GENKO_API_KEY`とランタイム条件にも従います。

## APIキーをNuxtのruntimeConfigで管理する

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: { genkoApiKey: "" },
});
```

```ts
// server/utils/genko.ts
import { createGenko, listApi } from "@genko-me/sdk";
type Blog = { title: string; body: string };
const apis = {
  blog: listApi<Blog>({
    fields: [
      { id: "title", type: "text" },
      { id: "body", type: "richtext" },
    ],
  }),
};
export function createBlogClient(apiKey: string) {
  if (!apiKey) throw new Error("NUXT_GENKO_API_KEY is required");
  return createGenko({ workspace: "your-workspace", apiKey, apis }).apis.blog;
}
```

キーは`runtimeConfig.public`へ入れず、本番環境にも`NUXT_GENKO_API_KEY`を設定します。`your-workspace`を実際のIDへ置き換えます。

## useFetchで記事一覧をSSR表示する

ページは`app/pages/blog/index.vue`です。

```ts
// server/api/blog/index.get.ts
import { createBlogClient } from "../../utils/genko";
export default defineEventHandler(async event => {
  const config = useRuntimeConfig(event);
  return createBlogClient(config.genkoApiKey).getAll({
    orders: "-publishedAt",
    fields: ["id", "title"],
  });
});
```

```vue
<script setup lang="ts">
const { data: posts, error } = await useFetch("/api/blog");
if (error.value) {
  throw createError({ statusCode: 502, statusMessage: "Unable to load posts" });
}
useHead({ title: "Blog" });
</script>
<template>
  <main>
    <h1>Blog</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/blog/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
    <p v-if="posts?.length === 0">No published posts.</p>
  </main>
</template>
```

`useFetch()`はSSRの結果をクライアントへ引き継ぎます。内部APIは表示用の記事だけを返し、秘密値やCMS管理操作を公開しません。

記事数が増えたら`list()`でページ分割します。

## Nuxtで記事詳細と下書きプレビューを実装する

ページは`app/pages/blog/[id].vue`です。

```ts
// server/api/blog/[id].get.ts
import { createBlogClient } from "../../utils/genko";
export default defineEventHandler(async event => {
  const id = getRouterParam(event, "id");
  if (!id) throw createError({ statusCode: 400, statusMessage: "Missing id" });
  const query = getQuery(event);
  const draftKey = typeof query.draftKey === "string" ? query.draftKey : undefined;
  const config = useRuntimeConfig(event);
  const post = await createBlogClient(config.genkoApiKey).getOrNull(id, { draftKey });
  if (!post) throw createError({ statusCode: 404, statusMessage: "Post not found" });
  if (draftKey) setResponseHeader(event, "Cache-Control", "private, no-store");
  return { post, preview: Boolean(draftKey) };
});
```

```vue
<script setup lang="ts">
const route = useRoute();
const { data, error } = await useFetch(
  () => `/api/blog/${encodeURIComponent(String(route.params.id))}`,
  { query: computed(() => ({ draftKey: route.query.draftKey })) },
);
if (error.value || !data.value) {
  throw createError({
    statusCode: error.value?.statusCode === 404 ? 404 : 502,
    statusMessage: "Unable to load post",
  });
}
if (import.meta.server && data.value.preview) {
  useResponseHeader("Cache-Control").value = "private, no-store";
}
useHead(() => ({
  title: data.value?.preview ? "Draft preview" : data.value?.post.title,
  meta: data.value?.preview ? [
    { name: "robots", content: "noindex, nofollow" },
    { name: "referrer", content: "no-referrer" },
  ] : [],
}));
</script>
<template>
  <main v-if="data">
    <p v-if="data.preview">Draft preview</p>
    <h1>{{ data.post.title }}</h1>
    <div v-html="data.post.body" />
  </main>
</template>
```

管理画面のプレビューURLには`https://your-site.example/blog/{id}?draftKey={key}`を設定します。`{id}`と`{key}`は管理画面が置き換えます。

`draftKey`はページから内部APIへ自動転送されません。`query`とSDKの引数に明示的に渡し、保存済み下書きを取得します。

この実装にはVue連携のプレビューバーはありません。終了時は`draftKey`を外します。下書きキーも共有しないでください。

`v-html`はgenkoの保存時サニタイズ済み本文に使います。任意の外部HTMLを安全にする機能ではありません。

## プレビューのキャッシュ対策

APIとSSRページに`private, no-store`を付けます。CDN側の強制キャッシュも確認します。

プレビューURLを共有リンクへ使わず、ログ中のキーも保護します。[実践ガイド](https://docs.genko.me/frameworks/nuxt/tutorial)も参照してください。

## Nuxtのブログをプリレンダリングするには？

公開記事を静的生成する構成では、例えば次の設定を使えます。

`nuxt.config.ts`の`nitro.prerender`に`routes: ["/blog"]`と`crawlLinks: true`を追加します。

`/blog`から詳細リンクをたどって生成します。リンクのない記事はURLを列挙し、全記事の生成を確認します。

静的配信では実行時の`server/api`は使えません。必要な結果もビルド時に生成します。プレビューには別のサーバー環境かサーバー対応の配信が必要です。

[Webhook](https://docs.genko.me/guides/webhooks)で再ビルドする場合、手動公開の作成・更新イベントも処理します。

## 公開前の確認

`npm run build`後、直接アクセスとNuxtLink遷移で公開・下書き・404・更新反映を確認します。記事が空なら公開状態、認証失敗なら本番のキーとアクセス範囲を調べます。

関連：[Astro版](https://blog.genko.me/astro-blog-cms)、[SvelteKit版](https://blog.genko.me/sveltekit-headless-cms)。

## よくある質問

### NuxtのuseFetchからCMSを直接呼んでもよいですか？

秘密のAPIキーが必要な場合はサーバールートを通します。ブラウザへ渡るコードや公開runtimeConfigにキーを置かないようにします。

### Nuxtの下書きプレビューで公開版が表示されるのはなぜですか？

ページから内部API、内部APIからSDKへ`draftKey`を渡したか確認します。保存前の編集内容は取得できません。

### Nuxtの静的サイトでserver/apiは動きますか？

静的ファイルだけを配るホストでは動きません。必要な結果をビルド時に生成するか、実行時のサーバーを用意します。

## 出典

- [genko Nuxt導入](https://docs.genko.me/frameworks/nuxt/getting-started)
- [Nuxt公式：Data fetching](https://nuxt.com/docs/4.x/getting-started/data-fetching)
- [Nuxt公式：Runtime Config](https://nuxt.com/docs/4.x/guide/going-further/runtime-config)
- [Nuxt公式：Rendering Modes](https://nuxt.com/docs/4.x/guide/concepts/rendering)

公式ページの確認日：2026年10月20日。
