A simple testimonial widget for Next.js (no npm package, one iframe)
You are building your portfolio or a client site in Next.js and you want real reviews on it. You could build a testimonials table, an admin screen and a moderation flow, or you could drop in one iframe. This guide shows the second option with SimplerFeedback: the plain HTML snippet, a small App Router component, the strip variant for a home page, and what an iframe honestly cannot do.
The idea: your reviews live on a wall you own, your site just frames it
With SimplerFeedback, reviews are collected through your forms and published on your wall, a public page at simplerfeedback.com/wall/your-slug. That page is the source of truth: you approve a review, it appears on the wall, and every site that embeds the wall shows it too.
Your Next.js site does not store anything. It frames the wall in an iframe. The wall is static HTML served from a CDN, so there is no API key to keep secret, no npm package to install and no build step to re-run when a new review comes in. Publish a review in the app and the embed is up to date.
The plain HTML snippet
In the app, open My wall, then Share, and copy the embed code. It is two lines: the iframe, and an optional script that sizes it.
<iframe
src="https://simplerfeedback.com/wall/your-slug?embed=wall"
title="Your name"
style="width:100%;height:900px;border:0"
loading="lazy"
></iframe>
<script src="https://simplerfeedback.com/embed.js" async></script>The 900px height is only a fallback. embed.js listens for a message the wall sends with its real content height (sf-embed-height) and sets the iframe height to match, so the wall never shows an inner scrollbar or a blank gap. Without the script, the iframe simply keeps the height in its style attribute. One script tag is enough for every wall iframe on the page.
A Next.js App Router component
Pasting raw HTML into JSX works, but a small component keeps the slug in one prop and loads the script once. next/script with strategy="lazyOnload" loads embed.js when the browser is idle and deduplicates it, so rendering the component twice on a page does not add the script twice (the script also guards itself).
// components/TestimonialWall.tsx
'use client';
import Script from 'next/script';
type Props = {
slug: string;
title: string;
variant?: 'wall' | 'strip';
featured?: boolean; // strip only, Premium
};
export function TestimonialWall({ slug, title, variant = 'wall', featured = false }: Props) {
const params = new URLSearchParams({ embed: variant });
if (variant === 'strip' && featured) params.set('featured', '1');
const src = `https://simplerfeedback.com/wall/${encodeURIComponent(slug)}?${params}`;
return (
<>
<iframe
src={src}
title={title}
loading="lazy"
style={{ width: '100%', height: variant === 'strip' ? 320 : 900, border: 0 }}
/>
<Script src="https://simplerfeedback.com/embed.js" strategy="lazyOnload" />
</>
);
}Two attributes matter. title gives the iframe an accessible name, which screen readers announce; use something meaningful like "Client reviews", not "iframe". loading="lazy" means a wall below the fold is only fetched when the visitor scrolls near it, so it costs nothing on first paint.
The strip for a home page, and pinned reviews only
A full wall is right for a /testimonials page. On a home page you usually want a short band of proof under the hero. Use ?embed=strip: a horizontal strip of reviews, 320px high by default. On Premium, add featured=1 to the strip to show only the reviews you pinned (up to three), so the home page always opens on your best ones.
// app/page.tsx — a strip of pinned reviews under the hero
import { TestimonialWall } from '@/components/TestimonialWall';
export default function Home() {
return (
<main>
{/* ...hero... */}
<TestimonialWall slug="your-slug" title="What clients say" variant="strip" featured />
</main>
);
}
// app/testimonials/page.tsx — the full wall
// <TestimonialWall slug="your-slug" title="Client reviews" />Styling and layout
Treat the iframe like an image: it fills the width of its container and embed.js handles the height. Put it in whatever wrapper your layout uses (a max-width container, a grid cell) and it adapts.
- Width: 100% of the parent. Constrain the parent, not the iframe.
- Height: set by embed.js from the content; the inline height is only the fallback.
- Colours and dark or light mode come from the wall style, not from your site. Your CSS cannot reach inside a cross-origin iframe, and you should not try.
- Pick a wall style that fits the page (Terminal suits a developer portfolio), or use Build your own on Premium to set the background, accent and text colours to your brand.
The limits, honestly
It is an iframe, with what that implies. The reviews are not server-rendered into your DOM, so search engines do not credit the review text to your page; that text is indexed on the wall URL, which you can also link to directly. There is no React component package and no public API to fetch reviews as JSON: the component above is yours to copy, and the iframe is the integration.
For most portfolios and client sites that is the right trade: two minutes to add, nothing to maintain, and reviews that stay current on their own.