# 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.

```bash
npm install @trackdolphin/sdk
```

Die Collector-URL aus dem Dashboard (**Einrichtung**) steht nur im
Server-Environment — kein `NEXT_PUBLIC_`, der Browser braucht sie nicht:

```bash
# .env.local
TD_ENDPOINT=https://abc123.trdph.com/collect
```

## Die Proxy-Route

Ein Route Handler nimmt Web-Standard `Request` und gibt `Response` zurück —
genau die Schnittstelle des Proxys. `createCollectProxy` liefert den
Handler direkt:

```ts
// 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](/docs/abdeckung.md).

## Provider im Layout

```tsx
// 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;
}
```

```tsx
// 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](/docs/sdk.md#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:

```ts
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.

> **Hinweis:** Das hilft nur bei clientseitigen Umschreibungen. Leitet ein
> **serverseitiger** Redirect (`redirect()` in der Middleware, `next.config`
> `redirects`) auf eine URL ohne Query, ist die gclid schon weg, bevor der
> Browser irgendetwas lädt. Solche Redirects müssen die Query mitnehmen.

## 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:

```ts
// 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 });
}
```

```tsx
// 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](/docs/sdk.md#personen-erkennen-identify)
- `sendBeacon` mit `fetch`-Rückfall, damit ein Klick auf „Kaufen“ das Event
  nicht abreißt

Alles Weitere zur API steht auf der [SDK-Seite](/docs/sdk.md).
