# Deploying Next.js to Netlify

> Source: https://agilitycms.com/docs/nextjs/deploying-next-js-to-netlify

Netlify runs Next.js through its **Next.js Runtime**, which is detected and installed automatically — App Router, React Server Components, streaming, route handlers, the proxy and on-demand revalidation all work. This guide covers deploying an Agility-powered site, wiring publish-triggered updates, and the two things that most often break preview after a green deploy.

> **Coming from the older recipe?** This article used to tell you to let Netlify install the **Essential Next.js Build Plugin** and warned that "Netlify Preview for Next.js is still experimental." Both are out of date. The Essential Next.js plugin was superseded by the **Next.js Runtime** (v5), which Netlify installs for you — you should *not* be adding `@netlify/plugin-nextjs` to `netlify.toml` by hand on a new site. And preview is no longer experimental; it just needs the configuration in the "Making preview work" section below. The old guide also pointed you at **Settings → Sitemaps**, which is now **Settings → Deployment**.

## Deploy via the Agility integration

In the Agility Manager, go to **Settings → Deployment** and click **Setup Deployment**, then choose the Netlify automated deployment.

> **Note:** You'll need a GitHub account and a Netlify account.

The wizard authorizes Netlify against your Git provider, creates the repository, sets your `AGILITY_*` environment variables, builds, and writes the resulting domain back into your Agility instance.

## Deploy manually

1. Push your repository to GitHub, GitLab or Bitbucket.
2. In Netlify, **Add new site → Import an existing project**, and pick the repo. Netlify detects Next.js; leave the build command (`npm run build`) and publish directory alone — the runtime handles them.
3. Under **Site configuration → Environment variables**, add:

   | Variable | Purpose |
   |---|---|
   | `AGILITY_GUID` | Your instance identifier |
   | `AGILITY_API_FETCH_KEY` | Live (published) content |
   | `AGILITY_API_PREVIEW_KEY` | Draft content, for preview |
   | `AGILITY_SECURITY_KEY` | Validates preview links and webhooks |
   | `AGILITY_LOCALES` | Comma-separated, first is the default — **no spaces** |
   | `AGILITY_SITEMAP` | Sitemap channel name, usually `website` |

   Make sure they're scoped to **all deploy contexts**, not just production. A variable missing from Deploy Previews is the usual reason a PR preview 500s while production is fine.

4. Deploy.

Then register the deployed URL in Agility under **Settings → Deployment → Setup Deployment → Custom Deployment**, so editors' preview links point at the right host.

## Updating content without rebuilding

You do not need a full rebuild to publish a change. With [Cache Components](/docs/nextjs/caching-with-next-js-and-agility), each content read is cached under a tag:

```ts
const cachedContentItem = async (params) => {
  "use cache"
  cacheTag(`agility-content-${params.contentID}-${params.languageCode}`)
  cacheLife("days")
  return fetchContentItem(params)
}
```

and a route handler clears the tag when Agility says something changed:

```ts
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache"

export async function POST(req: NextRequest) {
  const data = await req.json()

  if (data.state === "Published" && data.referenceName) {
    // Next 16 requires the second argument — the cache profile to expire
    revalidateTag(`agility-content-${data.referenceName}-${data.languageCode}`, "max")
  }

  return NextResponse.json({ revalidated: true })
}
```

> `revalidateTag(tag)` with one argument is a Next 15 signature. In Next 16 it takes a cache-life profile as a second argument, and omitting it is a build error.

In the Manager, go to **Settings → Web Hooks** and point a hook at `https://your-site.com/api/revalidate`, with **Receive Content Publish Events** checked. Send a **Test Payload** and confirm a success response before relying on it.

### If you'd rather rebuild

For a fully static site, trigger a build instead:

1. In Netlify, go to **Site configuration → Build & deploy → Build hooks** and create a hook. Copy the URL it generates.
2. In Agility, **Settings → Web Hooks**, add a hook pointing at that URL.
   - **Production builds** — check *Receive Content Publish Events*.
   - **Staging/preview builds** — uncheck publish events, check *Receive Content Save Events*.

A rebuild takes minutes and re-renders the whole site; tag revalidation takes seconds and re-renders only what changed. Prefer the webhook above unless you have a reason not to.

## Making preview work on a deployed site

### 1. Cached pages skip the proxy

This is the one that costs people a day. Netlify serves prerendered pages straight from its CDN **without invoking your proxy** (`proxy.ts` — Next 16's renamed `middleware.ts`). So on exactly the pages that matter most, `?agilitypreviewkey=` never reaches `/api/preview`, draft mode is never enabled, and Web Studio silently renders the **published** page. No error appears anywhere, and you cannot reproduce it locally, because under `next start` the proxy always runs.

Fix it with a `beforeFiles` rewrite, which is compiled into the routes manifest and evaluated *before* the cache lookup:

```js
// next.config.mjs
async rewrites() {
  return {
    beforeFiles: [
      {
        source: "/:path((?!api).*)",
        has: [{ type: "query", key: "agilitypreviewkey" }],
        destination: "/api/preview?slug=/:path",
      },
    ],
  }
}
```

Keep your proxy logic as well — it handles uncached requests, and the two converge on the same handler. Full detail in [Handling Preview URLs & Request Lifecycle](/docs/nextjs/handling-preview-urls-request-lifecycle).

### 2. Draft renders must not reach a shared cache

Draft mode shows unpublished content. If your cache headers are unconditional, that content gets stored on a CDN and served to the public. Set them where the draft cookie is visible — in the proxy:

```ts
const DRAFT_COOKIE = "__prerender_bypass"

if (request.cookies.has(DRAFT_COOKIE)) {
  res.headers.set("Cache-Control", "private, no-store")
} else {
  res.headers.set("CDN-Cache-Control", "public, s-maxage=60, stale-while-revalidate=86400")
}
```

`CDN-Cache-Control` is honoured by Netlify and Vercel alike, so this stays vendor-neutral.

## Deploy Previews

Netlify builds a Deploy Preview for every pull request automatically. Two things to check:

- Your `AGILITY_*` variables are available in the **Deploy Preview** context, not just Production.
- Deploy Preview URLs change per PR. If you want editors previewing against them, register the pattern in Agility rather than a single fixed host.

## Verify the deployment

```bash
curl -I https://your-site.com/about-us         # 200, with CDN cache headers
curl -I https://your-site.com/no-such-page     # a real 404, not a 200
```

Then publish something in Agility and confirm it appears within seconds — the end-to-end test that actually proves the webhook, the tag and the cache are all wired together.

## Summary

1. **Settings → Deployment** in Agility for the automated path, or import the repo in Netlify directly.
2. The **Next.js Runtime** is installed for you — don't hand-add the old Essential Next.js plugin.
3. Set every `AGILITY_*` variable in **all deploy contexts**.
4. Prefer a publish webhook calling `revalidateTag(tag, "max")` over a full rebuild.
5. Add the `beforeFiles` preview rewrite, or preview breaks on exactly the cached pages editors care about — silently.
