# Use a headless CMS with Next.js: build a post list, detail pages, and draft previews

Published: 2026-10-06 | Updated: 2026-10-18 | URL: https://blog.genko.me/en/headless-cms-nextjs

> Connect genko.me to the Next.js App Router and build a post list, detail pages, and previews of saved drafts. The guide covers everything from setting up an API key to going live, with code examples and checks.

You have a site built with Next.js and want to add a blog or an updates page. Instead of editing source code every time a post changes, you want to write and publish from the browser. That is what a headless CMS is for: it keeps managing content separate from displaying the site.

In this guide, you manage posts in genko.me and show a list and detail pages with the Next.js App Router. Finally, you make drafts viewable on the same detail page before they are published. The look of the site stays in Next.js, so it fits into your existing design.

The steps are based on the versions published as of September 17, 2026 (CLI 0.2.3, SDK 0.3.0, React components 0.3.1) and the [official quickstart](https://docs.genko.me/en/getting-started).

## What you need

- A Next.js 16+ App Router project with React 19 that runs
- Node.js 20.12 or later, in a version your Next.js supports
- A genko.me account and a workspace where you are the Owner

The examples use TypeScript and the Node.js runtime. File paths assume an `app/` layout. With a `src/app/` layout, pages go in `src/app/` and the files the CLI generates go in `src/lib/`. Adjust the `@/` alias to your project too.

## 1. Create an API for your posts

In your genko.me workspace, create an API named "Blog" with the endpoint `blog` and the "List" type. The List type is for content with many entries, such as posts.

Add these two fields to the schema. The code below uses the field IDs, so match the IDs as well as the display names.

| Display name | Field ID | Type | Required |
| --- | --- | --- | --- |
| Title | title | Text | Yes |
| Body | body | Rich text | Yes |

Next, create one post. Enter a title and body, set the status to "Published" in the editor's "Settings", and save. The regular delivery API does not return drafts, so use a published post to check the connection.

You can also follow the screens in [APIs and schemas](https://docs.genko.me/en/studio/apis) and [Create and publish content](https://docs.genko.me/en/studio/content).

## 2. Create an API key and connect with the CLI

From the workspace menu, open "Settings and plan" → "API keys". Create a key just for this site, set its access to "Selected APIs only", and allow `blog`. Set a validity period too, and copy the key shown right after creation. You cannot view the full key again later.

Your workspace ID is `your-workspace` if the dashboard URL is `https://genko.me/w/your-workspace`.

Run this in the directory that contains your Next.js project's `package.json`.

```bash
npx @genko-me/cli init
```

Enter your workspace ID and API key, and choose `blog` as the API to sync. The CLI adds the SDK and other dependencies, and generates the schema types and a client for connecting.

- `lib/schema.ts`: types for the API and its fields
- `lib/client.ts`: the client that connects to genko.me
- `genko.config.json`: settings used for the next sync
- `.env.local`: where the API key is stored (unless the key is already in an existing environment variable)

Treat the API key as the server-side `GENKO_API_KEY`. Do not prefix it with `NEXT_PUBLIC_` or send it to the browser. Also make sure `.env.local` is not tracked by Git. For details, see [Manage API keys](https://docs.genko.me/en/studio/api-keys).

The CLI does not generate pages. Add the next two files to your project yourself.

## 3. Show a list of posts

Create `app/blog/page.tsx`. This page is a server component, so do not add `"use client"` at the top.

```tsx
import { client } from "@/lib/client";
import { List, ListItem, Pagination, Error, Null } from "@genko-me/react/next";

export default function BlogPage() {
  return (
    <main>
      <h1>Blog</h1>
      <List api={client.blog} limit={6} orders="-publishedAt" href="/blog/{id}">
        <article>
          <h2><ListItem id="title" /></h2>
        </article>
        <Pagination />
        <Error template />
        <Null>No posts have been published yet.</Null>
      </List>
    </main>
  );
}
```

The `article` inside `List` repeats for each post, and `ListItem` shows its title. `limit={6}` is the number per page, `orders="-publishedAt"` sorts by newest publish date, and `href` is where each detail page lives. `Pagination` handles paging.

Start the dev server and open `/blog`. If the title of the post you published appears, you are connected. If the server was already running before you added the environment variable, restart it.

## 4. Build the post detail page

Next, create `app/blog/[id]/page.tsx`. It passes the post ID from the list link to `View` and shows the title and body.

```tsx
import { client } from "@/lib/client";
import { View, ViewItem, Error, Null } from "@genko-me/react/next";

type Props = {
  params: Promise<{ id: string }>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

export default async function BlogDetail({ params, searchParams }: Props) {
  const { id } = await params;

  return (
    <main>
      <View api={client.blog} id={id} searchParams={searchParams}>
        <h1><ViewItem id="title" /></h1>
        <ViewItem id="body" />
        <Error template />
        <Null>Post not found.</Null>
      </View>
    </main>
  );
}
```

Open a post from the list and check its title and body. Style headings, spacing, and body formatting with your site's CSS. The options for each component are in the [List and View reference](https://docs.genko.me/en/sdk/components).

## 5. Preview drafts before publishing

In the API settings in the dashboard, set the preview URL in this form. Replace the domain and path with your own site's.

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

During development, you can use a local URL that you can reach from your machine. Save a post as a draft and open "Preview" in the editor to see the saved draft on the same detail page.

That is why the detail page passes `searchParams` to `View`: it receives this `draftKey`. With this setup, you do not need a separate Route Handler just for previews.

The preview shows saved content only; unsaved edits are not reflected. Do not share URLs that contain a draft key as public links. For details, see [Preview drafts](https://docs.genko.me/en/guides/preview).

## 6. Connect your production site

Set the `GENKO_API_KEY` environment variable on your host as well, then deploy. Your local `.env.local` is not carried over to production. After deploying, check the list, detail pages, and draft preview on the production URL.

This example is the basic setup for showing posts. To finish it as a public site, also handle per-post metadata such as title, description, and OG image, and return a 404 for posts that do not exist. Writing a message in `Null` does not by itself set an HTTP 404.

When you add or change schema fields, sync the types with this command.

```bash
npx @genko-me/cli pull
```

## If nothing shows up

| Symptom | What to check |
| --- | --- |
| The list is empty | Whether the post was saved as "Published". Drafts do not appear in the regular list. |
| 401 error | The key's validity period, and GENKO\_API\_KEY in the runtime environment. |
| 404 error | The workspace ID, endpoint, and post ID, and whether the key allows reading blog. |
| client.blog is not found | Whether you synced blog with the CLI, and whether the generated path matches your import path. |
| Drafts do not appear in the preview | Whether the draft is saved, whether the URL has draftKey, and whether you pass searchParams to View. |

Start by taking one post through publishing, displaying, and previewing a draft. Once that works, adding images and categories makes it easier to tell connection problems apart from styling. If you want to start with a setup that already has screens, try the [updates page template](https://docs.genko.me/en/guides/announcements-starter).
