# Next.js (/docs/getting-started/nextjs)

This sets up a Next.js App Router site that serves every page entry at its path, shows drafts in the canvas, and lets editors sign in on the site to change it. The [example in the SDK repository](https://github.com/Obelism/sdk/tree/main/examples/content-nextjs) is the finished version.

## 1. Start the repository on a space [#1-start-the-repository-on-a-space]

Create a **management token** under **Settings → API tokens**, then run in the site's root:

```sh
export CONTENT_MANAGEMENT_TOKEN=cms_m_…
npx @obelism/content init
```

`init` writes the model file (`content/content-model.json`), an example of what the delivery API answers for every content type (`content/examples/`), installs `@obelism/content`, and adds the token variables to `.env.example`. Add `@obelism/content-react` for the React helpers:

```sh
npm i @obelism/content-react
```

Put a delivery token in `.env.local` (never commit it):

```sh
CONTENT_DELIVERY_TOKEN=cms_d_…
# The space and environment it reads (Space settings), so the canvas's
# token for another space reads nothing here.
CONTENT_SPACE_ID=space_…
CONTENT_ENVIRONMENT=production
```

## 2. The clients [#2-the-clients]

One client for published content, cached by Next. Drafts come with a token for the person editing: in the canvas the page's URL carries `cms_token`, and an editor [signed in on the site](/docs/guides/site-editing) has a site token while draft mode is on. Those requests get a client of their own, never cached. [Previews and draft mode](/docs/guides/preview) explains the order and why.

```ts title="src/lib/content.ts"
import 'server-only'

import { ContentClient, readPreviewParams } from '@obelism/content'
import { readSiteToken } from '@obelism/content/site'
import { cookies, draftMode } from 'next/headers'

export const delivery = new ContentClient({
	token: process.env.CONTENT_DELIVERY_TOKEN!,
	init: { next: { revalidate: 60, tags: ['content'] } },
})

// Naming the space and environment makes a token someone made for another
// space read nothing here.
const drafts = (token: string) =>
	new ContentClient({
		token,
		space: process.env.CONTENT_SPACE_ID!,
		environment: process.env.CONTENT_ENVIRONMENT!,
		init: { cache: 'no-store' },
	})

// `cms_preview` alone proves nothing: anyone can add it to a URL.
export const getContent = async (
	searchParams: Record<string, string | string[] | undefined>,
) => {
	const preview = readPreviewParams(searchParams)
	if (preview?.token) return drafts(preview.token)
	if ((await draftMode()).isEnabled) {
		const token = readSiteToken(await cookies())
		if (token) return drafts(token)
	}
	return delivery
}
```

## 3. Every page by path [#3-every-page-by-path]

```tsx title="src/app/[[...slug]]/page.tsx"
import { ContentApiError } from '@obelism/content'
import { notFound } from 'next/navigation'

import { Page, type PageEntry } from '@/components/Page'
import { delivery, getContent } from '@/lib/content'

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

export default async function CatchAll({ params, searchParams }: Props) {
	const { slug } = await params
	const path = slug?.join('/') || 'home'
	const content = await getContent(await searchParams)

	const entry = await content
		.getPage<PageEntry>(path, { depth: 2 })
		.catch((error: unknown) => {
			// A made-up, expired or other space's `cms_token`: show what's published.
			if (error instanceof ContentApiError && error.status === 401) {
				// Says why, when the canvas shows published content (guides/canvas).
				console.warn('Content refused the draft token:', error.body)
				return delivery.getPage<PageEntry>(path, { depth: 2 })
			}
			throw error
		})
	if (!entry) notFound()

	return <Page entry={entry} />
}
```

## 4. Render the entry [#4-render-the-entry]

`useLiveEntry` returns the editor's unsaved version while the page is open in the canvas, and `entry` everywhere else. `editable` marks the entry and its blocks, so editors can click them.

```tsx title="src/components/Page.tsx"
'use client'

import {
	type Block,
	editable,
	type Entry,
	RichText,
	type RichTextNode,
	useLiveEntry,
} from '@obelism/content-react'

type HeroBlock = Block<{ heading: string | null }, 'hero'>
type TextBlock = Block<{ text: RichTextNode | null }, 'text'>

export type PageEntry = Entry<
	{ title: string | null; body: (HeroBlock | TextBlock)[] | null },
	'page'
>

export const Page = ({ entry }: { entry: PageEntry }) => {
	const { fields } = useLiveEntry(entry)

	return (
		<main {...editable(entry)}>
			<h1>{fields.title}</h1>
			{fields.body?.map((block) =>
				block._type === 'hero' ? (
					<section key={block._uid} {...editable(block)}>
						<h2>{block.heading}</h2>
					</section>
				) : (
					<section key={block._uid} {...editable(block)}>
						<RichText document={block.text} />
					</section>
				),
			)}
		</main>
	)
}
```

The types in `content/examples/` show what each of your content types looks like; adjust `PageEntry` to your own model.

## 5. Start the bridge [#5-start-the-bridge]

```tsx title="src/app/layout.tsx"
import { ContentBridge } from '@obelism/content-react'

export default function RootLayout({
	children,
}: {
	children: React.ReactNode
}) {
	return (
		<html lang="en">
			<body>
				<ContentBridge />
				{children}
			</body>
		</html>
	)
}
```

It does nothing outside the editor.

## 6. Let the CMS frame the site [#6-let-the-cms-frame-the-site]

```ts title="next.config.ts"
const nextConfig = {
	async headers() {
		return [
			{
				source: '/:path*',
				headers: [
					{
						key: 'Content-Security-Policy',
						value: "frame-ancestors 'self' https://content.obelism.studio",
					},
				],
			},
		]
	},
}

export default nextConfig
```

Then set the environment's **preview URL** (Settings) to where the site runs, such as `http://localhost:3000`, and open the **Canvas**.

## Next steps [#next-steps]

- [Revalidate on publish](/docs/guides/webhooks): call `revalidateTag('content')` from a webhook route.
- [Editing on the site](/docs/guides/site-editing): one route handler, and editors sign in on the site itself.
- [Images](/docs/api/images): resized and re-encoded copies with a query on the URL.
- [A/B tests in content](/docs/guides/ab-tests), with Obelism Improve.
