# How the Next.js Starter Works

> Source: https://agilitycms.com/docs/nextjs/how-the-next-js-starter-works

This is a deep dive into how the [Agility Next.js Starter](https://github.com/agility/agilitycms-nextjs-starter) is put together — routing, rendering, components, data fetching, caching, and preview. It's built on the **App Router** with **React Server Components** and **Cache Components**, the same model described in [Rendering & Data Fetching with Next.js](/docs/nextjs/next-js-and-server-side-rendering) and [Caching with Next.js and Agility](/docs/nextjs/caching-with-next-js-and-agility).

## Core Concepts

### Content-Driven Architecture

The starter is built on the principle that **content drives everything**:

```
Agility CMS (content & structure)
         ↓
   Sitemap + Pages
         ↓
  React Components
         ↓
  Prerendered HTML
```

- Editors control page structure in Agility CMS.
- Developers define component behavior in React.
- Next.js prerenders optimized pages.
- Users get fast page loads.

### Server-First Rendering

By default, everything is a **React Server Component**:

```tsx
// Default: Server Component (async) — fetch directly, ship no JS
export default async function MyComponent({ module }) {
  const data = await fetchData()
  return <div>{data.title}</div>
}

// Only when you need interactivity: a Client Component
"use client"
export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}
```

### Separation of Concerns

| Layer | Location | Responsibility |
| --- | --- | --- |
| Presentation | `components/` | UI rendering |
| Domain logic | `lib/cms-content/` | App-specific content shaping |
| CMS utilities | `lib/cms/` | Generic, cached CMS reads |
| Types | `lib/types/` | TypeScript interfaces |
| Routing | `app/` | Next.js routing |

## The Page Lifecycle

### 1. Static params

At build time, Next.js asks which pages exist. The starter answers from the Agility sitemap:

```tsx
// app/[...slug]/page.tsx
export async function generateStaticParams() {
  const sitemap = await getSitemapFlat({ locale: "en-us", preview: false })
  return Object.values(sitemap).map((node) => ({
    slug: node.path.split("/").filter(Boolean),
  }))
}
```

### 2. Metadata

Each page produces SEO metadata via `generateMetadata`:

```tsx
export async function generateMetadata({ params }) {
  const { slug } = await params
  const { page } = await getAgilityPage({ slug: slug ?? [], locale: "en-us", preview: false })
  return {
    title: page.title,
    description: page.seo?.metaDescription,
    openGraph: { title: page.title, description: page.seo?.metaDescription },
  }
}
```

### 3. Rendering

```tsx
export default async function Page({ params }) {
  const { slug } = await params
  const { page } = await getAgilityPage({ slug: slug ?? [], locale: "en-us", preview: false })
  const Template = getPageTemplate(page.templateName)
  return <Template page={page} />
}
```

### 4. In production

Because every CMS read is cached and tagged (see **Caching** below), a published page is served from the prerendered cache — instantly. When an editor publishes, Agility's webhook invalidates exactly the tags that changed, and the affected pages rebuild on the next request. There's no fixed revalidate timer to wait out.

## Dynamic Routing

Instead of a file per page, the starter uses a single **catch-all** route:

```
app/
├─ layout.tsx        # root layout
└─ [...slug]/
   ├─ page.tsx       # handles /, /about, /blog, /blog/post-1, …
   ├─ error.tsx      # error boundary
   └─ not-found.tsx  # 404
```

`[...slug]` matches any path; `getAgilityPage` resolves it against the Agility sitemap to the right page, template, and components.

| URL | `slug` param | CMS page |
| --- | --- | --- |
| `/` | `[]` | Homepage |
| `/about` | `['about']` | About |
| `/blog/my-post` | `['blog','my-post']` | Blog post |

## Component Architecture

### Component Registry

Agility Components are mapped to React components by name:

```tsx
// components/agility-components/index.ts
import { Module } from "@agility/nextjs"
import Heading from "./Heading"
import RichTextArea from "./RichTextArea"

const allModules: Module[] = [
  { name: "Heading", module: Heading },
  { name: "RichTextArea", module: RichTextArea },
]

export const getModule = (name: string) =>
  allModules.find((m) => m.name.toLowerCase() === name.toLowerCase())?.module || null
```

> The `@agility/nextjs` API still calls these "modules" in code — in the Agility UI they're **Components**. Same thing.

### Component Props

```tsx
import { UnloadedModuleProps } from "@agility/nextjs"

interface IMyComponent { heading: string; content: string }

export default async function MyComponent({ module, page, languageCode }: UnloadedModuleProps) {
  const { fields } = module as { fields: IMyComponent }
  return (
    <section>
      <h2>{fields.heading}</h2>
      <div>{fields.content}</div>
    </section>
  )
}
```

### Server by default, client when interactive

Server Components can fetch directly; add `"use client"` only where you need state/effects. A common pattern is a Server Component that fetches and hands data to a small Client Component for interactivity:

```tsx
// PostsListing.tsx (server) — fetches
export default async function PostsListing({ module }) {
  const { posts } = await getPostListing({ take: 10 })
  return <PostsListingClient initialPosts={posts} />
}

// PostsListing.client.tsx (client) — filtering / infinite scroll
"use client"
export default function PostsListingClient({ initialPosts }) {
  const [posts, setPosts] = useState(initialPosts)
  return <div>{/* interactive UI */}</div>
}
```

## Page Templates

Templates define layout and render Components into named **zones** with `<ContentZone>`:

```tsx
// components/agility-pages/MainTemplate.tsx
import { ContentZone } from "@agility/nextjs"
import { getModule } from "../agility-components"

export default function MainTemplate({ page }) {
  return (
    <div className="max-w-7xl mx-auto">
      <ContentZone name="MainContent" page={page} getModule={getModule} />
    </div>
  )
}
```

`<ContentZone>` looks up `page.zones.MainContent`, resolves each Component via `getModule`, and renders it. Templates can declare multiple zones (e.g. `MainContent` + `Sidebar`), and are resolved by name through a template registry:

```tsx
// components/agility-pages/index.ts
export const getPageTemplate = (name: string) =>
  ({ MainTemplate, TwoColumnTemplate } as Record<string, any>)[name] || MainTemplate
```

## Data Fetching Strategy

Content flows through three tiers, so business logic stays out of both the SDK and your components:

```
Component  (what to display)
   ↓  getPostListing()
Domain     lib/cms-content/  (how to build "blog posts with URLs")
   ↓  getContentList()
CMS        lib/cms/          (cached, tagged Agility reads)
   ↓  @agility/content-fetch
Agility CMS API
```

**CMS layer** — a thin cached wrapper (this is the Cache Components pattern; see the [caching guide](/docs/nextjs/caching-with-next-js-and-agility)):

```tsx
// lib/cms/getContentItem.ts
import { cacheTag, cacheLife } from "next/cache"
import { connection } from "next/server"

export const getContentItem = async <T>(params) => {
  if (params.preview) {
    await connection()               // preview is never cached
    return fetchContentItem<T>(params)
  }
  return cachedContentItem<T>(params)
}

const cachedContentItem = async <T>(params) => {
  "use cache"
  cacheTag(`agility-content-${params.contentID}-${params.languageCode}`)
  cacheLife("days")
  return fetchContentItem<T>({ ...params, preview: false })
}
```

**Domain layer** — app-specific shaping (compose CMS reads, add computed URLs/excerpts):

```tsx
// lib/cms-content/getPostListing.ts
export async function getPostListing({ take = 10, skip = 0 }) {
  const { items } = await getContentList<IPost>({ referenceName: "posts", languageCode: "en-us", take, skip })
  const sitemap = await getSitemapFlat({ locale: "en-us", preview: false })
  const blog = Object.values(sitemap).find((n) => n.name === "Blog")
  return items.map((p) => ({ ...p.fields, url: `${blog?.path}/${p.fields.slug}` }))
}
```

**Component layer** — just renders what the domain layer returns.

## Caching & Performance

The starter uses **Cache Components** (`cacheComponents: true`). In short:

- Every CMS read is wrapped in `"use cache"` and tagged with a stable key — `agility-content-{contentID}-{locale}`, `agility-content-{referenceName}-{locale}`, `agility-page-{pageID}-{locale}`, `agility-sitemap-flat-{locale}`.
- Pages prerender to a static shell and are served from cache.
- On publish, Agility's webhook calls `revalidateTag(tag, "max")` for the tags that changed, so only the affected pages rebuild — editors see updates almost immediately.
- Preview/draft reads bypass the cache entirely via `connection()`.

The complete setup, the tag contract, and the gotchas are in **[Caching with Next.js and Agility](/docs/nextjs/caching-with-next-js-and-agility)** — the canonical reference. Don't hand-tune `export const revalidate` / `dynamic` per route; Cache Components handles that.

## Preview Mode

Preview lets editors see unpublished drafts. It's built on Next's **`draftMode()`**:

1. Agility opens a preview URL with an `agilitypreviewkey`. A route (or middleware) validates the key and calls `draftMode().enable()`, then redirects to the real page.
2. With draft mode on, `getAgilityContext()` reports `isPreview: true`, so the CMS wrappers take their **uncached** `connection()` branch and request the **preview** API key — returning the latest draft content.
3. A small `PreviewBar` client component shows preview is active and can exit draft mode.

Because preview is uncached and per-request, drafts always reflect the editor's latest save. See [Rendering & Data Fetching with Next.js](/docs/nextjs/next-js-and-server-side-rendering#preview-is-always-dynamic).

## Image Optimization

Use **`AgilityPic`** for Agility images — it renders a responsive `<picture>` backed by Agility's image API:

```tsx
import { AgilityPic } from "@agility/nextjs"

<AgilityPic image={fields.image} fallbackWidth={800} className="rounded-lg" />
```

See [Using the AgilityPic Component](/docs/nextjs/using-the-agilitypic-component-for-responsive-images) for the full API. For non-CMS images, use Next's own `<Image>`.

## API Routes

### Revalidate (the publish webhook)

```tsx
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache"

export async function POST(request: Request) {
  const p = await request.json()
  const locale = p.languageCode
  if (p.contentID)     revalidateTag(`agility-content-${p.contentID}-${locale}`, "max")
  if (p.referenceName) revalidateTag(`agility-content-${p.referenceName.toLowerCase()}-${locale}`, "max")
  if (p.pageID) {
    revalidateTag(`agility-page-${p.pageID}-${locale}`, "max")
    revalidateTag(`agility-sitemap-flat-${locale}`, "max")
  }
  return Response.json({ revalidated: true })
}
```

Configure it in Agility under **Settings → Webhooks** pointing at `POST /api/revalidate`. (`revalidateTag(tag, "max")` is the Next 16 form; on Next 15 use `revalidateTag(tag)`.)

## Summary

The starter is a modern App Router build:

- **Server-first** rendering (RSC) for performance.
- **Catch-all routing** driven by the Agility sitemap.
- A **component + template registry** for editor-composed pages.
- **Cache Components** caching with tag-based, publish-driven invalidation.
- **`draftMode()`** preview that bypasses the cache.
- **Type safety** throughout.
