Embed
Feedback widget for Next.js
Put a VoteFirst widget inside a Next.js app, loading the script with next/script and mounting the widget from a client component. The React page covers the component underneath this, for an app that is not on Next.js.
App Router
The widget call touches window and the element it draws into, so it has to run in a client component. next/script loads the file itself; the component below reaches for window.vfq rather than an onLoad callback, because the queue runs a call whether the script has already arrived or not.
'use client';
import { useEffect, useRef } from 'react';
import Script from 'next/script';
export default function VoteFirstRoadmap({ project, embedKey }: { project: string; embedKey: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let handle: { destroy: () => void } | undefined;
let cancelled = false;
(window as any).vfq = (window as any).vfq || [];
(window as any).vfq.push((VoteFirst: any) => {
if (cancelled) return;
VoteFirst.init({ project, key: embedKey });
handle = VoteFirst.roadmap(ref.current);
});
return () => {
cancelled = true;
handle?.destroy();
};
}, [project, embedKey]);
return (
<>
<Script src="https://app.votefirst.app/embed/votefirst.js" strategy="afterInteractive" />
<div ref={ref} />
</>
);
}
Render it from a server component or a page exactly as any other client component:
<VoteFirstRoadmap project="your-project-slug" embedKey="your-embed-key" />
As on the React page, swap VoteFirst.roadmap for any other call on the widgets page to draw the board, the changelog, the Most Wanted list, a vote button, a badge, or either popup.
Pages Router
The same component works unchanged. The one difference is where next/script with strategy="afterInteractive" is allowed to sit: in the App Router it can go inside the component itself, while in the Pages Router it has to be rendered from a page or from _app.tsx rather than from _document.tsx, which runs before the page is interactive.
What goes wrong
| Symptom | Cause and fix |
|---|---|
origin_not_allowed, a 403 | The site's origin is missing from the embed key's allowlist, set under Share & Embed in the dashboard. Add both http://localhost:3000 for development and the deployed origin, as the quickstart describes. |
| The widget mounts twice in development | Next.js renders with React Strict Mode by default, which runs an effect, tears it down, and runs it again. The first run usually queues its call before the script has arrived, so its cleanup has nothing to destroy yet; the cancelled flag makes that queued call return without drawing, and only the second run's widget appears. |
window is not defined during the build | A component that reads window outside useEffect, or that is missing 'use client', runs during server rendering, where there is no window. Keep every read of window or document inside the effect, as above. |
Next
Plans start at $9 a month after a 14 day trial. Start one, see the prices or open the demo.