Skip to main content

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.md items 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=focused or ?layout=classic query 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 from useSearchParams, so SSR and the client agree): /s/[slug]RealEstateSiteTemplate, /s/[slug]/listing/[mls]ListingDetailView.

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.

WidthClassic heightFocused heightDelta
1400px (desktop)2740px989px-1752px (-64%)
400px (mobile)6460px965px-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 SAME VideoSection the homepage uses, wrapped in VideoGallery with 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). footerNavLinks gained a "Videos" entry, gated on the tenant actually having videos configured (same pattern as the existing hasBlog gate) — 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: SiteNav now accepts an optional desktopNavLinks prop used ONLY for the desktop row (xl:flex); the mobile menu always renders the full navLinks unchanged, 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 existing NavDropdown component (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.

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:

WidthClassic heightFocused heightDelta
1400px (desktop)788px791px~0 (focused very slightly TALLER, noise)
400px (mobile)1682px1037px-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 homepageLayout is 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 touch RealEstateSiteTemplate.tsx and several of the same section files independently; this PR is expected to be REBASED onto Phase 2 after Phase 2 merges, not the reverse — the layout === '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 on resolvedLayout, confirms the featured row slices to exactly 3 real listings via the shared searchHref (not a bare, filter-discarding template literal — see carry-filters-into-search.contract.test.ts), and confirms the photo-gate banner count is derived, not hardcoded. carry-filters-into-search.contract.test.ts and site-pages.contract.test.ts reconciled 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, uncollapsed navLinks.
  • Playwright against the Vercel PREVIEW for the branch's HEAD sha (title-gated, not status-gated), at 1400px and 400px:
    1. /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, /videos all resolve 200.
    2. /s/terry-and-sheri?layout=classic and /s/terry-and-sheri (no param) — today's layout, unchanged.
    3. One listing detail page with ?layout=focused shows the single banner with the correct N.
    4. ?layout=classic on 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-sheri is host-aliased — the Vercel preview host skips the hostname-rewrite middleware even though the template code is identical.

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 isAutoScrollingRef guard set immediately before, and cleared immediately after, every autoplay/nudge scroll.
  • A small, always-visible pause/play <button> (aria-pressed reflecting "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.loading flipped 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:

CheckFeatured listings (1400px)Featured listings (400px)Communities (1400px)Communities (400px)
Has overflow to advance throughyesyesno (3 communities fit the row)yes
Advances within 8sn/a — no autoplay, no button (correct)
Static under prefers-reduced-motion: reducen/a
Pauses on hovern/a
Stops permanently after manual swipen/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).