Skip to content
Hazelsong is still being built.Skip to content
Hazelsong

Two lines

Testimonials on React or Next.js

A div and a script appended once, in an effect.

Paste these two lines

Paste these two lines
<script src="https://hazelsong.com/widget/YOUR-SLUG.js?template=wall" async></script>
<div id="hazelsong-wall"></div>

Your slug is in your dashboard, with the tag ready to copy.

Where they go

  1. Render <div id="hazelsong-wall" /> where the testimonials belong.
  2. In a useEffect that runs once, create a script element with the widget's src and append it to the container.
  3. Remove it in the effect's cleanup so a fast refresh does not append it twice.
  4. In Next.js you can use next/script with strategy="afterInteractive" instead, which does the same thing.

Or let your coding agent do it

If an agent edits your site — Cursor, Claude Code, Lovable, Bolt or another — paste this into it. It carries the steps above for React or Next.js, keeps the id the script looks for, and tells the agent to change nothing else, add no other analytics, and show you the diff before anything is published. Replace YOUR-SLUG with your slug, or let the agent ask you for it; your dashboard, under Walls and widgets, has the same prompt with your slug already in it.

Paste this into your coding agent
Add the Hazelsong testimonial widget to this site. Make only the change described here.

What to add: these two lines. Keep the src and the id exactly as written. Where the page cannot hold a raw script tag, the instructions below say how to load the same src.

<script src="https://hazelsong.com/widget/YOUR-SLUG.js?template=wall" async></script>
<div id="hazelsong-wall"></div>

YOUR-SLUG is a placeholder. Before you change anything, ask me for my project's slug — it is in my Hazelsong dashboard, under Walls and widgets — and put it in place of YOUR-SLUG. Do not guess it.

The div with id="hazelsong-wall" is where the testimonials are drawn, so it goes where they should appear on the page. The script finds that element by its id: keep the id exactly as written, and use it once on a page. The script tag can sit next to the div or before </body>.

This site is built with React or Next.js. Hazelsong's instructions for React or Next.js:

1. Render <div id="hazelsong-wall" /> where the testimonials belong.
2. In a useEffect that runs once, create a script element with the widget's src and append it to the container.
3. Remove it in the effect's cleanup so a fast refresh does not append it twice.
4. In Next.js you can use next/script with strategy="afterInteractive" instead, which does the same thing.

Worth knowing: Or skip the widget entirely and read /api/v1/testimonials from your own code — that is what the API is for, and it filters on permission the same way.

Some of these steps may happen in React or Next.js's own editor rather than in files you can change. Do the ones you can, and tell me which ones are left for me to do; do not find another way around them, such as a plugin, a package or a different file.

Rules:
- Do not change anything else: no other files, no formatting, no renaming, no refactoring, no new dependencies.
- Do not add any other analytics, tracking, cookie or consent code.
- Do not change the src, the id or the attributes of the lines above, and do not install a package for Hazelsong: there is none.
- When you are done, show me the diff of every change you made, and wait for me to approve it before you commit, deploy or publish anything.

If it draws nothing

Open the page with ?hazelsong=debug added to its address and look in the browser's console. The widget says there why it drew nothing or counted nothing: no element to draw into, a layout that does not exist, nothing approved that your customers allowed on your website, a page that is not on your list of sites, a browser that says it is automated or is marked as yours, this month's delivery spent, or how a lift test placed that browser. It says this in that browser only and sends nothing anywhere.

If your site sends a Content-Security-Policy header, it has to allow the widget's addresses, or the browser blocks them without drawing anything. Add these to the directives you already have, each beside your own sources:

Content-Security-Policy, the widget's part
script-src https://hazelsong.com; connect-src https://hazelsong.com; img-src https://hazelsong.com https://*.public.blob.vercel-storage.com data:; media-src https://*.public.blob.vercel-storage.com; frame-src https://hazelsong.com https://www.youtube-nocookie.com https://player.vimeo.com https://www.loom.com; style-src 'unsafe-inline'
  • script-src — The widget and the ask dialog are each one script from this address.
  • connect-src — The testimonials are fetched from it, a video's address is asked for when somebody presses play, and the counts are sent to it.
  • img-src — Pictures and video stills are asked for at this address, which sends the browser on to storage; photographs uploaded through a form are served from storage directly. A photograph that arrived through the API with an address of its own is loaded from that address: allow https:, or turn photographs off with photos=0.
  • media-src — A recorded video plays from storage once somebody presses play.
  • frame-src — The ask dialog is a frame of the collection form, and a YouTube, Vimeo or Loom video is that platform's own player.
  • style-src — Each script adds one style element of its own; without this the widget is drawn with no styling.

If your project answers on a domain of your own (Starter and Pro), the two lines can load the widget from it instead: https://testimonials.yourdomain.com/widget/YOUR-SLUG.js. Everything it asks for then goes to that address, which serves your project and no other, and your policy names your domain where it named ours:

Content-Security-Policy, from your own domain
script-src https://testimonials.yourdomain.com; connect-src https://testimonials.yourdomain.com; img-src https://testimonials.yourdomain.com https://*.public.blob.vercel-storage.com data:; media-src https://*.public.blob.vercel-storage.com; frame-src https://testimonials.yourdomain.com https://www.youtube-nocookie.com https://player.vimeo.com https://www.loom.com; style-src 'unsafe-inline'

When the widget cannot draw on one of your pages — no element to draw into, drawn at no size, a layout that does not exist, an error while drawing — it also tells us that page's address, without its query and with anything that looks like an id or an email address cut out, and which of the four it was. Nothing about the reader. Your report's “Is anything broken?” then lists the page with the fix. So that a page reports each thing once a day, that report keeps a note in the visitor's browser, and it waits for the same consent answer as the lift test below: without it, nothing is kept or sent, and ?hazelsong=debug still says what went wrong.

What you already have

Each guide below is one of those places and React or Next.js together: how they come across, and where they go.

Collect one, and put it on React or Next.js

A form link, a customer who writes for a minute, and two lines on your page.