Client (Public Site)
posthog-js on your public Next.js frontend — visitor pageviews, autocapture, and consent gating.
The admin client is scoped to the Payload admin panel: it
reads @payloadcms/ui config and identifies the logged-in admin. That's
correct for /admin, but it can't be reused on your public Next.js
frontend — outside the admin there's no @payloadcms/ui context, and it
would tag anonymous visitors with the admin's distinct id.
For visitor-facing analytics, use the separate payload-posthog/react entry
instead. It has no @payloadcms/ui dependency, reads a public key/host from
props or NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOST, and tracks
anonymous visitors as themselves.
Setup
Drop the provider into your root layout:
import { PostHogProvider } from 'payload-posthog/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PostHogProvider>{children}</PostHogProvider>
</body>
</html>
)
}Then provide the key and host through the environment:
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.comOr pass them as props (apiKey, host), which take precedence over the
environment variables. If no key can be resolved, the provider renders its
children and skips initialization (with a dev-only warning) rather than
throwing.
Pageviews
Pageviews are captured on the initial load and on every App Router
client-side navigation, automatically — the provider initializes posthog-js
with capture_pageview: 'history_change', which patches the History API
(pushState/replaceState) and listens for popstate. No manual
usePathname tracker is required.
Consent gating
For a consent-management platform (c15t, cookie-consent, etc.), start opted
out and flip capturing on once consent is granted. The declarative consent
prop keeps posthog-js in sync — true opts in, false opts out, undefined
leaves the current state untouched (e.g. the visitor hasn't chosen yet):
'use client'
import { PostHogProvider } from 'payload-posthog/react'
import { useCookieConsent } from 'your-cmp'
export function Analytics({ children }: { children: React.ReactNode }) {
const { hasConsent } = useCookieConsent()
return (
<PostHogProvider optOutCapturingByDefault consent={hasConsent}>
{children}
</PostHogProvider>
)
}optOutCapturingByDefault maps to posthog-js's opt_out_capturing_by_default,
so nothing is captured until consent flips it on.
You can also drive consent imperatively, or reach the underlying instance, with the exported hooks:
'use client'
import { usePostHog, usePostHogConsent } from 'payload-posthog/react'
function ConsentBanner() {
const { optIn, optOut } = usePostHogConsent()
return (
<>
<button onClick={optIn}>Accept</button>
<button onClick={optOut}>Reject</button>
</>
)
}
function SignupButton() {
const posthog = usePostHog() // the posthog-js instance, or null before init
return <button onClick={() => posthog?.capture('signup_clicked')}>Sign up</button>
}Props
| Prop | Type | Description |
|---|---|---|
apiKey | string | Project API key. Falls back to NEXT_PUBLIC_POSTHOG_KEY. |
host | string | Ingestion host. Falls back to NEXT_PUBLIC_POSTHOG_HOST, then https://us.i.posthog.com. |
optOutCapturingByDefault | boolean | Start opted out until consent is granted. Default false. |
consent | boolean | Reactive CMP signal; syncs opt-in/opt-out on change. |
options | Partial<PostHogConfig> | Passed verbatim to posthog.init(); overrides every default above. |
PostHogProviderProps is exported from payload-posthog/types.
Using the reverse proxy
If you enabled the reverse proxy, point the provider's host
at the same path so ingestion routes through your own domain instead of
i.posthog.com:
<PostHogProvider host="/ingest">{children}</PostHogProvider>Is it safe to expose the API key?
Yes — the same as the admin client.
PostHog project API keys (phc_...) are write-only and public by design.
personalApiKey is a server-only secret and is never used here.