Next.js Integration
Datsnip works with all Next.js rendering modes: static generation, server-side rendering, and client-side rendering.
Option A — Client-side (simplest)
Add the Datsnip snippet to _app.tsx or app/layout.tsx. Use the next/script component with strategy="afterInteractive".
Pages Router (_app.tsx)
// pages/_app.tsx
import Script from 'next/script'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
{/* Anti-flicker — must be before the SDK */}
<style dangerouslySetInnerHTML={{ __html:
'html{visibility:hidden;pointer-events:none}'
}} id="_gs_af" />
<Script
id="gs-af-timeout"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html:
`setTimeout(function(){var e=document.getElementById('_gs_af');if(e)e.remove()},3000)`
}}
/>
<Script id="datsnip-sdk" strategy="afterInteractive">
{`(function(w,d,k){w._sf={siteId:k,ingestUrl:'https://api.datsnip.com'};var s=d.createElement('script');s.async=1;s.src='https://api.datsnip.com/sdk/tracker.js';d.head.appendChild(s)})(window,document,'${process.env.NEXT_PUBLIC_GS_SITE_ID}');`}
</Script>
<Component {...pageProps} />
</>
)
}
App Router (app/layout.tsx)
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<style id="_gs_af"
dangerouslySetInnerHTML={{ __html:
'html{visibility:hidden;pointer-events:none}'
}}
/>
<Script id="gs-af-timeout" strategy="beforeInteractive">
{`setTimeout(function(){var e=document.getElementById('_gs_af');if(e)e.remove()},3000)`}
</Script>
</head>
<body>
<Script id="datsnip-sdk" strategy="afterInteractive">
{`(function(w,d,k){w._sf={siteId:k,ingestUrl:'https://api.datsnip.com'};var s=d.createElement('script');s.async=1;s.src='https://api.datsnip.com/sdk/tracker.js';d.head.appendChild(s)})(window,document,'${process.env.NEXT_PUBLIC_GS_SITE_ID}');`}
</Script>
{children}
</body>
</html>
)
}
Add to .env.local:
NEXT_PUBLIC_GS_SITE_ID=sf_your_site_id
Option B — Server-side (zero flicker)
Get variant assignments in getServerSideProps before the page renders. The variant content is in the initial HTML — no JavaScript required on the client for the experiment to work.
// pages/pricing.tsx
import type { GetServerSideProps } from 'next'
interface Props {
heroText: string
}
export const getServerSideProps: GetServerSideProps<Props> = async ({ req, res }) => {
// Persist visitor ID in cookie
let visitorId = req.cookies['_sf_vid']
if (!visitorId) {
visitorId = `vis_${Math.random().toString(36).slice(2, 12)}`
res.setHeader('Set-Cookie',
`_sf_vid=${visitorId}; Max-Age=31536000; Path=/; SameSite=Lax`)
}
// Fetch variant assignment
try {
const resp = await fetch(
`${process.env.GS_API_URL}/v1/sites/${process.env.GS_SITE_ID}/experiments/assign` +
`?visitorId=${visitorId}&url=/pricing`
)
const { assignments } = await resp.json()
const heroText = assignments[0]?.changes
?.find((c: any) => c.changeType === 'text' && c.selector === 'h1')
?.changeValue ?? 'Start your free trial'
return { props: { heroText } }
} catch {
return { props: { heroText: 'Start your free trial' } }
}
}
export default function PricingPage({ heroText }: Props) {
return (
<main>
<h1>{heroText}</h1>
</main>
)
}
Option C — Middleware (apply to all routes)
For redirect experiments or global experiment assignments:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const visitorId = request.cookies.get('_sf_vid')?.value
?? `vis_${crypto.randomUUID().replace(/-/g, '').slice(0, 20)}`
const resp = await fetch(
`${process.env.GS_API_URL}/v1/sites/${process.env.GS_SITE_ID}/experiments/assign` +
`?visitorId=${visitorId}&url=${encodeURIComponent(request.nextUrl.pathname)}`
)
const { assignments } = await resp.json()
const redirect = assignments
.flatMap((a: any) => a.changes)
.find((c: any) => c.changeType === 'redirect')
if (redirect) {
return NextResponse.redirect(new URL(redirect.changeValue, request.url))
}
const response = NextResponse.next()
if (!request.cookies.get('_sf_vid')) {
response.cookies.set('_sf_vid', visitorId, { maxAge: 60 * 60 * 24 * 365 })
}
return response
}
Environment variables
# .env.local
NEXT_PUBLIC_GS_SITE_ID=sf_your_site_id # for client-side snippet
GS_SITE_ID=sf_your_site_id # for server-side API calls
GS_API_URL=https://api.datsnip.com # or your self-hosted URL