How to add video testimonials to Next.js (App Router & Pages)
You're a Next.js developer. You don't need a generic embed tutorial — you need the right component pattern, the right script-loading strategy, and the gotchas specific to App Router and React's hydration model. This guide gives you all three.
Three approaches covered: a server-rendered iframe (simplest, zero client-side work), a script-loaded widget using next/script (recommended for production), and a reusable React component that wraps both behind a typed API.
Setup
You need:
- A Next.js project. App Router and Pages Router both supported, with notes for each.
- A GetPureProof account with at least one approved testimonial, and either a widget ID or a Space slug.
- Your dashboard's embed snippet copied to clipboard.
If you haven't set up the collection side yet, see how GetPureProof works for the basic flow. From here on, we assume you have a widget ID or Space slug.
Approach 1: Server-rendered iframe (simplest)
Drop this into any server component or page:
export default function TestimonialsSection() {
return (
<iframe
src="https://getpureproof.com/embed/YOUR_SPACE_SLUG"
width="100%"
height="800"
frameBorder="0"
scrolling="yes"
style={{ border: 'none', borderRadius: '8px' }}
title="Customer Testimonials"
/>
)
}
Pros:
- Zero client-side JavaScript on your end.
- Renders identically on server and client (no hydration mismatch).
- Works in Server Components, Client Components, App Router, Pages Router — anywhere.
Cons:
- Fixed height. If your Space grows, the iframe doesn't auto-resize unless you wire up a resize observer.
- You can't swap between layout styles (Wall of Love, Carousel, Spotlight, Floating Pop, and others) from the dashboard without republishing — the iframe locks you into one rendering.
Use this when you want a quick Wall-of-Love drop-in and don't need fine-grained widget control.
Approach 2: Script + div using next/script (recommended)
This gives you the full widget feature set with auto-resizing handled by the embed script.
For App Router, in any client component or page:
"use client"
import Script from "next/script"
export default function TestimonialsSection() {
return (
<>
<div
className="pureproof-widget"
data-widget-id="YOUR_WIDGET_ID"
/>
<Script
src="https://getpureproof.com/embed.js"
strategy="lazyOnload"
/>
</>
)
}
For the Pages Router, the same code works — next/script is supported in both.
Notes on strategy choice:
lazyOnloadis the right default for testimonials. The widget isn't critical to the initial paint, so loading after the page is interactive keeps your LCP and TBT clean.afterInteractiveis fine if testimonials are above the fold and you want them slightly earlier.beforeInteractiveis wrong — you don't want to block the page on a testimonial widget.
The embed script loads once per page even if you have multiple <div className="pureproof-widget"> elements. Next.js's Script component deduplicates by src.
Approach 3: Reusable typed component
For projects where testimonials appear on multiple pages, wrap both approaches behind a single component:
"use client"
import Script from "next/script"
interface TestimonialWidgetProps {
widgetId?: string
spaceSlug?: string
height?: number
className?: string
}
export function TestimonialWidget({
widgetId,
spaceSlug,
height = 800,
className,
}: TestimonialWidgetProps) {
if (spaceSlug) {
return (
<iframe
src={`https://getpureproof.com/embed/${spaceSlug}`}
width="100%"
height={height}
frameBorder="0"
scrolling="yes"
style={{ border: 'none', borderRadius: '8px' }}
title="Customer Testimonials"
className={className}
/>
)
}
if (widgetId) {
return (
<>
<div
className={`pureproof-widget ${className ?? ''}`}
data-widget-id={widgetId}
/>
<Script
src="https://getpureproof.com/embed.js"
strategy="lazyOnload"
/>
</>
)
}
return null
}
Usage:
<TestimonialWidget widgetId="abc-123" />
<TestimonialWidget spaceSlug="my-space" height={600} />
Drop into any page, server-rendered or client-rendered.
Server Components vs Client Components
A note on the App Router. The script-loaded widget (Approach 2) requires "use client" because next/script is a client-side primitive. The iframe approach (Approach 1) does not — iframes work in server components.
If you want to use the script approach inside a server component, the workaround is:
- Render the widget div in the server component.
- Render the
<Script>in a small client component sibling. - The embed script will pick up the div regardless of which component rendered it.
Practically, just mark the section that contains your testimonials as a client component. Negligible bundle impact.
TypeScript considerations
The widget div takes a data-widget-id attribute, which React's typings handle generically — this works without additional typing:
<div data-widget-id="abc-123" /> // OK
If you want typed widget IDs (recommended for projects with multiple widgets), define them as a const map:
const widgetIds = {
homepage: 'abc-123',
pricing: 'def-456',
features: 'ghi-789',
} as const
type WidgetKey = keyof typeof widgetIds
Then accept a WidgetKey in your component instead of a raw string.
Performance considerations
Core Web Vitals matter for SEO. How each approach interacts with the metrics:
- LCP. The iframe loads with the iframe render — fast. The script approach with
lazyOnloaddefers until after the page is interactive, which keeps LCP unaffected. - CLS. Both approaches render with a fixed initial height (iframe) or have the script set iframe dimensions on init (widget script). Either way, give the embed container a fixed
min-heightto prevent layout shift. - TBT / INP. The embed script is lightweight and runs cross-iframe, so it doesn't meaningfully block the main thread.
lazyOnloadfurther isolates it from interactivity-blocking concerns. - JS bundle size. The embed script loads from an external origin, not bundled with your Next.js app. Zero impact on your
_next/static/chunks/size.
For the broader story on widget performance, see how to embed video testimonials without killing PageSpeed.
Going live
Before deploying:
- Test in a production-like build:
next build && next start. The dev server's behavior withnext/scriptdiffers subtly from production. - Check both server-rendered and client-side-navigated paths. Make sure the widget renders on a fresh page load (full SSR) and when navigating from another page (client-side routing).
- Run Lighthouse in production mode to confirm the widget isn't dragging Core Web Vitals.
- Verify only approved testimonials appear. The widget pulls only items with Approved status from your dashboard.
Closing thought
Next.js plus this widget is the cleanest integration on this list — the framework's primitives map directly to what the embed needs. Use the iframe for the simple case, use the script approach for production widgets, wrap both in a typed component for repeat use, and ship.
Ship it before your next deploy
Spin up a Space, record a test testimonial, paste the snippet into your Next.js app. Free forever on Starter — $49/month for unlimited videos when you're ready to scale.
Create free account