Audit Campaign Redirects with Next.js 16 proxy.ts and Vercel Analytics
Audit Campaign Redirects with Next.js 16 proxy.ts and Vercel Analytics A landing-page redirect can burn money quietly.
Audit Campaign Redirects with Next.js 16 proxy.ts and Vercel Analytics
A landing-page redirect can burn money quietly. I have seen a Google Ads launch send 1,184 paid clicks through a tidy /spring-demo URL, only to drop utm_campaign on the final hop into /demo. The page loaded. The form worked. HubSpot still got leads. The problem showed up two days later, when the founder asked why GA4, Vercel Web Analytics, and Stripe trial attribution disagreed by 38 percent.
Next.js 16 gives teams a cleaner place to catch that mistake. When Vercel released Next.js 16 on October 21, 2025, middleware.ts became proxy.ts, with a proxy export that runs before the route renders. As of September 4, 2026, Next.js lists 16.x as Active LTS in its support policy. That matters for small marketing sites because campaign routing stops being a scattered mix of next.config.ts, old redirects, ad-platform destination URLs, and somebody’s half-remembered rewrite rule from Q3.
The redirect layer deserves a launch checklist
Marketing operators already audit ad copy, budgets, pixels, CRM fields, and email follow-up before a launch. Redirects sit in the awkward gap between marketing and engineering, so they get tested with one browser tab and too much hope. Bad idea. A Meta campaign with 37 ad variants, 4 UTMs, and 3 geo pages can create hundreds of URL paths before anyone touches a landing-page builder.
proxy.ts is useful because it makes the request boundary visible. The Next.js proxy docs say Proxy can rewrite, redirect, modify headers, or respond before a request completes. The same docs recommend next.config.ts redirects for simple static cases, which is the right instinct. If /old-pricing always goes to /pricing, use redirects(). If /lp/startup-offer?utm_source=linkedin&utm_campaign=q4-founder-a needs channel rules, preview tagging, and parameter preservation, put the thinking in proxy.ts.
Tiny file. Big blast radius.
The key change in Next.js 16 is naming. The older middleware.ts convention sounded like Express middleware, so teams stuffed too much application logic into it. The upgrade guide says the file is now proxy.ts, the named export is proxy, and the runtime is nodejs. That last bit is easy to miss if your team used Edge-only assumptions in Next.js 13 or 14. For a redirect audit, Node.js is fine. You are reading the URL, stamping a few headers, and returning NextResponse.next() or NextResponse.redirect().
Here is the shape I like for campaign intake:
import { NextRequest, NextResponse } from "next/server";
const CAMPAIGN_ROUTES = new Map([
["/go/founder-demo", "/demo"],
["/go/seo-audit", "/services/seo-audit"],
["/go/q4-webinar", "/webinars/q4-growth"],
]);
const REQUIRED_UTMS = ["utm_source", "utm_medium", "utm_campaign"];
export function proxy(request: NextRequest) {
const url = request.nextUrl.clone();
const target = CAMPAIGN_ROUTES.get(url.pathname);
if (!target) return NextResponse.next();
const missing = REQUIRED_UTMS.filter((key) => !url.searchParams.get(key));
const destination = new URL(target, request.url);
url.searchParams.forEach((value, key) => destination.searchParams.set(key, value));
const response = NextResponse.redirect(destination, 307);
response.headers.set("x-campaign-route", url.pathname);
response.headers.set("x-campaign-utm-missing", missing.join(","));
return response;
}
export const config = {
matcher: ["/go/:path*"],
};
That code is intentionally boring. Three campaign routes. Three required UTM keys. A 307 redirect so the request method is preserved if a form or partner tool ever hits the route with something other than GET. The headers give you something to inspect in Vercel logs, Playwright traces, or a quick curl -I before the media buyer flips a $500-per-day campaign live.
Pair proxy.ts with resilient intake
A redirect audit catches the path problem. Analytics catches the traffic problem. Vercel shipped Web Analytics and Speed Insights version 2 with resilient intake on March 11, 2026. The changelog says the packages discover intake endpoints dynamically instead of relying on one predictable path, and existing setups keep working after you update packages and deploy.
Install the current package before the campaign QA pass:
npm install @vercel/analytics@latest
For an App Router site, I put the component in app/layout.tsx:
import { Analytics } from "@vercel/analytics/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}
Vercel’s quickstart says enabling Web Analytics adds routes under /_vercel/insights/* and a unique path after the next deployment. Its privacy documentation says resilient intake in version 2 uses a build-time random seed for the injected script URL and intake URLs. That is not a magic attribution system. It does mean your audit has a cleaner signal when browser extensions, proxies, or overzealous security filters would have treated one fixed analytics path as disposable plumbing.
This is where marketing ops should care. If LinkedIn Campaign Manager shows 612 clicks, Vercel Web Analytics shows 411 visits on /demo, and the CRM shows 39 form starts, you need to know whether the missing 201 sessions died in the ad platform, at the redirect, in analytics collection, or after the page loaded. Resilient intake does not answer all four questions. It removes one dumb failure mode from the list.
Vercel’s Using Web Analytics docs also call out Production, Preview, and All Environments filters. Use them. I treat preview traffic as launch rehearsal, not noise. A founder clicking 9 preview URLs from Slack, a lifecycle marketer checking 14 variants from Gmail, and a contractor in Webflow comparing old pages can all leave footprints before launch day. If those visits land without utm_source, or if every preview route collapses into /, the problem is in your routing map, not in Meta’s reporting UI.
What I test before spend goes live
For a 12-page B2B launch, I make a CSV with the exact URLs that will appear in Google Ads, LinkedIn Ads, Meta, ConvertKit, and the company’s own newsletter. The file has columns for source, medium, campaign, term, content, expected status, expected final path, and expected environment. A realistic row looks like this: linkedin,cpc,2026-q4-founder-demo,seed-stage,carousel-3,307,/demo,preview.
Then I run the URLs with Playwright or a 20-line Node script using fetch(url, { redirect: "manual" }). The script checks three things. The first response must be 307 or 308 for campaign shortlinks. The location header must keep every UTM key. The final rendered page must load the Vercel Analytics script without a 404. Vercel’s troubleshooting page names that exact failure mode: a missing script.js can happen when tracking code ships before Analytics is enabled, and the fix is enabling Analytics, redeploying, and promoting the deployment to production.
I also test the ugly URLs. A paid search manager will paste utm_campaign=Brand%20US%20Exact from Google Ads. A partner will send utm_source=Newsletter&utm_medium=email&utm_campaign=launch&utm_content=hero CTA with an unencoded space. Someone will capitalize UTM_Source because Marketo exported it that way in 2023. Decide which inputs you accept, normalize them once in proxy.ts, and log the rest.
My rule is simple. Preserve the original values unless they break downstream reporting, then normalize in a named way. For example, I will lower-case utm_source values like LinkedIn, linkedin.com, and LI into linkedin before a redirect reaches /demo. I will not rewrite utm_campaign=q4-founder-demo-a into a prettier label. Campaign names are join keys across Vercel, GA4, HubSpot, Salesforce, and Stripe. Pretty breaks joins.
The same pass should tag preview traffic. Vercel gives every pull request a preview deployment, and that is perfect for QA. Add a query value like qa_preview=1 only in non-production deployments, or add an x-campaign-preview response header that your server logs can filter. In Web Analytics, use the environment dropdown to check Preview first, then Production after the domain is promoted. That one habit catches the embarrassing class of bugs where /go/demo works on acme-git-campaign-fix.vercel.app but misses a production-only host rule on www.acme.com.
Keep sensitive data out of the URL
UTMs are safe enough when they say utm_campaign=2026-q4-founder-demo. They are not safe when a sales ops export sneaks in email=jane@example.com or company=AcmeMedicalTrial. Vercel says Web Analytics stores anonymized data and does not use cookies, and its privacy page lists filtered query params among stored data points. That still leaves you responsible for what your URLs contain.
I block a few parameters at the proxy boundary: email, token, session, customer_id, order_id, phone, and name. If a URL arrives with one of them, the redirect strips it and sets x-campaign-param-redacted=1. In the page layer, Vercel’s advanced analytics docs show a beforeSend hook that can return null or modify an event before it leaves the browser. I use that as the second guard, because a later client-side navigation can introduce a bad query string after the proxy has already done its work.
No drama. Just defense in depth.
The launch-day payoff
The practical win is speed. On a Monday morning launch, the operator can paste 48 URLs into the checker, see 46 green rows, and hand the 2 failures to engineering with exact evidence: /go/q4-webinar returned 308 to /webinar instead of /webinars/q4-growth, and utm_content=carousel-2 vanished on the second hop. That beats a Slack thread with screenshots from three browsers.
Next.js 16’s proxy.ts does not make marketing attribution clean by itself. Vercel’s 2026 resilient intake does not fix sloppy campaign taxonomy. Together, they give a small team a sane preflight system. The redirect map lives where developers can review it. The analytics package collects page views and custom events through version 2 intake. Preview and Production data stay separable in the Vercel dashboard. Before a $15,000 paid test goes live, somebody can prove that the path, UTMs, environment, and analytics script all survived the trip.
That is the standard I want for campaign pages now. If the URL carries budget, it gets audited before launch.
Newsletter
Get growth playbooks in your inbox.
Practical SEO, PPC, automation, and web strategy from the Micromarketing team. No fluff, unsubscribe anytime.