Skip to content

Sample testimonials that never reach production

A landing page template needs quotes, logos and numbers to look finished, and publishing invented ones is illegal. Mark them as samples, show them in development, and drop them from the production build.

Next.js on Vercel2 min readships at docs/solutions/nextjs-vercel/sample-testimonials-without-the-ftc-risk.md

Tags: landing-page · testimonials · ftc · compliance · nextjs

Every SaaS template ships a testimonials section. Most fill it with invented people: "Sarah J., Head of Growth at Acme: this changed everything." The template looks finished, the buyer ships it on Friday, and the fake quote is now a claim their company makes to customers.

Since October 2024 that is not just bad taste. The FTC's rule on consumer reviews and testimonials (16 CFR Part 465) makes it unlawful to publish a testimonial from someone who does not exist or did not say it, with civil penalties per violation. The EU's rules on unfair commercial practices say the same about fake reviews. Invented customer logos and made-up results ("3x faster") are the same problem: an endorsement or a factual claim you cannot back up.

The pattern

Keep the section, and make the build decide what may be published.

// src/lib/site.ts
export interface Proof {
  sample?: boolean;
}

export interface Testimonial extends Proof {
  quote: string;
  name: string;
  role: string;
  company: string;
}

export const SHOW_SAMPLES = process.env.NODE_ENV !== "production";

export function publishable<T extends Proof>(
  items: readonly T[],
  showSamples: boolean = SHOW_SAMPLES,
): T[] {
  return items.filter((item) => showSamples || item.sample !== true);
}

Template content carries sample: true. The section filters its list with publishable(), renders a small "Sample" badge on every sample it shows, and returns null when nothing is left:

export function Testimonials({ testimonials }: { testimonials: SiteConfig["testimonials"] }) {
  const quotes = publishable(testimonials.items);
  if (quotes.length === 0) return null;
  // ...each card renders {item.sample ? <SampleBadge /> : null}
}

What this buys you:

  • In next dev the page looks complete, and every invented entry is labelled.
  • next build inlines process.env.NODE_ENV as "production", so samples are dropped at build time. There is no runtime flag to forget.
  • A real entry is the same object without the flag. Adding the first real quote brings the section back in production with no code change.
  • The same helper covers logos, stats and social profile links.

Test it

Make the production behaviour a unit test, so a later "make the page look fuller" change cannot quietly publish samples:

it("never publishes a sample in production", () => {
  for (const list of [site.testimonials.items, site.logos.items]) {
    expect(publishable(list, false).some((item) => item.sample)).toBe(false);
  }
});

What counts as real

A real testimonial is a customer's own words, quoted accurately, from someone who agreed to be quoted. If you paid for it or gave something in return, say so next to it. A real logo is a customer who agreed to be named. A real number is one you can show the data for. When in doubt, leave the sample flag on and let the section stay hidden.