# Next.js quickstart

Source: https://capacms.com/docs/get-started/nextjs

Build a Next.js site that lists and shows your Capa entries with the SDK.

This page builds a two-page Next.js site: a list of articles and a page for each one. It reads published entries through `@capacms/sdk`.

It assumes the `article` model from the [Quickstart](https://capacms.com/docs/get-started/quickstart), with a `title` and a `body`, and a production key.

## Create the app

```bash
pnpm create next-app@latest northpeak --ts --app --yes
cd northpeak
pnpm add @capacms/sdk@next
```

The SDK runs on Node 18 or later.

## Add your key

Create `.env.local` in the project root:

```bash
CAPA_API_URL=https://cdn.capacms.com
CAPA_KEY=cap_live_...
```

These two names are the ones every Capa tool reads. Keep the key server-side: never prefix it with `NEXT_PUBLIC_`.

## Make one client

```ts
// lib/capa.ts
import { createClient } from "@capacms/sdk/next";
import { withCache } from "@capacms/sdk/nextjs";

export const capa = createClient({
  baseUrl: process.env.CAPA_API_URL!,
  apiKey: process.env.CAPA_KEY!,
  version: "2026-10-01",
  // Keep each read in Next's data cache for up to 60 seconds.
  fetch: withCache(fetch, { revalidate: 60 }),
});
```

`version` pins the shape of every response, so a later API version never changes your site under you.

## List the articles

```tsx
// app/page.tsx
import Link from "next/link";
import { capa } from "@/lib/capa";

export default async function Home() {
  const articles = await capa.entries.list("article", {
    select: ["title"],
    sort: ["-publishedAt"],
    limit: 20,
  });

  return (
    <main>
      <h1>Articles</h1>
      <ul>
        {articles.data.map((article) => (
          <li key={article.id}>
            <Link href={`/articles/${article.id}`}>{String(article.fields.title)}</Link>
          </li>
        ))}
      </ul>
    </main>
  );
}
```

`select` asks for only the fields the page shows. `sort: ["-publishedAt"]` puts the newest first.

## Show one article

```tsx
// app/articles/[id]/page.tsx
import { notFound } from "next/navigation";
import { capa } from "@/lib/capa";

export default async function ArticlePage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const article = await capa.entries.get("article", id, { select: ["title", "body"] });
  if (!article) notFound();

  return (
    <article>
      <h1>{String(article.data.fields.title)}</h1>
      <div>{String(article.data.fields.body)}</div>
    </article>
  );
}
```

`entries.get` returns `null` when the entry does not exist or is not published, so the page answers 404. Any other error throws a `CapaError` with the API's code and hint.

A Rich Text field arrives as the string it was saved as. The admin's editor saves HTML, so render it as HTML. A value written through the API in Markdown stays Markdown.

## Run it

```bash
pnpm dev
```

Open [localhost:3000](http://localhost:3000). Publish a change in Capa, and the page shows it within a minute.

## Make it yours

* **Readable URLs.** Add a `slug` field to the model, link to `/articles/${slug}`, and read with `filter: { slug: { eq: slug } }, limit: 1`.
* **Types.** Run `capa-codegen` and every field is typed from your model. See [TypeScript](https://capacms.com/docs/guides/typescript).
* **Instant updates and drafts.** Revalidate on publish, and show drafts to editors. See the [Next.js guide](https://capacms.com/docs/guides/nextjs).
* **Images.** Resize on the CDN with a `next/image` loader. See [Images](https://capacms.com/docs/guides/images).
