Updated 2026-09-25
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, ornext.config.ts. - One root layout:
app/layout.tsx,app/layout.jsx,src/app/layout.tsx, orsrc/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
- 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>." - Put this in the root layout. If the layout already exists, add the
Scriptimport and theScripttag. Do not replace the rest of the file. Install automatically does the same edit: it insertsimport 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.
- 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.
App with login
- Set Settings → Client access → Authentication to App with login and save.
- Set
MODIFEE_SECRETfrom 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 and Secret and redeploy. - 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.
- 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.tsandcomponents/modifee-widget.tsx, orsrc/lib/modifee.tsandsrc/components/modifee-widget.tsxwhen the layout is undersrc/app/. The layout gets an import ofModifeeWidgetfrom 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
- Deploy the layout.
- Open Settings → Widget install.
- Press Check installation.
- 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 onthe saved site URL.Paste the snippet just before </body> and check again.The widget on this page belongs to another project.We couldn't reachthe saved site URL.Connection failed. The widget will be marked installed as soon as it loads on the site.The same sentence usesTimed outorHTTPplus the status code instead ofConnection failed.This page is rendered in the browser, so the widget script is not in the HTML we fetched fromthe 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
modifeeTokenwhen 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.
- "This email isn't allowed to request changes on this site." is MDF-AUTH-002. See Email not allowed.
- More cases: Widget not showing and Secret and redeploy.