GetPureProof

How to add video testimonials to Next.js (App Router & Pages)

By , Founder5 min read

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.

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:

  • lazyOnload is 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.
  • afterInteractive is fine if testimonials are above the fold and you want them slightly earlier.
  • beforeInteractive is 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:

  1. Render the widget div in the server component.
  2. Render the <Script> in a small client component sibling.
  3. 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 lazyOnload defers 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-height to prevent layout shift.
  • TBT / INP. The embed script is lightweight and runs cross-iframe, so it doesn't meaningfully block the main thread. lazyOnload further 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:

  1. Test in a production-like build: next build && next start. The dev server's behavior with next/script differs subtly from production.
  2. 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).
  3. Run Lighthouse in production mode to confirm the widget isn't dragging Core Web Vitals.
  4. 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