WiseAI Realtor — Pro Website "Focused" Homepage Layout (preview)
Source: Terry Beckett (real paying customer, wiseaiagency.com/s/terry-and-sheri and teambeckett.ca) UX review, 2026-09-10 —
C:\dev\BECKETT_UX_REVIEW_2026-09-10\RECOMMENDATIONS.mditems 11-13 (STRUCTURAL tier) +mockup/homepage-before-after.md. Those three items were explicitly flagged as founder + Terry decisions, not unilateral changes because they affect every tenant's homepage. This spec builds a clickable PREVIEW so Terry can scroll his own site in the proposed structure before any decision is made — no tenant's site changes as a result of this PR by itself.
Design — merge-safe, opt-in, previewable on one URL
RESiteConfig.homepageLayout?: 'classic' | 'focused'(site-config.ts). Absent/undefined →'classic'. No tenant sets this field — every existing site (Beckett included) keeps rendering'classic'until a human changes it.resolveHomepageLayout(config, layoutParam)— a?layout=focusedor?layout=classicquery override wins over the config value, which wins over the'classic'default. Presentational only (same underlying data), so it is safe to try on any host, including the real Beckett site, without writing anything.- Threaded through three render paths, each reading
?layout=server-side and passing the resolved value down as an explicit prop (never re-derived client-side fromuseSearchParams, so SSR and the client agree):/s/[slug]→RealEstateSiteTemplate,/s/[slug]/listing/[mls]→ListingDetailView.
Item 11 — "Homes on the market now" → "Featured listings" row
Classic (unchanged): FeaturedListingsSection keeps the full Zoocasa-style
filter bar + inline whole-board search + Grid/Map toggle + pagination, exactly
as it renders today.
Focused: the same section id (id="listings", so the "Buy > Listings" nav
item keeps working) instead renders a "Featured listings" heading and a
horizontal scroll-snap carousel (see Item 16's ScrollSnapCarousel
primitive) containing ALL of the tenant's own active listings (revised
2026-09-11, founder: "for the featured listings, can we make that a carousel
with all Beckett listings" — supersedes an earlier same-day 3-card-cap
iteration), plus a single "See all homes →" button/link to /s/[slug]/search
(the existing full-board search page, untouched) beneath it. filteredListings
already IS the tenant's own-listings set here with no new query: filters
stays empty in 'focused' (no filter UI in this layout), and config.listings
is already own-only upstream whenever the tenant has any
(capListingsForHomepage in listing-ownership.ts returns the owned set
immediately once non-empty, no pool padding — its own 2026-08-21 revision
comment). An empty state (no listings at all) offers the same /search link
rather than showing nothing.
Measured (Playwright, live preview, 2026-09-11) — a clear, large win at both widths (unlike Communities, where a full-width grid already fit the row): the classic search widget stacks all 11 own listings (filters + grid); the carousel keeps one row regardless of count.
| Width | Classic height | Focused height | Delta |
|---|---|---|---|
| 1400px (desktop) | 2740px | 989px | -1752px (-64%) |
| 400px (mobile) | 6460px | 965px | -5496px (-85%) |
Verified on the live preview: exactly 11 cards in the carousel (Team Beckett's real own-listing count), first card visible and a later card genuinely off-screen at 1400px (proves it scrolls, isn't just a wall), and the mobile row actually scrolls on swipe/programmatic scroll.
Item 12 — Merge the credibility band; drop three sections from the scroll
Merged band: a new CredibilityBandSection composes the same
AnimatedStats count-up strip (StatsBand's content) and the same trust-badge
strip (BadgesSection's content, still hidden entirely when a tenant has no
badges) into ONE section, replacing the previous two adjacent sections.
Classic keeps StatsBand and BadgesSection as two separate, byte-identical
sections.
Dropped from the homepage scroll (focused only): ComingSoonSection,
VideoSection, SoldStoriesSection. Per RECOMMENDATIONS.md, "everything that
moves stays fully reachable from the navigation — nothing is deleted or
hidden":
- Sold and Coming soon were already linked from the shared page
footer (
footerNavLinks→/sold,/coming-soon) on every page, regardless of layout — no change needed there. - Videos had no dedicated route before this PR. Added
/s/[slug]/videos(new, mirrors/sold//coming-soon's pattern: resolves the same site config,force-dynamic, noindex) rendering the SAMEVideoSectionthe homepage uses, wrapped inVideoGallerywith an honest empty state (VideoSection itself renders nothing when a tenant has no parseable videos, which is fine mid-homepage but not on a dedicated page a nav link points at).footerNavLinksgained a "Videos" entry, gated on the tenant actually having videos configured (same pattern as the existinghasBloggate) — appears on every layout/tenant that has videos, not only 'focused'. - Revised same day (founder feedback, "too many links in header"): the
first pass added Sold/Coming soon/Videos as three MORE top-level desktop
header links, making 'focused' fuller than classic — the opposite of the
de-overwhelm goal. Fixed:
SiteNavnow accepts an optionaldesktopNavLinksprop used ONLY for the desktop row (xl:flex); the mobile menu always renders the fullnavLinksunchanged, and classic passes nothing so its desktop row stays byte-identical.'focused'desktop now shows 5 primary items — Buy, Sell, Resources, Reviews, Contact — plus one "More" dropdown collecting About, Blog, Communities, Sold, Coming soon, Videos, Calculators, and Partners (the last two reach the top nav for the first time; previously footer-only). "More" reuses the existingNavDropdowncomponent (same aria-haspopup/aria-expanded/ Escape/outside-click-close Buy/Sell already use) — no new a11y code.
Explicitly NOT done in this PR (optional further merges RECOMMENDATIONS.md names but does not require): Team + Specialties merge, Resources + Blog teaser merge. Terry can ask for those separately if he wants the deeper cut.
Item 13 — Listing-detail photo gate: one banner, not a grid of padlocks
Classic (unchanged): the gated grid of individually blurred thumbnails
with a 🔒 overlay on each locked photo, FREE_PHOTOS = 3 free.
Focused: when a listing is gated (pool/feed listing the visitor doesn't
own, not yet unlocked, not signed in — same gated boolean, same
FREE_PHOTOS constant, same openGate('photos') target), the thumbnail strip
shows the first FREE_PHOTOS photos normally (clickable, unblurred) followed
by ONE banner: "{N} more photo(s) — sign up free to see them →", where N = gallery.length - FREE_PHOTOS is computed from the real gallery, never
hardcoded. Clicking the banner opens the identical signup gate modal as
clicking a locked thumbnail does today. An ungated listing (the tenant's own,
or already unlocked) renders identically in both layouts.
Item 14 — Team bio "Read more" (founder feedback 2026-09-11)
Founder, on the TeamSection card grid: "we should cap characters and have a
read more to save some screen real estate." Classic (unchanged): full bio
text, no control. Focused: a bio over TEAM_BIO_TRUNCATE_LENGTH (220
chars) renders a word-boundary-safe preview (reusing the SAME
truncateQuote() pure function TestimonialQuote.tsx uses for review quotes,
at a shorter cap) plus a real <button aria-expanded> "Read more"/"Show
less" (re-foc focus-visible ring, matches the site's existing text-button
style — see GuidesFilterChips's "Clear filters"). The remainder is hidden
with the native hidden attribute, not removed — full text stays in the DOM
for SEO/screen-readers/the AI corpus, same rule
testimonial-truncation.contract.test.ts already enforces for quotes. A
short bio (under the cap) renders identically in both layouts — no control at
all. Unlike TestimonialQuote (deliberately server-only, native
<details>/<summary>, no client boundary), TeamSection already lives
inside the always-'use client' RealEstateSiteTemplate, so a real stateful
button costs no new client boundary here.
Item 15 — Homepage testimonials cap at 3 (founder feedback 2026-09-11)
Founder: "showing 3 reviews is enough because that main opening with all the
adjectives gives the proof and people can click read more reviews if
needed." Classic (unchanged): all 6 of the existing .slice(0, 6)
cards visible. Focused: only the first 3 of that SAME selection are
visible; cards 4-6 stay in the underlying .slice(0, 6) map (hide, don't
slice — no change to the 6-card selection itself) and get display:none +
aria-hidden, the exact idiom ReviewsLoadMore.tsx already established for
/reviews' pagination. The ReviewsSummary word-cloud/adjectives opening
above the cards is unchanged in both layouts. "Read Reviews →" (to
/reviews, which still renders every testimonial in full) is unchanged.
Item 16 — Communities carousel (founder feedback 2026-09-11)
Founder: "Communities section could be a carousel too if that saves some
space." Focused renders the same community cards through
ScrollSnapCarousel.tsx (new, generic over item type) — native CSS
scroll-snap (snap-x snap-mandatory overflow-x-auto), no carousel library —
originally mirroring VideoSection.tsx's existing 'scroll' layout, then
extracted into this ONE shared primitive so Featured listings (Item 11)
could reuse it rather than duplicating the carousel logic a second time
(founder, same day: "reuse the same scroll-snap row you're building for
Communities so there's one carousel primitive, not two"). ~1.3 cards peek on
a phone, ~3.2 at desktop. Prev/next are real <button>s with
aria-labels, rendered ONLY when the row is actually wider than its
viewport (scrollWidth > clientWidth, re-measured on resize) — not assumed.
Every community stays in the map in both layouts. Classic keeps the
exact original grid.
Measured honestly (Playwright, live preview, 2026-09-11) — the founder asked for real numbers, not an assumed win:
| Width | Classic height | Focused height | Delta |
|---|---|---|---|
| 1400px (desktop) | 788px | 791px | ~0 (focused very slightly TALLER, noise) |
| 400px (mobile) | 1682px | 1037px | -645px (-38%) |
Team Beckett has exactly 3 communities, and the classic grid is already
lg:grid-cols-3 — 3 cards already fit in ONE row at desktop width, so the
carousel provides no measurable space savings at 1400px for this tenant
today. It IS a clear, large win on mobile (grid stacks 3 cards vertically;
the carousel shows one scrollable row). The carousel still generalizes: a
tenant with more than the per-breakpoint card count (4+ communities at
desktop, 3+ at tablet) would see the same desktop benefit mobile shows here.
Shipped as asked, with this caveat reported rather than overstating the
desktop benefit.
Item 17 — Header finder → chat, not a search box (founder decision 2026-09-11)
Founder: no separate search box — "encourage people to make use of the
robust chatbot to help them find anything on the website." SiteFinder.tsx
(new) renders a compact, visually quiet, search-box-shaped control in
focused only: desktop a small inline input, mobile a search icon that
reveals the same input. It is explicitly NOT a fourth "talk to us"
mechanism (Phase 2's item 7 already consolidated those) — its only
destination is the EXISTING embedded chat (ChatWidgetStream), never the
voice TalkLauncher.
Investigated ChatWidgetStream's open-chat contract (showing-cta.ts):
it already dispatches a race-free cwa:open-chat CustomEvent (window-buffered
so it survives being fired before the widget mounts), used today by the
per-listing "Book a showing" CTA — but that path only ever PREFILLS the
composer (spec §5.1: a suggested message the visitor didn't write must be
reviewed before it sends). Added the minimal hook requested: an autoSend
flag on ShowingOpenEventDetail (widened showing to optional so the
finder needs no fake listing context) plus a few lines in
ChatWidgetStream's existing applyOpenDetail handler — no restructuring.
Two new helpers: requestOpenChatWithMessage() (finder submit — the
visitor's own typed-and-submitted text sends immediately, the same consent
as pressing Enter in a normal search box) and requestOpenChatPlain() (the
one "Can't find it? Ask our AI assistant" encouragement line near
guides/resources — opens chat with nothing prefilled, visitor types their
own first message; exactly ONE instance, site-wide).
No customer-facing copy anywhere hardcodes "Aria" — that is the team's
internal nickname (used only in code comments/docs, confirmed by search);
every existing CTA already says "our AI assistant" (AiBandSection, the
phone CTAs), so the finder and the encouragement line reuse that same
established, name-free phrasing.
VERIFIED — the actual promise, not just the control (live preview,
2026-09-11): typed "where are your closing cost guides?" into the header
finder and submitted. The assistant's real reply (captured from the live
chat DOM) reads: "Great question! We have a Guides & resources page
with buying and selling guides, including how-the-process-works articles —
that's where you'll find detailed breakdowns on closing costs and what to
expect. We also have Calculators for land transfer tax and other
costs..." — with "Guides & resources" and "Calculators" rendered as real
<a target="_blank"> links to /s/terry-and-sheri/guides and
/s/terry-and-sheri/tools respectively, confirmed via innerHTML inspection
scoped to the chat messages pane (not a footer/nav link coincidentally
matching /guides). The underlying capability (chat can send a visitor to a
real page with a link) is site-pages.ts + chat-knowledge.ts
(buildRealEstateSiteKnowledgeBlock), merged to main via PR #1480
("Aria can send someone to a page — she just had no addresses"). The
fix/aria-site-navigation branch/worktree (C:\dev\wt-arianav, tip
3c13dc66b, dated 2026-08-24) is STALE and superseded by that merge — its
own diff against main shows it missing a LATER listing-cap fix, confirming
it predates PR #1480 rather than improving on it. Not merged; left
untouched.
Guardrails (binding)
- No tenant's
homepageLayoutis set — this PR changes ZERO live rendering for any existing site. Verified by the classic-layout contract tests below. - No guide, review, listing, sold home, coming-soon home, or video is removed from the site — only where it sits in the homepage scroll changes, per RECOMMENDATIONS.md's "Explicitly NOT recommended" list.
- The same first-3-free photo-gate rule, the same gate/unlock logic, and the same signup CTA target apply in both layouts — only the locked-photo PRESENTATION differs.
- Real customer data (Team Beckett,
…-a000-000000000c01) is read-only throughout verification: no form submits, no calls, no sign-ups, no writes. - Items 7-10 (Phase 2,
acceptance/realtor-template-deoverwhelm-phase2.md) and this PR both touchRealEstateSiteTemplate.tsxand several of the same section files independently; this PR is expected to be REBASED onto Phase 2 after Phase 2 merges, not the reverse — thelayout === 'focused' ? … : …branches here are kept minimal and clearly delimited for that rebase.
Verification
- Unit/contract:
focused-layout.contract.test.ts(new) pins the classic section order byte-identical, confirms the three sections are gated onresolvedLayout, confirms the featured row slices to exactly 3 real listings via the sharedsearchHref(not a bare, filter-discarding template literal — seecarry-filters-into-search.contract.test.ts), and confirms the photo-gate banner count is derived, not hardcoded.carry-filters-into-search.contract.test.tsandsite-pages.contract.test.tsreconciled and green (not deleted). A second describe block (added with the header revision) asserts the focused desktop row has exactly 5 primary items, that Sold/Coming soon/Videos are NOT top-level (only inside "More"), and that the mobile menu still maps the full, uncollapsednavLinks. - Playwright against the Vercel PREVIEW for the branch's HEAD sha (title-gated,
not status-gated), at 1400px and 400px:
/s/terry-and-sheri?layout=focused— featured row shows 3 cards, the merged credibility band appears once, Coming Soon/Video/Sold Stories are absent from the scroll, and the new nav links to/sold,/coming-soon,/videosall resolve 200./s/terry-and-sheri?layout=classicand/s/terry-and-sheri(no param) — today's layout, unchanged.- One listing detail page with
?layout=focusedshows the single banner with the correctN. ?layout=classicon the same listing shows the existing locked-grid behavior, unchanged.
- Real-host follow-up (not required before Terry reviews the preview, but
needed before any go-live):
wiseaiagency.com/s/terry-and-sheriis host-aliased — the Vercel preview host skips the hostname-rewrite middleware even though the template code is identical.
Item 18 — Carousel autoplay, WCAG 2.2.2 compliant (founder feedback 2026-09-11)
Founder: "for carousel, can it move slowly automatically." Autoplay lives
entirely in the shared ScrollSnapCarousel.tsx primitive (Item 16), so
both Featured listings (Item 11) and Communities (Item 16) inherit it with
no caller changes — one implementation, not two, consistent with the
founder's earlier "one carousel primitive" instruction.
Behavior: advances one card every 5.5s with a smooth scroll; wraps to the start after the last card. WCAG 2.2.2 (pause/stop/hide), all four conditions enforced together:
- Never starts under
prefers-reduced-motion: reduce— read live inside the effect (never cached in component state, to avoid an SSR/hydration mismatch between server and client render output). The row is static; prev/next arrows still work. - Pauses (temporarily) on hover, on focus-within (keyboard users get the
same courtesy), on touch/pointer-down, and while off-screen
(
IntersectionObserver) — resumes automatically once the condition clears. - Stops PERMANENTLY for the rest of that page view after a manual
swipe/drag/wheel or a prev/next arrow click — distinguished from the
component's own programmatic scroll via an
isAutoScrollingRefguard set immediately before, and cleared immediately after, every autoplay/nudge scroll. - A small, always-visible pause/play
<button>(aria-pressedreflecting "currently paused") is the only way to resume after a permanent stop, or to pause pre-emptively. - The upcoming card's image is eager-loaded (
img.loadingflipped from'lazy'to'eager') immediately before each advance, so lazy-loading can never leave a blank card mid-transition.
Gated on measured overflow, not just item count (bug caught by live
Playwright verification, fixed same day): the first version gated the
interval and the pause button on items.length > 1 alone. On the deployed
preview, Team Beckett's Communities carousel (3 communities, lg:grid-cols-3
equivalent — see Item 16's own honest measurement, "no measurable space
savings at 1400px for this tenant") showed a working-looking pause/play
button on desktop that did nothing, because 3 cards already fill the row
with no overflow to advance through. Both the autoplay interval effect and
showPauseButton now also require scrollable (the same measured
scrollWidth > clientWidth flag the prev/next arrows already used) — a row
that shows everything gets neither an interval nor a control for one.
Measured/verified (Playwright, live preview, 2026-09-11), both widths:
| Check | Featured listings (1400px) | Featured listings (400px) | Communities (1400px) | Communities (400px) |
|---|---|---|---|---|
| Has overflow to advance through | yes | yes | no (3 communities fit the row) | yes |
| Advances within 8s | ✅ | ✅ | n/a — no autoplay, no button (correct) | ✅ |
Static under prefers-reduced-motion: reduce | ✅ | ✅ | n/a | ✅ |
| Pauses on hover | ✅ | ✅ | n/a | ✅ |
| Stops permanently after manual swipe | ✅ | ✅ | n/a | ✅ |
| Pause/play button toggles (verified via explicit blur + cursor-away, not a same-position click that keeps the hover-hold engaged) | ✅ | ✅ | not rendered (correct) | ✅ |
Contract coverage: 11 new tests in focused-layout.contract.test.ts
("ScrollSnapCarousel autoplay") covering interval timing, wrap-to-start,
the reduced-motion gate inside the effect, all four temporary-pause
conditions, permanent-stop on scroll and on nudge, the visible pause
control, image preloading, and two regression tests pinning the
scrollable-gate fix (button presence stays SSR-safe; both the interval
and the button require overflow, not just item count).