Acceptance Spec - Realtor Website → Testimonials tab
- Status: Built 2026-10-29 (John: "build it"), pending independent QA sign-off
- Product: WiseAI Realtor System →
/realtor/app/website(Website builder) + Reviews manager + Aria corpus - Owner surface:
/realtor/app/website?tab=testimonials(realtor Supabase Auth dashboard) - Related:
realtor-website-builder-mvp.md,realtor-reviews-manager-mvp.md§4.5,real-estate-content-editor-mvp(the/business/[token]editor),realtor-video-module-mvp.md(the immediate-publish pattern) - Code:
src/components/realtor/website/TestimonialsTab.tsx,src/app/api/real-estate/website/testimonials/route.ts,src/lib/real-estate/website/server/testimonials-resolve.ts,src/lib/real-estate/server/site-content.ts(caps +cleanMultilineText),src/components/realtor/reviews/ReviewsManagerBody.tsx,src/lib/real-estate/reviews/server/reviews-manager-data.ts, migrationmigrations/20261029_realtor_testimonials_tab_product_knowledge.sql
1. Why (the customer problem)
On 2026-10-08 Sheri Beckett (Team Beckett, a real free case-study customer) asked whether a new Google review loads onto teambeckett.ca automatically, was told no, and replied "Is there a way for me to add the review". The question sat 20 days because there was no honest answer:
- The only working add form (
RealEstateWebsiteEditor→ Testimonials) is mounted in the/business/[token]dashboard, which realtor accounts never see (they log in at/realtor/appvia Supabase Auth). - The realtor Website builder had Hero, Team bios and Videos tabs but no Testimonials tab.
- The Reviews manager's gallery rendered "Add to site" as a
<span>with no button behind it. - Aria's corpus (
product_knowledgecategoryrealtor_operations) told her to curate testimonials "in the Reviews manager", i.e. at the dead label.
A second trap found while building: the server sanitizer capped a quote at
600 chars (60 rows) and collapsed paragraph breaks. Team Beckett's committed
config carries 72 real reviews, 27 longer than 600 chars (longest 3,899) and
24 multi-paragraph. Any editor that round-trips the list through that
sanitizer (the /business/[token] editor does) would have truncated and
reflowed a real customer's reviews on the first save.
2. Scope
In: a Testimonials tab in the realtor Website builder (add / edit / reorder
/ remove, star rating, optional client photo, per-new-row consent tick,
immediate publish); a resolved-list GET so the tab never opens blank; a
contract-guarded PUT; caps widened and paragraph breaks preserved; the
Reviews gallery shows what is on the site and links to the tab; ?tab= and
?add=1 deep links; Aria corpus rows rewritten plus one new row answering the
Google-reviews question.
Out (explicit non-goals): pulling Google reviews automatically (Google
offers no supported path for this; the copy says so), a stored consent record
(the override shape has no field; the tick is a UI gate — follow-up), a
collect-only "unpublished" testimonial store, editing from the public site,
per-testimonial scheduling, the /business/[token] editor's own UI (unchanged
but it now benefits from the wider caps).
3. Data shape (unchanged, re-used)
Testimonials remain a per-tenant override on
local_businesses.metadata.website.{draft,published}.testimonials as
RETestimonialOverride[] (quote, author, location?, rating?,
photo?). The public template merges with WHOLE-ARRAY REPLACE semantics: a
non-empty override replaces config.testimonials; absent/empty falls back to
config. No migration, no new columns.
site-content.ts changes: LIMITS.testimonials 60 → 120,
LIMITS.testimonialQuote 600 → 4000, new exported cleanMultilineText
(keeps \n\n, collapses other whitespace per line, strips tags/control chars)
used for the quote only. LIMITS is now exported.
4. Behaviour
4.1 GET /api/real-estate/website/testimonials?account_id=
- Auth
settings:branding:edit(the Website page's gate, same as hero/videos). - Returns
{ testimonials, source: 'stored' | 'config', limits }wheretestimonialsis the RESOLVED list: the stored override (published, else draft, re-sanitized) when non-empty, otherwise the committed config list in override shape.sourcesays which won. - 503
not_provisionedfor a tenant with no committed RE config.
4.2 PUT /api/real-estate/website/testimonials?account_id= { value }
value: [...]sanitize and save to BOTH draft and published (immediate publish), then revalidate/s/[slug]and/s/[slug]/reviews.value: nullor[]clears the override (site falls back to config).- No
valuekey → 400value_required(never clears — the hero route's 2026-08-19 lesson). Non-array → 400value_invalid. - More than
LIMITS.testimonialsrows → 400too_many(never silently truncate). Any row without a non-blank quote → 400quote_requiredwith the row index (never silently drop). Sanitized count ≠ sent count → 400. - Response
{ ok, testimonials, source }with the resolved list.
4.3 The tab (/realtor/app/website?tab=testimonials)
- Loads the resolved list; shows
n of 120, "the top one shows first", and "these are the testimonials we set up for you" whensource === 'config'. - "+ Add testimonial" PREPENDS a blank row (newest first: the homepage shows
the first six, the Reviews page shows all). Fields: quote (textarea, cap
4000), first name (cap 80), town/role (optional, cap 80), 1–5 stars (click
again to clear), optional photo (upload via the existing
/api/real-estate/website/upload, kindgallery, ≤15 MB image). - Every row: move up / move down / remove.
- A row added in this session shows a consent tick: "The client agreed this can appear on my website (we show a first name only, never a full name or address)". Save refuses until every new row is ticked and names the row. Rows already on the site are not re-asked.
- "Save & publish" is disabled until something changed; on success shows "Live on your site · saved HH:MM" and the rows become non-new.
?tab=testimonials&add=1opens the tab with a blank row ready and focused.- Copy states plainly that Google reviews don't copy over on their own.
4.4 Reviews manager (/realtor/app/reviews)
- The gallery card is now "Testimonials on your website": shows the RESOLVED
list (so a testimonial added from the tab appears here), a count pill, the
first six, "See and edit all" when more, and for curators
(
reviews:testimonial:edit) a real "+ Add a testimonial" link to/realtor/app/website?tab=testimonials&add=1. The dead "Add to site" label is gone.
4.5 Aria / support corpus
acf5e17c…rewritten: "How do I add a testimonial or client review to my website?" → the tab, step by step.3620486f…anda194817c…rewritten to point testimonial management at Website → Testimonials and to say Google reviews do not flow automatically.- New row
7c2e0b64…(realtor_operations): "Do Google reviews load onto my website automatically?" → No, and the steps. - After apply:
validate_product_knowledge()CRITICAL/WARNING must be empty for these rows.
5. Should see / should NOT see
Should see (as Sheri, owner of Team Beckett): Website → Testimonials opens with her 72 existing reviews listed, oldest at the bottom; Add testimonial → paste → first name → tick consent → Save & publish → the review is the first card on teambeckett.ca and on teambeckett.ca/reviews within a page reload; Reviews page gallery shows it with "On website ✓" and the count went up by one; Aria, asked "how do I add a Google review to my site", names the Website → Testimonials tab.
Should NOT see: an empty Testimonials tab for a tenant whose reviews live in config; any of the 72 reviews shortened or reflowed after a save (round-trip test pins this against the real config); a save that silently dropped a row or clipped the list; "Add to site" with nothing behind it; Aria pointing at the Reviews manager for adding; a testimonial publishable without the consent tick on a new row; the Hero/Team/Videos overrides touched by a testimonials save.
6. Verification (evidence required before merge)
- Unit:
src/lib/real-estate/website/server/__tests__/testimonials-resolve.test.ts(resolution rules,cleanMultilineText, ROUND TRIP against the real Beckett config with positive controls) andsrc/app/api/real-estate/website/__tests__/testimonials-put-body-contract.test.ts(PUT contract, Reviews link, tab registration,?tab=validation). Both in.github/workflows/test.yml. site-content-hero.test.tsstill green (shared module).- Independent QA on the Vercel preview: tab renders with the resolved list
(count visible), add → consent gate blocks → tick → save → GET returns the
new row first; Reviews gallery link lands on the tab with a blank row;
PUT without
value→ 400. QA must use a test realtor account, never the Beckett account (customer data). - After merge: apply the corpus migration, run
validate_product_knowledge(), purgechatbot_response_cacheif the support bubble cached the old answer, then ask Aria the question on the real host. - Beckett's own first save is John's call (customer data write): the round-trip test is the evidence that it is lossless.
7. Open
- Stored consent evidence (who ticked, when) is a follow-up; the override shape has no field for it.
- Pulling Google reviews automatically is not possible through a supported Google API for a business profile the realtor owns without Google Business Profile API access and verification; parked, copy says "paste it".
- The
/business/[token]editor still drops quote-less rows silently and does not deep-link here; out of scope.