Provedly läuft in einer Next.js-App mit einem einzigen Script-Tag, das einmal aus deinem Root-Layout geladen wird. Das Widget rendert in einem Shadow DOM, fasst also weder dein Tailwind noch deine CSS Modules an, und der Loader ist bei clientseitigen Routenwechseln idempotent. Die einzige Next-spezifische Arbeit: entscheiden, wo das Tag hinkommt, und, wenn du Attribution willst, die Besucher-ID an deinen Checkout weitergeben.
Die Installation in einer Zeile
Leg im Dashboard ein Widget an, kopiere den Token und füge das Script in dein Root-Layout ein. Mit dem App Router ist next/script der saubere Weg.
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://provedly.app/w/widget.js"
data-token="pp_your_token"
strategy="afterInteractive"
/>
</body>
</html>
);
}
afterInteractive lädt das Script nach der Hydration, und das ist der richtige Kompromiss für ein nicht kritisches Element. Nimm nicht beforeInteractive; nichts an einer Kaufbenachrichtigung muss das Laden der Seite blockieren. Das Bundle ist gzip-komprimiert etwa 6 KB groß, und der Loader holt die Widget-Konfiguration von GET /api/w/{token}/config, das die Einstellungen und bis zu 30 aktuelle Einträge zurückgibt.
Nutzt du noch den Pages Router, gehört dasselbe <Script> in pages/_app.tsx. Das Verhalten ist identisch.
Routenwechsel und das SPA-Problem
Benachrichtigungen unterliegen Limits pro Sitzung: standardmäßig 3 Sekunden Startverzögerung, 6 Sekunden Anzeigedauer, 30 Sekunden Abstand und höchstens 5 Benachrichtigungen pro Sitzung. Diese Limits verwaltet das Widget, nicht die Seite. Wer sich durch fünf Produktseiten klickt, bekommt also nicht fünf frische Durchläufe. Das ist Absicht. Eine Benachrichtigung, die bei jeder Navigation neu auftaucht, wird am schnellsten als Spam weggeklickt.
Der Loader prüft vor der Initialisierung, ob schon eine Instanz existiert. Selbst wenn dein Layout das Script-Tag neu mountet, wird nichts doppelt gerendert. Willst du es lieber aus einer Client-Komponente mounten, sieht das dokumentierte Muster so aus:
'use client';
import { useEffect } from 'react';
export function ProofWidget() {
useEffect(() => {
if (document.querySelector('script[data-token="pp_your_token"]'))
return;
const s = document.createElement('script');
s.src = 'https://provedly.app/w/widget.js';
s.dataset.token = 'pp_your_token';
s.async = true;
document.body.appendChild(s);
}, []);
return null;
}
Render <ProofWidget /> genau einmal, im Root-Layout. Das URL-Targeting (alle Seiten, nur diese, alle außer) stellst du im Dashboard ein. Du brauchst also kein bedingtes Rendering in React, um das Widget zum Beispiel von /account fernzuhalten.
Styling: was du steuerst und was nicht
Weil das Widget in einem Shadow DOM lebt, erreicht dein globales CSS es nicht, und sein CSS erreicht deine Seite nicht. Damit fallen die üblichen Kollisionen bei Klassennamen und Überraschungen mit z-index weg. Den Look wählst du stattdessen im Dashboard: eines von fünf Themes (Clean white, Deep ocean, Midnight, Electric violet, Sunset blaze), das Namensformat, das Ortsformat und die Position. Die Details stehen in Shadow DOM: warum das Widget dein CSS nicht kaputt machen kann.
Setzt du eine strikte Content Security Policy ein, erlaube https://provedly.app für script-src und connect-src. Das Widget schickt Anfragen an die Endpoints für Config, Tracking und Präsenz auf diesem Origin und sonst nirgendwohin.
Käufe als „Verifizierter Kauf“ kennzeichnen
Eine Benachrichtigung mit „Verifizierter Kauf“ ist eine, die über einen signierten Webhook angekommen ist. Für eine Next.js-App, die über Stripe Checkout abrechnet, heißt das: https://provedly.app/api/hooks/stripe/{token} als Webhook-Endpoint für checkout.session.completed registrieren und das Signing Secret im Data-Tab des Widgets einfügen. Die komplette Anleitung findest du in Stripe-Webhooks für verifizierte Käufe.
Rechnest du über dein eigenes Backend ab oder über einen Anbieter ohne Provedly-Webhook, schick die Einträge aus einem Route Handler oder einer Server Action:
// app/api/orders/route.ts, after payment is confirmed
await fetch('https://provedly.app/api/v1/widgets/pp_your_token/records', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PROVEDLY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: order.customerName,
city: order.shipping.city,
country: order.shipping.countryCode,
product_name: order.lines[0].title,
price: order.totalFormatted,
external_id: order.id,
visitor: order.provedlyVisitor,
}),
});
So gesendete Einträge werden angezeigt, aber mit „Imported“ gekennzeichnet, nie als verifiziert. Das ist die Ehrlichkeitsregel: Nur ein kryptografisch signiertes Event vom Zahlungsanbieter bekommt das Kennzeichen. Mehr dazu in Die Records-API für Käufe aus deinem eigenen Backend nutzen.
Attribution: die Besucher-ID weitergeben
Das Dashboard zeigt durch Proof unterstützte Bestellungen, also Käufer, die vor dem Kauf eine Benachrichtigung gesehen haben, zugeordnet über die Besucher-ID innerhalb eines 30-Tage-Fensters. Damit die Zuordnung klappt, muss der Kauf die ID mitbringen. Im Client liest du sie mit window.Provedly?.visitorId() aus und schickst sie an deinen Checkout-Code. Bei Stripe übergibst du sie beim Anlegen der Checkout Session als metadata.provedly_visitor. Bei der Records-API sendest du sie im Feld visitor.
| Schritt | Wo | Was übergeben |
|---|---|---|
| ID auslesen | Client-Komponente im Checkout | window.Provedly?.visitorId() |
| Stripe Checkout | Server, beim Anlegen der Session | metadata: { provedly_visitor: id } |
| Records-API | Server, nach der Zahlung | "visitor": id |
Keine ID, keine Attribution. Das Dashboard schätzt nie.
Checkliste vor dem Livegang
- Script im Root-Layout mit
strategy="afterInteractive". - Produktionsdomain bei der Website unter den erlaubten Domains eingetragen; Preview- und Staging-Hosts ausdrücklich hinzufügen, denn
*.vercel.app-Previews passen nicht zu deiner Produktionsdomain. - CSP angepasst, falls du eine hast.
- Webhook registriert (Stripe) oder Records-API angebunden (eigenes Backend).
- Besucher-ID im Checkout weitergegeben.
Häufige Fragen
Funktioniert das Widget mit React Server Components?
Ja. Das Script-Tag ist schlichte HTML-Ausgabe aus dem Layout und braucht keine Client-Grenze. Die optionale ProofWidget-Komponente ist nur deshalb eine Client-Komponente, weil sie useEffect verwendet.
Verursacht die Benachrichtigung Layout-Verschiebungen?
Nein. Das Widget ist fest positioniert in seinem eigenen Shadow Root und belegt keinen Platz im Dokumentfluss. Es trägt also nichts zum CLS bei.
Kann ich es nur auf Marketingseiten zeigen und nicht in der App?
Ja. Nutz das URL-Targeting im Dashboard („all except“ mit /app/* oder „only these“ mit deinen Marketingpfaden) statt bedingtem Rendering.
Starte mit dem Free-Plan, der verifizierte Käufe und 2.000 Impressionen im Monat enthält: Konto erstellen.