Skip to content
Delivery API
Before you plan around a feature, check the capability status.

Get started

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.

  • 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://localhost while you work locally).

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:

Terminal window
ATLAS_CMS_ORIGIN=https://delivery.atlas.example
ATLAS_ENVIRONMENT_ID=env_XXXXXXXX
ATLAS_DELIVERY_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
lib/atlas.ts
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.

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.

app/[[...slug]]/page.tsx
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.

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')).

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.

  • A page published in Studio appears on your Development website and looks the same as before.
  • With ATLAS_CMS_ORIGIN pointed 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/static output).
  • Studio shows the deployment as In sync.

Next: migration checklist for the rest of the site.