Luuphub developers View as Markdown

Luuphub widget install & API

The widget is one <script> snippet. It defines a global command function, injects a small loader from a stable URL, and mounts the core on demand.

Install

Paste this just before </head> (on Next.js, use the element under Next.js instead), swapping lh_site_YOUR_KEY for your site's publishable key (safe to expose — it only permits submitting feedback):

<script>!function(w,d){w.luuphub=w.luuphub||function(){(w.luuphub.q=w.luuphub.q||[]).push(arguments)};var s=d.createElement('script');s.async=1;s.src="https://w.luuphub.com/loader.js";s.setAttribute('data-lh-site',"lh_site_YOUR_KEY");d.head.appendChild(s)}(window,document);</script>

Next.js

Do not paste the snippet into a Next.js app. Load the widget the way the Next.js scripts guide loads any script every route needs: import Script from 'next/script' in your root layout (app/layout.tsx or src/app/layout.tsx, and each root layout if route groups give you several) and render this inside <body>, right after {children}:

<Script
  src="https://w.luuphub.com/loader.js"
  data-lh-site="lh_site_YOUR_KEY"
  strategy="afterInteractive"
/>

The scripts guide's own example puts it just after </body>; either spot works, because Next.js creates the real tag in the browser. On the Pages Router, render the same element in pages/_app after <Component {...pageProps} />. Keep the layout a Server Component: the element needs no 'use client' and no onLoad. Next.js forwards data-* attributes to the real script tag and adds it to <body> after hydration, so the served HTML proves nothing either way (the key can appear in Next's inline data before any script has run): check the live page in a browser, not with curl. With a nonce-based policy, pass nonce to the element as the Next.js CSP guide shows. The element has no queue stub, so window.luuphub exists only once the widget has loaded: call it as window.luuphub?.('open').

Content-Security-Policy

If your site sends a Content-Security-Policy, extend it and change nothing else. For each directive: if the policy has it, append these sources; if it lacks it but has default-src, add it with default-src's sources plus these; if it has neither, add nothing, because that type is not restricted.

Directive Add Why
script-src https://w.luuphub.com The loader and the widget code it loads.
connect-src https://luuphub.com https://w.luuphub.com https://vercel.com Settings, the install check and feedback; the loader's asset manifest; recordings too large for the API upload, which go straight to Vercel Blob.
img-src blob: data: Screenshot and annotation previews.
media-src blob: Playing a recording back before it is sent.

The widget needs no style-src, font-src or frame-src change: it styles itself through constructable stylesheets and uses system fonts. If script-src uses a nonce, put the page's nonce on the tag you add, the way the project already does; under 'strict-dynamic' that nonce, not the host, is what lets the loader in. If your policy forbids inline scripts and uses no nonce, load the loader as a plain tag instead of the snippet: <script async src="https://w.luuphub.com/loader.js" data-lh-site="lh_site_YOUR_KEY"></script>.

How it works

The snippet defines window.luuphub as a queue stub — the classic analytics-style pattern. Any command you call before the core finishes loading is pushed onto window.luuphub.q and replayed once the core mounts, so a call on a cold page is never dropped. The injected loader reads its own tag's attributes:

Commands

Call the global with a command name and optional argument:

// Open the widget panel (e.g. from your own "Feedback" button).
luuphub('open');

// Close it.
luuphub('close');

// Attach a signed identity (see the Identify & SSO recipe).
luuphub('identify', { userId: 'u_123', email: 'a@b.com', auth: signedToken });

// Clear identity on logout.
luuphub('reset');
Command Argument Effect
open — Opens the feedback panel.
close — Closes the panel.
identify { userId?, email?, auth } Associates submissions with a signed user.
reset — Forgets the current identity (call on logout).

Single-page apps

The widget tracks client-side navigation automatically (history + a lh:navigation event), so route changes in a SPA are captured with the correct page context — no manual re-init.

Voice and video

Visitors can leave a voice note, or a video of their camera or (on desktop) their screen, from the same box they type in. Nothing extra to install.

Recording Longest Largest file
Voice note 3 minutes 8 MiB
Video (camera or screen) 2 minutes 24 MiB

Next