SvelteKitでヘッドレスCMSを使うには、+page.server.tsのload関数からCMSを呼び出し、表示に必要な記事だけをページへ渡します。公開記事はprerenderで静的生成する構成も選べますが、下書きプレビューにはリクエストを処理できるサーバーが必要です。

本記事はgenko.meの運営側が執筆しています。2026年10月20日時点でSvelteKit 2・Svelte 5を対象にします。現行公式ページの3系とは設定が異なります。

CMS取得はどのloadに書く?

秘密値を使う取得は+page.server.tsに置きます。+page.tsはブラウザでも動く共通loadです。戻り値はブラウザへ渡るため、記事だけを返します。

SvelteKitでブログAPIを接続する準備

  1. SvelteKit 2・Svelte 5を用意します。

  2. genko.meでリスト形式のAPI blogを作ります。

  3. titleをテキスト、bodyをリッチテキストとして追加し、記事を公開します。

  4. blogの読み取りを許可したAPIキーを発行します。

  5. ビルド環境にGENKO_API_KEYを設定します。

bash
npm install @genko-me/sdk

初期操作はクイックスタートを参照してください。表示部品は導入ガイドの別構成です。

CMSクライアントをサーバー専用にする

ts
// src/lib/server/genko.tsimport { createGenko, listApi } from "@genko-me/sdk";import { GENKO_API_KEY } from "$env/static/private";type Blog = { title: string; body: string };export const client = createGenko({  workspace: "your-workspace",  apiKey: GENKO_API_KEY,  apis: {    blog: listApi<Blog>({      fields: [        { id: "title", type: "text" },        { id: "body", type: "richtext" },      ],    }),  },}).apis;

ワークスペースIDを置き換え、CMSのフィールドID・種類をそろえます。

$lib/serverでブラウザ側への誤importを防ぎます。SSGにも使えるようビルド時のキーを読むため、交換には再ビルドが必要で、サーバー成果物も秘密値として扱います。

SSR専用なら$env/dynamic/privateへ変更できますが、プリレンダリングには使えません。

load関数で記事一覧を表示する

ページはsrc/routes/blog/+page.svelteです。

ts
// src/routes/blog/+page.server.tsimport { client } from "$lib/server/genko";import type { PageServerLoad } from "./$types";export const load: PageServerLoad = async () => ({  posts: await client.blog.getAll({    orders: "-publishedAt",    fields: ["id", "title"],  }),});
svelte
<script lang="ts">  import type { PageData } from "./$types";  let { data }: { data: PageData } = $props();</script><svelte:head><title>Blog</title></svelte:head><h1>Blog</h1><ul>  {#each data.posts as post (post.id)}    <li><a href={`/blog/${post.id}`}>{post.title}</a></li>  {/each}</ul>{#if data.posts.length === 0}<p>No published posts.</p>{/if}

getAll()は全公開記事を集めます。記事が増えたらlist()のlimit・offsetでページ分割し、本文を一覧へ含めない設計を維持します。

SvelteKitで記事詳細と404を作る

ページはsrc/routes/blog/[id]/+page.svelteです。

ts
// src/routes/blog/[id]/+page.server.tsimport { error } from "@sveltejs/kit";import { client } from "$lib/server/genko";import type { PageServerLoad } from "./$types";export const load: PageServerLoad = async ({ params }) => {  const post = await client.blog.getOrNull(params.id);  if (!post) error(404, "Post not found");  return { post };};
svelte
<script lang="ts">  import type { PageData } from "./$types";  let { data }: { data: PageData } = $props();</script><svelte:head><title>{data.post.title}</title></svelte:head><article>  <h1>{data.post.title}</h1>  <div>{@html data.post.body}</div></article>

getOrNull()は404だけをnullに変換します。認証失敗などはエラーのまま、存在しない記事はページも404にします。

bodyは保存時にサニタイズ済みのHTMLです。{@html ...}は描画用で、任意のHTMLを安全にする機能ではありません。

prerenderとentriesでブログを静的生成する

一覧の+page.server.tsへexport const prerender = true;を追記します。詳細の同ファイルには、既存のloadを残して次を追加します。

ts
import type { EntryGenerator } from "./$types";export const prerender = true;export const entries: EntryGenerator = async () => {  const posts = await client.blog.getAll({ fields: ["id"] });  return posts.map(post => ({ id: post.id }));};

entries()で全公開記事のIDを列挙します。一覧からリンクされていない記事も生成対象になります。

更新反映にはWebhookなどで再ビルドが必要です。削除・公開終了後に古いページが残らないことも確認します。

adapter-staticとadapter-nodeはどう選ぶ?

配信先

アダプター

完全静的

adapter-static

Nodeサーバー

adapter-nodeなど

他の配信先では依存パッケージとの互換性も確認します。

SvelteKit 2のadapter-staticはsvelte.config.jsで設定します。既存の前処理を残し、公式の2系ガイドに従って変更してください。

完全静的なら全ルートを生成します。ssr = falseの空シェルで代用しません。

下書きプレビューを公開ブログから分離する

サーバー配信なら公開記事を静的生成し、/preview/[id]だけをリクエスト時に処理できます。

ts
// src/routes/preview/[id]/+page.server.tsimport { error } from "@sveltejs/kit";import { client } from "$lib/server/genko";import type { PageServerLoad } from "./$types";export const prerender = false;export const load: PageServerLoad = async ({ params, url, setHeaders }) => {  setHeaders({    "cache-control": "private, no-store",    "x-robots-tag": "noindex, nofollow",    "referrer-policy": "no-referrer",  });  const draftKey = url.searchParams.get("draftKey");  if (!draftKey) error(400, "Missing draft key");  const post = await client.blog.getOrNull(params.id, { draftKey });  if (!post) error(404, "Draft not found");  return { post };};

src/routes/preview/[id]/+page.svelteは上の詳細と同じ描画にし、プレビューURLをhttps://your-site.example/preview/{id}?draftKey={key}にします。保存済み下書きが対象です。このURLを共有しないでください。

draftKeyはプレビューだけで扱います。adapter-staticには動的ルートを同梱できないため、専用環境かサーバー対応アダプターが必要です。

通知・終了ボタンは自動追加されません。終了時は公開ページへ戻り、CDNにも下書きを残しません。

npm run checkとnpm run build後、全記事・遷移・404・削除後の反映を確認します。取得失敗は空の一覧と区別します。

関連:Astro版、Nuxt版。

よくある質問

SvelteKitの+page.tsからCMSキーを使えますか?

共通のloadはブラウザでも動くため、秘密値を扱う取得は+page.server.tsへ置きます。表示用の記事だけを戻り値に含めます。

SvelteKitでCMSブログを完全静的にできますか?

公開記事をビルド時に取得し、生成対象を列挙できるなら可能です。記事の追加・削除後は再ビルドが必要です。

adapter-staticで下書きプレビューは動きますか?

リクエストごとに下書きを取得するサーバールートは動きません。別のプレビュー環境か、サーバー対応アダプターが必要です。

出典

確認日:2026年10月20日。