Next.js (App Router)
Zwei Dinge sind bei Next.js anders als bei einer klassischen Seite: Der
Browser soll nur mit deiner Domain sprechen — dafür gibt es einen Route
Handler, der an den Collector weiterreicht. Und das SDK läuft im Browser;
Server-Komponenten kennen weder document noch location. Deshalb gehört
init() in eine Client-Komponente, die im Layout einmal eingehängt wird, und
Seitenaufrufe kommen aus usePathname, weil ein Wechsel zwischen Routen keine
neue Seite lädt.
npm install @trackdolphin/sdkDie Collector-URL aus dem Dashboard (Einrichtung) steht nur im
Server-Environment — kein NEXT_PUBLIC_, der Browser braucht sie nicht:
# .env.local
TD_ENDPOINT=https://abc123.trdph.com/collectDie Proxy-Route
Ein Route Handler nimmt Web-Standard Request und gibt Response zurück —
genau die Schnittstelle des Proxys. createCollectProxy liefert den
Handler direkt:
// app/td/route.ts
import { createCollectProxy } from "@trackdolphin/sdk/proxy";
export const POST = createCollectProxy({ endpoint: process.env.TD_ENDPOINT! });OPTIONS musst du nicht selbst anlegen — Next.js beantwortet es aus den
vorhandenen Methoden. Die Route ergänzt Besucher-IP und User-Agent, setzt die
Besucherkennung _td_vid als Server-Cookie (das ist der Unterschied zu einem
JS-Cookie, das Safari nach sieben Tagen kappt) und reicht Klick-ID-Cookies
über eine Positivliste weiter. Fehler stehen im Server-Log, nie im Browser.
Warum dieser Weg und nicht ein CNAME: 100 % Abdeckung.
Provider im Layout
// app/trackdolphin-provider.tsx
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { init, track } from "@trackdolphin/sdk";
export function TrackdolphinProvider() {
const pathname = usePathname();
// Einmal starten — der Effekt läuft erst nach der Hydration. Relativ,
// also an die eigene Route; die Besucherkennung vergibt die Route.
useEffect(() => {
init({ endpoint: "/td", requireConsent: true });
}, []);
// Jede Route ist ein Seitenaufruf. Das erste Rendern zählt mit, weil der
// Effekt beim ersten pathname ebenfalls läuft.
useEffect(() => {
track({ type: "custom", custom_name: "page_view" });
}, [pathname]);
return null;
}// app/layout.tsx
import { TrackdolphinProvider } from "./trackdolphin-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<body>
<TrackdolphinProvider />
{children}
</body>
</html>
);
}Die Einwilligung aus dem Cookie-Banner übergibst du mit setConsent() — als
true/false oder granular nach Consent Mode v2.
Mit requireConsent: true bleiben Events bis dahin in der Warteschlange.
Soll das SDK bei einem Ausfall der Route (Deployment, Middleware, die /td
abfängt) nicht schweigen, gib die direkte Collector-URL als Rückfall mit —
dann ist sie allerdings im Client-Bundle:
init({ endpoint: "/td", fallbackEndpoint: process.env.NEXT_PUBLIC_TD_FALLBACK });Warum die gclid sonst verloren geht
init() läuft erst im useEffect, also nach der Hydration. Bis dahin ist
oft schon etwas mit der Adresszeile passiert: ein Locale-Redirect
(/?gclid=… → /de/), ein Trailing-Slash-Redirect, ein router.replace,
das die Query „aufräumt“, oder ein Middleware-Rewrite. Dann ist
location.search leer, und ein SDK, das nur dort nachsieht, hat die gclid
nie gesehen — die Conversion kommt bei Google Ads ohne Klick an.
Das SDK liest deshalb zusätzlich die URL aus dem Navigations-Eintrag der
Performance-API (performance.getEntriesByType("navigation")). Der trägt die
Adresse, mit der die Seite tatsächlich geladen wurde — vor allen
clientseitigen replaceState-Aufrufen. Er wird nur als Rückfall benutzt:
Trägt die aktuelle Adresse selbst Signale, gelten diese; und die
First-Touch-Logik bleibt unverändert, ein späterer direkter Besuch
überschreibt den bezahlten Klick nicht.
Kauf: Browser und Server mit derselben event_id
Der verlässliche Kauf kommt vom Server — aus der Bestellung, nicht aus dem
DataLayer. Damit Meta beide Seiten als eine Conversion erkennt, teilen sich
Browser und Server die event_id. Ableiten, nicht würfeln:
// app/api/checkout/complete/route.ts (Route Handler — oder eine Server Action)
import { createClient } from "@trackdolphin/sdk/server";
const td = createClient({ endpoint: process.env.TD_ENDPOINT! });
export async function POST(req: Request) {
const order = await finishOrder(await req.json()); // zuerst das Geschäft
try {
await td.purchase({
event_id: `order_${order.id}`,
value: order.total,
currency: "EUR",
email: order.email, // wird hier gehasht, verlässt den Server nie im Klartext
phone: order.phone,
gclid: order.gclid, // aus dem Formular mitgeschickt, siehe unten
});
} catch (e) {
console.error("Tracking fehlgeschlagen — Bestellung ist gesichert", e);
}
return Response.json({ orderId: order.id });
}// Danke-Seite (Client-Komponente): dieselbe event_id im Browser
"use client";
import { useEffect } from "react";
import { purchase } from "@trackdolphin/sdk";
export function PurchaseBeacon({ orderId, total }: { orderId: string; total: number }) {
useEffect(() => {
purchase({ event_id: `order_${orderId}`, value: total, currency: "EUR" });
}, [orderId, total]);
return null;
}Die Klick-IDs kennt nur der Browser. readAttribution() liefert die
gesicherte First-Touch-Attribution (gclid, fbclid, utm_*, …) — schick
sie mit dem Checkout-Formular an deinen Server, damit das Server-Event sie
tragen kann. Das Browser-Event bekommt sie automatisch.
Was das SDK von selbst tut
- First-Touch-Attribution über 90 Tage, Klick-IDs aus der Adresszeile (mit dem oben beschriebenen Rückfall)
- Besucherkennung aus dem Server-Cookie der Route lesen — nicht selbst anlegen, sonst gäbe es zwei
- Hashing von E-Mail und Telefon im Browser,
identify()für eingeloggte Nutzer — siehe Personen erkennen sendBeaconmitfetch-Rückfall, damit ein Klick auf „Kaufen“ das Event nicht abreißt
Alles Weitere zur API steht auf der SDK-Seite.