How do you capture UTM parameters in a Next.js form?
To capture UTM parameters in a Next.js form, read them on arrival with useSearchParams in a client component, save them in a first-party cookie, and include them in the form payload, for example as hidden inputs read by a server action. The cookie matters because client-side navigation drops the query string before most visitors reach the form.
- Capture UTMs in a client component in the root layout.
- Store them in a cookie.
- Read the cookie in the server action.
- Save the values with the lead.
- Test across client-side navigations.
A tagged URL hits a Next.js route, the UTMs go into a cookie that survives client navigation, and the form's server action receives them with the submission.
Step 1: capture UTMs on arrival
"use client";
import { useEffect } from "react";
import { useSearchParams } from "next/navigation";
const KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
export function UtmCapture() {
const params = useSearchParams();
useEffect(() => {
const found = KEYS.filter((k) => params.get(k));
if (!found.length) return;
const data = Object.fromEntries(found.map((k) => [k, params.get(k)]));
document.cookie = `utms=${encodeURIComponent(JSON.stringify(data))}; path=/; max-age=7776000; SameSite=Lax`;
}, [params]);
return null;
}
Render it inside a Suspense boundary in the root layout, as the useSearchParams docs recommend for static routes.
Step 2: read it in a server action
"use server";
import { cookies } from "next/headers";
export async function submitLead(formData: FormData) {
const raw = (await cookies()).get("utms")?.value;
const utms = raw ? JSON.parse(decodeURIComponent(raw)) : {};
// save formData fields plus utms to your database or CRM
}
Reading the cookie server-side avoids hidden inputs entirely. If you post to a third-party form endpoint instead, render hidden inputs from the cookie.
Step 3: first or last touch
The code above is last touch. For first touch, skip the write when the cookie exists. See persisting UTM parameters across pages for the trade-offs and consent.
Step 4: test
| Test | Expected |
|---|---|
Land on /?utm_source=test, navigate with Link, submit | Lead has "test" |
| Hard refresh on a later page, submit | Still "test" |
| No consent given | No cookie written (if gated) |
That's the manual route. To skip it, pre-register for Trakt and every submission arrives with its source attached.
Why hand-built Next.js tracking breaks (and how Trakt helps)
Static rendering without Suspense, middleware rewrites that drop params, and consent gating all bite.
How Trakt closes the gap
Trakt's script stores UTMs and click IDs in a first-party cookie, respects consent tools, and adds auto-injected hidden UTM fields to your forms. It works with all AI-built sites, keeps the first touch, and syncs source and revenue into your CRM.
Hand-built vs. Trakt, by requirement
| Requirement | Your code | Trakt |
|---|---|---|
| Capture and store | Component and cookie | Built in |
| Forms on any route | Server action reads cookie | Auto-injected hidden UTM fields |
| Consent | Your logic | Consent Mode v2, OneTrust, Cookiebot |
| A form worth submitting | Same either way | Same either way |
FAQ
How do I get query parameters in Next.js?
In the App Router, use useSearchParams in client components or the searchParams prop in pages. Store UTMs on arrival because they disappear on navigation.
Why do UTM parameters disappear in Next.js?
Client-side navigation changes the URL without the query string. Save the values in a cookie before the visitor moves on.
Should I use middleware to capture UTMs?
You can set the cookie in middleware on the first request, which also works without JavaScript. Make sure consent rules still apply.
How do I send UTMs with a server action?
Read the UTM cookie with cookies() inside the action, or include hidden inputs in the form. Save both with the lead.
Does this work with the Pages Router?
Yes. Use useRouter().query to read params and an API route to read the cookie.
What to do first
Drop the capture component into your root layout, then land with ?utm_source=test, navigate twice and submit. Check the lead record.
