Connect a Next.js website
Read published Atlas content from your existing Next.js App Router site, with preview, fallback and instant refresh on publish.
This guide connects an existing Next.js (App Router) website to an Atlas Development environment. Nothing changes on your public site until you deploy the code yourself.
Before you start
Section titled “Before you start”- A site in Atlas Studio, and an active site contract for it. See Site contract.
- Your website can run server code: server components, route handlers, Node or Cloudflare Workers.
- The HTTPS origin your Development website runs on (or
http://localhostwhile you work locally).
1. Create a delivery token
Section titled “1. Create a delivery token”In Studio open Integrations → Website → New delivery token. Choose a lifetime (1 to 365 days). The token is shown once; Atlas stores only a fingerprint of it.
Put the values in your website’s server environment, for example .env.local or your host’s environment settings:
ATLAS_CMS_ORIGIN=https://delivery.atlas.exampleATLAS_ENVIRONMENT_ID=env_XXXXXXXXATLAS_DELIVERY_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX2. Add a small server-only client
Section titled “2. Add a small server-only client”import 'server-only';
export type AtlasSection = { id: string; component: { id: string; version: string }; // a component from your site contract props: Record<string, unknown>; hidden: boolean;};export type AtlasPage = { page: { id: string; route: string; title: string }; document: { regions: Record<string, AtlasSection[]> }; // region keys come from your page type seo: { title: string; description: string; noindex: boolean; canonicalPath?: string }; assets: Record<string, { url: string; alt?: string }>;};
const base = `${process.env.ATLAS_CMS_ORIGIN}/v1/delivery/environments/${process.env.ATLAS_ENVIRONMENT_ID}`;
export async function atlas<T>(path: string, init?: { revalidate?: number }): Promise<T | null> { try { const response = await fetch(`${base}${path}`, { headers: { Authorization: `PublishedConsumer ${process.env.ATLAS_DELIVERY_TOKEN}` }, next: { revalidate: init?.revalidate ?? 60, tags: ['atlas'] }, signal: AbortSignal.timeout(5000), }); if (response.status === 404) return null; if (!response.ok) throw new Error(`Atlas ${response.status}`); return (await response.json()) as T; } catch (error) { console.error('Atlas unavailable, using built-in content', error); return null; // the caller falls back to the site's own content }}The client returns null when Atlas is unreachable. That is what makes the fallback work.
3. Render a page from its sections
Section titled “3. Render a page from its sections”The pages endpoint returns the published page for a route. Its document.regions hold sections in order; each section names one of your contract’s components and carries its field values.
import { notFound } from 'next/navigation';import { atlas, type AtlasPage } from '@/lib/atlas';import { renderSection } from '@/atlas/sections';import { builtInPage } from '@/content/built-in';
export const revalidate = 300;
export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) { const route = '/' + ((await params).slug ?? []).join('/'); const page = await atlas<AtlasPage>(`/pages?route=${encodeURIComponent(route)}`);
if (!page) { const fallback = builtInPage(route); // today's content, unchanged if (!fallback) notFound(); return fallback; } const sections = (page.document.regions.main ?? []).filter((section) => !section.hidden); return <main>{sections.map((section) => renderSection(section, page.assets))}</main>;}renderSection maps each component.id (for example acme.home.hero) in your contract to your existing React component. Render field values as text or through your components. Never pass CMS content to dangerouslySetInnerHTML.
Your hard-coded routes keep working next to this catch-all route. You decide which routes Atlas manages.
4. Add draft preview
Section titled “4. Add draft preview”Studio’s Preview on website opens https://<your site>/api/atlas/preview?token=…. The link is valid for 15 minutes. Add that route:
// app/api/atlas/preview/route.ts — outline// 1. Read ?token= and call GET /preview/pages?token=<token> with your delivery token.// 2. 403 PREVIEW_INVALID or 410 PREVIEW_EXPIRED → show a plain error.// 3. Render the draft with <meta name="robots" content="noindex"> and never cache it.Preview works for Development environments only.
To let editors see drafts inside Studio’s page editor, allow Studio in your Content-Security-Policy: frame-ancestors 'self' https://<studio origin>. Optional click-to-edit is described in Preview, publish and roll back.
5. Refresh pages the moment content is published
Section titled “5. Refresh pages the moment content is published”Create a webhook in Studio (Integrations → Webhooks) pointing at a route on your site, and store its signing secret in a server environment variable. Verify every request before acting on it; see Webhooks for the exact check. Then call revalidatePath for the affected routes (or revalidateTag('atlas')).
6. Report your build
Section titled “6. Report your build”When your server starts, send POST /deployments with the digest of the contract bundle this build renders and the component ids and versions it supports. Studio then shows under Integrations → Website whether the website is in sync with the active contract. A release cannot go to Production while the site is out of sync. Details: Delivery API.
7. Check it
Section titled “7. Check it”- A page published in Studio appears on your Development website and looks the same as before.
- With
ATLAS_CMS_ORIGINpointed at a wrong address, the website still serves its built-in content. - A webhook with a wrong signature is rejected.
- The delivery token does not appear in any browser bundle (search your
.next/staticoutput). - Studio shows the deployment as In sync.
Next: migration checklist for the rest of the site.