Docs
Get started

Next.js quickstart

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

View as Markdown

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, with a title and a body, and a production key.

Create the app

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:

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

// 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

// 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

// 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

pnpm dev

Open 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.
  • Instant updates and drafts. Revalidate on publish, and show drafts to editors. See the Next.js guide.
  • Images. Resize on the CDN with a next/image loader. See Images.