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.