跳到正文
DEV Community· Level Up Digital Marketing Group·· 3 小時前AI 評分56

如何在 Next.js 擷取 gclid 並從伺服器傳送轉換事件,追蹤潛在客戶來源

Stop Guessing Which Ad Produced the Lead: Capture gclid in Next.js and Send the Conversion Server-Side

AI 導讀

這篇教學示範如何在 Next.js 首次到站時保存 gclid 等點擊識別碼及 UTM 參數,提交表單時寫入潛在客戶資料,並由伺服器傳送 GA4 轉換事件。Middleware 以有效期 90 日的 Cookie 保存參數,表單處理程序再讀取資料;文章亦以 _ga Cookie 還原 GA4 client ID,讓事件沿用原工作階段來源。

正文

A marketing team asks you a simple question: which campaign produced the twelve leads that came in last week? You open the database. Every lead has a name, an email, a message, and a timestamp. Nothing about where the person came from. The ad platform says it drove forty clicks. Analytics says eight form submissions. Nobody can connect a single row in your leads table to a single ad.

This is fixable in an afternoon, and the fix lives in your codebase, not in a marketing tool. The plan: persist the click identifiers and UTM parameters the moment a visitor lands, attach them to the lead when the form posts, and send the conversion event from the server so it survives ad blockers and abandoned tabs.

Why client-side tracking alone misses leads

The usual setup fires a conversion from a gtag call on the thank you page. It works until it does not. Content blockers strip the tag. The visitor closes the tab before the redirect finishes. The form posts to an API route and the page never changes. Each of those cases is a real lead the ad platform never hears about, which means the bidding algorithm learns from an incomplete picture.

Server-side events solve the delivery problem. The attribution problem is separate: you still need to know which click the lead belongs to. Google Ads appends a gclid parameter to the landing URL on every click, and Meta, Microsoft and others do the same with their own identifiers. Capture it on first touch, keep it for the length of the buying cycle, and write it to the lead. The same approach covers referrals from AI assistants, which matters now that GA4 separates AI traffic into its own channel: a lead that arrived from a chat answer should say so in your database, not only in a report.

Step one: persist attribution in middleware

Next.js middleware runs on every request before rendering, which makes it the right place to read query parameters and set cookies without touching page components.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export const ATTR_KEYS = [
  'gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid',
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
]

export function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const params = req.nextUrl.searchParams

  for (const key of ATTR_KEYS) {
    const value = params.get(key)
    if (!value) continue
    res.cookies.set(`attr_${key}`, value.slice(0, 200), {
      maxAge: 60 * 60 * 24 * 90,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
      path: '/',
    })
  }

  if (params.toString().length > 0 && !req.cookies.get('attr_landing')) {
    res.cookies.set('attr_landing', req.nextUrl.pathname, {
      maxAge: 60 * 60 * 24 * 90,
      sameSite: 'lax',
      path: '/',
    })
  }

  return res
}

export const config = {
  matcher: ['/((?!_next|api|.*\\..*).*)'],
}

Two decisions are baked in here. Ninety days covers most service business buying cycles, where someone clicks an ad in March and books in May. Last touch wins for the click identifiers, because that is how the ad platforms attribute by default and you want your numbers to match theirs. If you need first touch as well, store it under a second cookie prefix and skip the write when it already exists, the same way attr_landing works above.

If your site has to honor a consent banner, gate the cookies.set calls behind the consent cookie. The attribution still works for everyone who accepts, and the code path stays the same.

Step two: attach attribution when the lead posts

The form posts to a route handler. Read the cookies there, write them to the lead row, and you have a permanent record of the source for every lead, independent of whatever the analytics tool decides to keep.

// app/api/lead/route.ts
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { ATTR_KEYS } from '@/middleware'
import { db } from '@/lib/db'

function gaClientId(raw?: string) {
  // _ga looks like GA1.1.1234567890.1700000000
  const parts = raw?.split('.') ?? []
  return parts.length >= 4 ? `${parts[2]}.${parts[3]}` : undefined
}

export async function POST(req: Request) {
  const body = await req.json()
  const jar = await cookies()

  const attribution = Object.fromEntries(
    ATTR_KEYS.map((k) => [k, jar.get(`attr_${k}`)?.value ?? null]),
  )
  const clientId = gaClientId(jar.get('_ga')?.value) ?? crypto.randomUUID()

  const lead = await db.lead.create({
    data: {
      name: body.name,
      email: body.email,
      message: body.message,
      landingPath: jar.get('attr_landing')?.value ?? null,
      gaClientId: clientId,
      ...attribution,
    },
  })

  await sendServerEvent(clientId, lead.id, attribution)

  return NextResponse.json({ ok: true, id: lead.id })
}

The _ga cookie is the piece most people skip. Its last two segments are the GA4 client ID for that browser. Sending the server event with the same client ID lets GA4 stitch the lead to the session that produced it, so the event inherits the session's source, medium and campaign. Without it, the event still records, but as a brand new user with no origin.

Step three: send the conversion from the server

GA4's Measurement Protocol accepts events over plain HTTPS. Create an API secret under the data stream settings, store it with the measurement ID in environment variables, and post the event.

// lib/ga.ts
export async function sendServerEvent(
  clientId: string,
  leadId: string,
  attribution: Record<string, string | null>,
) {
  const endpoint = new URL('https://www.google-analytics.com/mp/collect')
  endpoint.searchParams.set('measurement_id', process.env.GA_MEASUREMENT_ID!)
  endpoint.searchParams.set('api_secret', process.env.GA_API_SECRET!)

  const res = await fetch(endpoint, {
    method: 'POST',
    body: JSON.stringify({
      client_id: clientId,
      events: [
        {
          name: 'generate_lead',
          params: {
            lead_id: leadId,
            lead_source:
              attribution.utm_source ??
              (attribution.gclid || attribution.gbraid || attribution.wbraid
                ? 'google_ads'
                : 'direct'),
            has_click_id: Boolean(attribution.gclid || attribution.fbclid || attribution.msclkid),
          },
        },
      ],
    }),
  })

  if (!res.ok) console.error('GA4 MP rejected event', res.status)
}

While you are building, swap /mp/collect for /debug/mp/collect. The debug endpoint returns validation messages instead of a silent 204, which saves a lot of guessing about field names.

Then do the two things that make the event useful to the people buying the ads. In GA4, mark generate_lead as a key event. In Google Ads, with the GA4 property linked, import that key event as a conversion. From that point the ad platform optimizes against leads that actually reached your database, not against page loads.

What this gives the business

Every lead row now answers the question the marketing team asked at the start, and it answers it from your own database. You can group leads by campaign in SQL, compare cost per lead across channels with numbers you trust, and hand the sales team the context of how each person found you.

When we manage paid search for a service business, this attribution plumbing is the first thing we check, because a bidding algorithm fed by half the conversions will happily spend the whole budget on the wrong half.

The next step, once the data is clean, is routing: getting the lead to the right person within minutes instead of letting it sit in a table. That is a workflow problem more than a code problem, and lead routing is where most marketing stacks bottleneck. But none of it works until the lead knows where it came from. Start there.

Level Up Digital Marketing Group builds Next.js sites and marketing systems for service businesses in San Diego. More at levelupdigitalmarketinggroup.com.

來源:DEV Community · dev.to