# Next.js App Router

Add the widget script to the Next.js root layout, then confirm Widget install is complete.

## Prerequisites

- A Modifee project whose repository has `next.config.js`, `next.config.mjs`, `next.config.cjs`, or `next.config.ts`.
- One root layout: `app/layout.tsx`, `app/layout.jsx`, `src/app/layout.tsx`, or `src/app/layout.jsx`.
- **Site URL** saved under **Settings → Connection**.
- **Authentication** chosen under **Settings → Client access**. The wizard step **Client access** uses **Public site, no login** and **Site with login** for the same two modes. Settings calls them **Site without login** and **App with login**. The default is **Site without login**.

## Steps

Do this from the wizard step **Install the widget**, or from **Settings → Widget install**.

### Site without login

1. Open **Install manually** and select the tab **Next.js (App Router)**. The instruction is "Add this once to the root app/layout.tsx, inside `<body>`."
2. Put this in the root layout. If the layout already exists, add the `Script` import and the `Script` tag. Do not replace the rest of the file. **Install automatically** does the same edit: it inserts `import Script from "next/script"` and places the tag just before `</body>`.

```
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Script
          src="https://www.modifee.com/widget.js"
          data-project="YOUR_PROJECT_KEY"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
```

`YOUR_PROJECT_KEY` is the `data-project` value already filled in on the **Next.js (App Router)** tab. The same value is shown under **Settings → Danger zone**. `https://www.modifee.com/widget.js` is the script URL the dashboard uses when `NEXT_PUBLIC_APP_URL` is unset. Copy `src` from the tab if your dashboard shows a different origin.

This mode does not use `MODIFEE_SECRET`.

3. Or press **Install automatically**. Modifee opens a pull request titled "Install Modifee widget" on branch `modifee/install-widget`. Merge it, wait for the site to redeploy, then check the installation. The button on that pull request is **Merge on GitHub**.

If the repo also has `pages/_document`, this mode edits that file too. See [Next.js Pages Router](https://www.modifee.com/docs/install/nextjs-pages-router).

### App with login

1. Set **Settings → Client access → Authentication** to **App with login** and save.
2. Set `MODIFEE_SECRET` from **Settings → Client access → Signing secret**. The value is in the code block labeled **MODIFEE_SECRET**. If Vercel is not connected, the page says "Add this value as the MODIFEE_SECRET environment variable on your host. The agency app signs the logged-in user's email with HMAC-SHA256." Press **Connect Vercel** when that button is shown. After a push, the line is "MODIFEE_SECRET set on Vercel ✓". Redeploy production and preview so the new variable is live. See [Signing secret](https://www.modifee.com/docs/vercel/signing-secret) and [Secret and redeploy](https://www.modifee.com/docs/troubleshooting/secret-and-redeploy).
3. Open **Install manually** and select **Next.js (App Router)**. The tab also lists **Next.js (Pages Router)**, **Flask**, **Django**, **Laravel**, and **Rails**. Paste the signing function and the layout. The hint is "Server Component. Create the token on the server and render the tag only for a logged-in user." Then "Store the secret as MODIFEE_SECRET in your server environment." and "Sign the email on the server. The browser only receives the signature."

```
// server-only — never import this file from client code
import crypto from "crypto";

export function modifeeToken(email, name) {
  const payload = Buffer.from(
    JSON.stringify({
      email,
      name,
      exp: Math.floor(Date.now() / 1000) + 86400,
    }),
  ).toString("base64url");
  const sig = crypto
    .createHmac("sha256", process.env.MODIFEE_SECRET)
    .update(payload)
    .digest("base64url");
  return `${payload}.${sig}`;
}

import { modifeeToken } from "@/lib/modifee";

export default async function RootLayout({ children }) {
  const user = await getSessionUser();
  return (
    <html lang="en">
      <body>
        {children}
        {user ? (
          <script
            src="https://www.modifee.com/widget.js"
            data-project="YOUR_PROJECT_KEY"
            data-token={modifeeToken(user.email, user.name)}
            async
          />
        ) : null}
      </body>
    </html>
  );
}
```

`YOUR_PROJECT_KEY` is the key in the dashboard snippet. `getSessionUser` is not a Modifee function. Replace it with the call that returns the signed-in user. The tag renders only when that user exists. Guests do not get a token, so they do not see the widget.

4. Or press **Install automatically**. For **App with login** the pull request adds three files and does not use the manual layout above. Paths are `lib/modifee.ts` and `components/modifee-widget.tsx`, or `src/lib/modifee.ts` and `src/components/modifee-widget.tsx` when the layout is under `src/app/`. The layout gets an import of `ModifeeWidget` from the relative path to the component, and `<ModifeeWidget />` just before `</body>`.

```
import { createHmac } from "crypto";

/** Server-only. Signs a Modifee client token. Never import this from client code. */
export function modifeeToken(email: string, name?: string | null): string {
  const secret = process.env.MODIFEE_SECRET;
  if (!secret) {
    throw new Error("MODIFEE_SECRET is not set");
  }
  const payload = Buffer.from(
    JSON.stringify({
      email,
      name: name ?? undefined,
      exp: Math.floor(Date.now() / 1000) + 86_400,
    }),
  ).toString("base64url");
  const signature = createHmac("sha256", secret).update(payload).digest("base64url");
  return `${payload}.${signature}`;
}

/**
 * Return a token for the signed-in user, or null for guests.
 * The widget stays hidden until this returns a token.
 *
 *   const user = await auth();
 *   if (!user?.email) return null;
 *   return modifeeToken(user.email, user.name);
 */
export async function modifeeWidgetToken(): Promise<string | null> {
  return null;
}
```

```
import { modifeeWidgetToken } from "../lib/modifee";

export async function ModifeeWidget() {
  const token = await modifeeWidgetToken();
  if (!token) return null;
  return (
    <script src="https://www.modifee.com/widget.js" data-project="YOUR_PROJECT_KEY" data-token={token} async />
  );
}
```

The generated component imports `../lib/modifee`, the path from `components/modifee-widget.tsx` to `lib/modifee.ts` (the same relative path under `src/`). `modifeeWidgetToken` returns `null` until you replace that body with your session, as in the comment. Until it returns a token, the widget stays hidden and **Check installation** can still find the script only after you render the tag.

The page that prints `data-token` must be rendered for that request. A static build, ISR, or a CDN cache can send one user's token to someone else. On the App Router, add `export const dynamic = "force-dynamic"` to the layout or page that reads the session. Reading cookies, headers, or the session in that layout does the same job when Next treats the route as dynamic.

If the root layout starts with `"use client"`, automatic install stops and shows "The root layout is a client component, so the signing file can't be added there. Switch to Site without login, or paste the snippet on the server. No pull request was opened."

If `pages/_document` also exists, the pull request body includes "Pages Router routes are not included. Those pages still need the snippet, or switch this project to Site without login."

## Verify

1. Deploy the layout.
2. Open **Settings → Widget install**.
3. Press **Check installation**.
4. The check text is "Found on the site." The section also says "Widget found on the site." The mark beside **Widget install** turns green.

The mark is also green when the widget has loaded on the public site and no failed check is stored. A failed check keeps it yellow. The tooltip is "Widget not found on the site".

Loading the page in a browser sends a ping. That records the widget as seen. It does not turn the mark green if the latest check is missing, the wrong key, unreachable, or a browser-rendered page.

## Troubleshooting

- "Add a site URL before checking."
- "Enter a public http(s) URL."
- "Enter a public site URL. Local addresses can't be used here."
- `We couldn't find the widget on` the saved site URL. `Paste the snippet just before </body> and check again.`
- `The widget on this page belongs to another project.`
- `We couldn't reach` the saved site URL. `Connection failed. The widget will be marked installed as soon as it loads on the site.` The same sentence uses `Timed out` or `HTTP` plus the status code instead of `Connection failed`.
- `This page is rendered in the browser, so the widget script is not in the HTML we fetched from` the saved site URL. `Open the site after you paste the snippet. We'll mark this done as soon as the widget loads.`
- "Merged — waiting for deploy"
- "This project has no API key." "Generate an API key before installing the widget." **Settings → Danger zone** shows "No active key." until a key exists. Projects get a key when they are created.
- "Missing GitHub permissions. Re-authorize the GitHub App with contents:write and pull_requests:write." The link is **Re-authorize**.
- "This repository is archived. No pull request was opened."
- "GitHub rate limit reached. Wait a minute and try again."
- "Could not open the pull request."
- "This repo has more than one Next.js app, so there isn't one layout to edit. Paste the snippet above. No pull request was opened."
- "This Next.js app has more than one root layout. Paste the snippet above. No pull request was opened."
- The file path, then `has no </body> tag to attach the widget to. Paste the snippet above. No pull request was opened.`
- "MODIFEE_SECRET is only used for App with login."
- "Link a Vercel project before setting MODIFEE_SECRET."
- "MODIFEE_SECRET is not set" is thrown by the generated `modifeeToken` when the variable is missing on the server.
- Browser console: "Modifee: data-project is required". "Modifee: missing script element". "Modifee: data-project and data-token are required".
- Widget still hidden after a green check, with **Site without login**: the client must open the access-marker URL. See [Access marker](https://www.modifee.com/docs/client-access/access-marker).
- "This email isn't allowed to request changes on this site." is [MDF-AUTH-002](https://www.modifee.com/docs/errors/MDF-AUTH-002). See [Email not allowed](https://www.modifee.com/docs/troubleshooting/email-not-allowed).
- More cases: [Widget not showing](https://www.modifee.com/docs/troubleshooting/widget-not-showing) and [Secret and redeploy](https://www.modifee.com/docs/troubleshooting/secret-and-redeploy).
