Add a two-question feedback card to a Next.js settings page, rendered as native React controls. It records a rating and a note with the signed-in account attached, and you verify one real response end to end.
The card renders in your page's DOM, not in an iframe. Why iframe form embeds break covers that trade-off.
Install the React renderer
pnpm add @usefillo/reactImport the optional default stylesheet from the root layout:
// app/layout.tsx
import "@usefillo/react/styles.css";Create a publishable key in the Fillo project, allow your app's origin, and add the key to .env.local:
NEXT_PUBLIC_FILLO_KEY=pk_your_publishable_keyThe browser needs this key only because the form below is defined in code. Keep fsk_ project keys on the server.
Add the feedback card
Create a Client Component. Keep the Fillo client outside the component.
// app/settings/FeedbackCard.tsx
"use client";
import { createClient, Fillo } from "@usefillo/react";
const publishableKey = process.env.NEXT_PUBLIC_FILLO_KEY;
const fillo = publishableKey ? createClient({ key: publishableKey }) : null;
type Respondent = { id: string; email?: string; name?: string; hash?: string };
export function FeedbackCard({ respondent }: { respondent?: Respondent }) {
if (!fillo) {
return <p role="status">Add NEXT_PUBLIC_FILLO_KEY to render feedback.</p>;
}
return (
<section aria-labelledby="feedback-title">
<h2 id="feedback-title">How did this settings page work for you?</h2>
<Fillo.Form
client={fillo}
id="nextjs-settings-feedback"
title="Settings feedback"
respondent={respondent}
>
<Fillo.Rating id="score" label="Overall experience" max={5} required />
<Fillo.LongText id="note" label="What should we fix?" />
</Fillo.Form>
</section>
);
}The field IDs are the durable contract. You can change labels later, and score and note keep naming the response columns, exports, and webhook keys.
In a claimed workspace, the first render stages the form as a draft if the project allows publishable-key sync. Publish it before users see the card. Later schema edits stage again.
Attach account context
Compute the respondent hash on the server with the project's identity secret. Sign the exact profile values you pass to the renderer. Use trimmed values within the documented bounds, and omit empty optional values:
// lib/fillo-identity.ts (server only)
import { createHmac, hkdfSync } from "node:crypto";
export function filloRespondentHash(user: { id: string; email?: string; name?: string }): string {
const key = Buffer.from(
hkdfSync("sha256", process.env.FILLO_IDENTITY_SECRET!, "fillo-identity-v2", "respondent-profile", 32),
);
return createHmac("sha256", key)
.update(JSON.stringify(["fillo-identity-v2", user.id, user.email ?? null, user.name ?? null, []]))
.digest("hex");
}Pass the result from the Server Component that renders the route:
// app/settings/page.tsx
import { FeedbackCard } from "./FeedbackCard";
import { filloRespondentHash } from "@/lib/fillo-identity";
import { getCurrentUser } from "@/lib/auth"; // your session helper
export default async function SettingsPage() {
const user = await getCurrentUser();
return (
<FeedbackCard
respondent={{ id: user.id, email: user.email, name: user.name, hash: filloRespondentHash(user) }}
/>
);
}Never give the identity secret a NEXT_PUBLIC_ name. Legacy ID-only hashes
retain account identity but discard unsigned profile fields. See
respondents and identity for verified-only submissions.
Why the hash has to come from the server
Page code can claim any user ID. An unsigned ID is context, not proof. Once the project holds an identity secret, Fillo records a respondent only when the hash matches, so the secret must never reach the browser.
Place the card in your product
Put the card where the question makes sense, such as after an import, beside a new feature, or at the end of a task.
| Your page owns | Fillo owns |
|---|---|
| Route, session, and layout | Schema and field IDs |
| A visible heading when the form title is hidden | Validation and error messages |
| What happens after submit | The accepted response and its form version |
| Product analytics events | Delivery to webhooks and integrations |
Match your design system with theme tokens, slot classes, or custom UI.
Other placements
The same card works at the end of a docs article or help page. Add a hidden path field and pass the current route, so each answer carries the page it's about:
// app/docs/ArticleFeedback.tsx
"use client";
import { usePathname } from "next/navigation";
import { createClient, Fillo } from "@usefillo/react";
const fillo = createClient({ key: process.env.NEXT_PUBLIC_FILLO_KEY! });
export function ArticleFeedback() {
const pathname = usePathname();
return (
<section aria-labelledby="article-feedback-title">
<h2 id="article-feedback-title">Was this page helpful?</h2>
<Fillo.Form
key={pathname} // a fresh form for each page
client={fillo}
id="article-feedback"
initialData={{ path: pathname }}
showTitle={false}
>
<Fillo.Hidden id="path" label="Page path" />
<Fillo.Rating id="helpful" label="How useful was this page?" max={5} required />
<Fillo.LongText id="note" label="What was missing?" />
</Fillo.Form>
</section>
);
}A visitor can edit a hidden value, so treat path as context, not proof.
Review one real response
- Publish the form.
- Load the signed-in route from an allowed origin.
- Submit without a rating and confirm focus moves to the error.
- Submit one response.
- Find the rating, note, and respondent in the project's Responses view.
- Confirm any webhook or integration received it.
The callback proves the request returned. The saved response proves the workflow continues after the page closes.
Start from the starter
Clone the Next.js product-feedback starter. It includes this App Router page, an environment example, and a clean-install checklist. For a dashboard-managed version, open the customer feedback template.
Related
- Custom layout example: See native fields inside a product-owned layout.
- Add a form to Next.js: Render a published form by ID.
- Add an in-app NPS or CSAT survey: Measure satisfaction with a standard score.
- In-app feedback overview: Compare structured feedback with product analytics.