How to build a testimonial wall
How to build a testimonial wall with a collect form, an approval inbox, and a pasteable embed. Follow the prompts, check each step, then skip video.
You can learn how to build a testimonial wall as a collect form, an approval inbox, and a snippet you paste into the site. Senja is that product. The catalog line is “Nice things customers said, arranged in a grid.”
You pay $19–$49 a month. That is $228–$588 a year. The verdict is “An easy ego boost”. The first estimate is 4 hours: collection form, approval inbox, embed wall.
There is no standalone teardown yet. The Typeform post already splits a public form from the table you keep. This guide is the walk. Prompts in order, then what to look at.
The audience is a founder collecting praise for one product. If you need video capture, a widget studio, or a wall for every brand you run, keep paying. The same v1 also replaces Testimonial.to and Famewall.
A one-question intake is Typeform, $29–$99 a month, about 2 hours. I walked that as How to build a Typeform alternative. A document of blocks that lands in a table is Tally, $29–$89 a month, about 8 hours. That walkthrough is How to build a Tally alternative. The page you paste the snippet into can be a Carrd v1, $2–$4 a month, about 2 hours.
How to build a testimonial wall without video capture
The Senja page splits the work into three rows.
The first row is the one you want. A public form with name, role, avatar, rating, and text. An inbox with approve, reject, and feature. A cached embed of the quotes you said yes to. That is the 4-hour v1.
The second row is 2 days. Layouts, themes, drag-to-reorder, paste-in quotes from email. Do that after three real customers have already said yes.
The third row is 2 weeks and up. Video capture, social auto-imports, a widget studio. Skip it.
If you tell an agent to “clone Senja”, it aims at the third row. Pin every prompt to the first one.
v1 does this. Flavio shares https://love.flaviocopes.com/handbook. Ada Rossi writes a quote, ticks consent, and submits. The row sits in pending. Flavio approves it. The marketing site loads a snippet that shows Ada and nobody else. A later reject takes her off the next cache refresh. Video, Twitter imports, and a marketplace stay off the page.
The parts and where a request goes
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The capability line for Senja is Astro, D1, and R2. This site is an Astro app on Cloudflare, so that is the path I default to. Use TanStack Start if React is your weekday language. Pick one stack and stay on it.
The stack post is which tab you copy. The prompt post is how you pin the first estimate. The scope post is why the widget studio waits.
Open Build your own Senja. The guardrail is the useful sentence: publish a testimonial only after explicit approval, and record the submitter’s consent at submission time. The non-goal sits next to it. No video testimonials, multi-product workspaces, automated social imports, or a testimonials marketplace.
Submit, approve, and reject are ordinary form posts. The email starts after the pending row is stored. If mail is down, Ada still sees the thank-you and Flavio still has the inbox. The embed is a separate cached endpoint. If the plan invents a Twitter crawler or a nightly video job, send it back.
The data the wall carries
The card lists CollectionForm, Testimonial, and Wall. Tonight you need one form, many quotes, and one wall.
The form has a slug, handbook, a welcome line, and a thank-you. Ada never edits it.
A testimonial belongs to that form. It carries display name, role, company, avatar, rating, plain text, a consent flag, and a status. Status is pending, approved, or rejected. Featured is a flag on an approved row. Consent is recorded on the submit request. You cannot add it later from the inbox.
The wall points at approved, visible quotes. v1 order is approval time, featured first. Drag handles wait for the 2-day row.
Avatars live in R2 under keys you issued. The stored URL is yours, not Ada’s original upload path, and never a javascript: string she typed.
Every owner query is scoped to Flavio. A guessed testimonial id from a private window returns nothing useful: no quote, no email, no 404 that confirms the id. The public form and the embed are the only anonymous surfaces.
The screens you ship
Three surfaces. Keep them boxes and labels. If the agent starts matching Senja’s live UI, send it back. Themes and drag-to-reorder wait for the 2-day row, so a layout picker is not a screen tonight.
The collect form is what Ada sees. Welcome line. Name, role, company. Avatar file. Rating. The quote. A consent checkbox that must be on. Submit. A thank-you that does not mention pending or spam. A honeypot field she never notices.
The inbox is Flavio’s product. Pending, approved, and rejected tabs. Each row shows the quote, Ada’s name and role, the rating, and the avatar. Approve, reject, and feature sit on the pending and approved rows. A missing-consent row cannot be approved. That should never exist if submit did its job.
The embed page is a preview plus two snippets. An iframe pointing at a minimal render URL. A script that fetches the same public JSON. Sizing is width and a short revalidation window. The preview must match what a stranger’s site will paint.
Collect, approve, and embed
The first journey on the card is this walk. Share the form. Ada submits with consent. You approve. You paste the snippet.
A waitlist with a confirm link is a different job. That walkthrough is How to build a waitlist. The page that hosts the snippet can be the Carrd guide.
Prompt 1. Scaffold Bragwall and stop
Copy the Astro prompt from the product page. Paste it into your coding tool. Put this block above it.
Ship the 4-hour row only.
Collection form, approval inbox,
embed wall.
Project name: Bragwall.
Public URL is https://love.flaviocopes.com.
Owner is Flavio.
Seed form slug: handbook.
Welcome: What did the handbook
change for you?
Thank-you: Thanks. I will pick
a few for the site.
Do not add layouts, themes,
drag-to-reorder, or quote imports.
Do not add video capture, social
auto-imports, a widget studio,
or a testimonials marketplace.
When the plan is done, stop.
Do not write code until I say go.
The official prompt already asks for a short technical plan first: routes, tables, authorization, jobs. Let it produce that plan. Read it.
You want /handbook for the public form, /inbox for the three tabs, /embed for the preview and snippets, plus a read-only render URL the iframe hits. Tables for the form, testimonials, and the wall. R2 for avatars. No videos, no imports, no themes.
If the plan includes a Twitter crawler, Stripe, or a recorder, send it back. When the plan is narrow, say go.
What to check after the scaffold
Start the app. Sign in as Flavio. You should see an empty pending tab and a link to /handbook.
Open the migrations. Testimonials store status and a consent timestamp. There is no path from pending to the embed without an approve write. Avatars are an R2 key, not a free-text URL column the form accepts as-is.
If the README is a placeholder, stop. It has to name the consent rule, the honeypot, the cache window, and the five public embed fields.
Prompt 2. Take a quote and keep it pending
The collect form is the intake. The first mini-prompt on the card about hostile text sits next to it. So does the honeypot one.
Build the public collect form
for Bragwall at /handbook.
Ada sees welcome, name, role,
company, avatar file, rating
1-5, quote text, and a required
consent checkbox. Submit stores
one pending testimonial with
consent recorded now.
No consent, no row.
Strip all markup on the way in.
Name, role, company, and quote
are plain text. Decode entities,
drop tags, cap length, reject
control characters.
Avatar uploads go to R2 after
you check size and decoded
image type. Reject javascript:
and anything that is not
http or https if she pastes
a URL. Serve the file from
your origin later.
Honeypot field, hidden with
CSS. A filled honeypot returns
the same thank-you and writes
nothing. Rate-limit by IP.
Reject submits faster than a
person can type, using a signed
timestamp from the form render.
Do not publish from this page.
Do not send Ada a login.
Do not add video.
Done when Ada can submit the
Rome JS quote, the inbox shows
one pending row with consent,
and a script-tag paste renders
as inert text in the inbox.
What to check after a quote lands
Open /handbook in a private window. Leave consent off. Submit stays on the form. No inbox row.
Tick consent. Send Ada’s quote with a <script> in the text and a javascript:alert(1) avatar. Thank-you. Inbox shows the quote as readable words. No script runs. No javascript: URL is stored.
Fill the honeypot in a second private window. Same thank-you. Pending count is still one.
Hammer send. One pending row, or a polite try-later. The wall preview is still empty.
If mail is pointed at a dead adapter, Ada still got the thank-you and the row is in D1. The inbox is the record. Mail can fail.
Prompt 3. Approve into the wall
The inbox is the gate. Approval is the only write that makes a quote public.
Build the Bragwall inbox at
/inbox.
Tabs: pending, approved,
rejected. Each row shows name,
role, company, rating, quote,
and avatar. Actions: approve,
reject, feature.
Approve is allowed only when
consent was recorded at submit.
Rejected quotes leave the
embed at the next cache
refresh. Feature pins an
approved quote above the
others without a drag list.
Login required. A guessed
row id from a signed-out
window returns nothing
useful. No submitter email
on this page tonight.
Do not add drag-to-reorder.
Do not add a CSV.
Do not import from email.
Done when Ada moves from
pending to approved, Luca
is rejected, the embed
preview shows Ada only,
and rejecting Ada afterwards
clears her from that preview
after refresh.
What to check after you approve Ada
Sign in. Pending shows Ada and Luca. Approve Ada. Feature her. Reject Luca.
Open /embed. Ada is there. Luca is not.
Reject Ada. Refresh the preview, or wait the cache window the README named. Ada is gone. The layout does not leave a hole that still says her name.
Open /inbox with no cookie. Login wall. Guess /inbox/ada-id from that window. Empty response, no quote text.
Prompt 4. Paste a cached embed
The first mini-prompt on the card is the widget. This is what a visitor sees.
Build the embed for Bragwall.
Two snippets on /embed: an
iframe to a minimal render
URL, and a script that
injects a box and fetches
JSON. Same public fields in
both: display name, role,
avatar URL, rating, text.
The embed endpoint is
read-only and cache-friendly.
Short revalidation window.
No management routes on
that surface. No emails,
no consent flags, no
submission ids.
Approved and visible only.
Featured first, then
approval time. A reject
drops the quote on the
next refresh without
breaking the grid.
Script styles stay inside
a namespace. No globals.
Empty state if the fetch
fails. CORS and
frame-ancestors headers
that allow a host page
you do not control.
Do not add theme pickers.
Do not add video players.
Done when I paste the
iframe into a local HTML
file on another origin,
see Ada, reject her in
the inbox, refresh after
the cache window, and she
is gone. The JSON has
those five fields only.
What to check after the snippet works
Copy the iframe into a file you open as a different origin, or a Carrd page. Ada renders. View source on the embed response. Five fields. No email.
Reject Ada. Wait the window. Reload the host page. She is gone.
Break the JSON URL on purpose. The script shows an empty box, not a stack trace.
If the agent served /inbox from the same cacheable host with public CORS, send prompt 4 back.
Failures the agent ships anyway
A quote that goes live on submit
Pending is the product. If /handbook writes approved, you skipped the gate. You notice when the host page shows Ada before you opened the inbox.
Markup that survives into the host page
The embed runs on a site you do not control. Name, role, company, and quote are all hostile. You notice when a test <img> or an entity-encoded tag becomes a real node in the iframe. Strip on ingest. Escape on render.
A hotlinked avatar
A URL Ada typed can be a tracker or a javascript: link. You notice when the embed HTML contains her original host. Re-host through R2.
An inbox full of bots
The honeypot that returns a different status code trains the bot. You notice when pending jumps by dozens and every row looks generated. Same thank-you. No row. Rate limit the rest.
A video recorder on the 4-hour row
The third estimate is 2 weeks and up for video capture, social auto-imports, and a widget studio. If videos or an OAuth crawl appears in the plan, you left the first estimate.
Deploy the wall
Bragwall wants Astro, D1, and R2. Put it on a subdomain you already own, love.flaviocopes.com. D1 holds the rows. R2 holds avatars. There is no cron on the 4-hour row. Mail is the only background job.
The hosting post is that path: the adapter, then the bindings, then the custom domain.
Attach the domain on the Worker. Open /handbook on your phone on cellular. Submit a quote. Approve it on the laptop. Paste the iframe into a page that is not this origin. Confirm Ada shows, then reject her and wait the cache window.
Take a D1 backup and an R2 listing before you delete the paid account. The approved rows are the history you will miss.
Use it as the real wall for a month. If you miss drag-to-reorder or a pasted email quote, those are the 2-day row. Then cancel Senja, or Testimonial.to, whichever one you pay for.
What to skip on purpose
Leave out video testimonials, multi-product workspaces, automated social imports, and a testimonials marketplace.
Layouts, themes, manual ordering, and quote imports wait until three approved quotes already sit on a real host page. The fourth mini-prompt on the card lives there. Hide must restore the same slot. A new approve appends. Feature pins without destroying that order.
The first-project post still points at a tonight card. This is the afternoon after that, when you still rent a grid to show five sentences.
A form you search as a table is the Typeform guide. A document of blocks with a signed webhook is the Tally guide. This app publishes only the rows you click.
More writeups land on the guides index. The audit is the catalog if you want a smaller first ship.
Start now
Go to Build your own Senja. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only: collection form, approval inbox, embed wall. Four hours.
Send the handbook link to one person who already said something kind.