Next.js quickstart
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, 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@nextThe 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 devOpen localhost:3000. Publish a change in Capa, and the page shows it within a minute.
Make it yours
- Readable URLs. Add a
slugfield to the model, link to/articles/${slug}, and read withfilter: { slug: { eq: slug } }, limit: 1. - Types. Run
capa-codegenand 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/imageloader. See Images.