Build Standards
The rules the website must be built to. Internal: visitors never see this chapter’s ideas, only their results.
Accessibility: WCAG 2.2 AA
A launch requirement, tested on the search and listing flows first because they are the hardest.
- KeyboardEvery control reachable and operable in a logical order; visible 2 px focus everywhere (2.4.7, 2.4.11). Skip link on every page.
- Dialogs and sheetsNative
<dialog>where possible; focus moves in, is contained, Escape closes, focus returns to the trigger. - SearchCombobox pattern for location suggestions; result-count changes announced with a polite live region; the list is the accessible equivalent of the map.
- FormsVisible labels,
autocompletetokens, errors linked witharia-describedbyand summarised at the top on submit (3.3.1–3.3.3). No CAPTCHA puzzles (3.3.8). - TargetsAt least 44 × 44 px; never below the 24 × 24 px minimum (2.5.8). Dragging on the map always has a non-drag alternative (2.5.7).
- ContrastPairs from 3.6 only. Text on photographs only over a solid field.
- MediaListing photos get alt text from room and feature data (“Kitchen with gas range and window over the sink”), never “image 1.” Virtual tours offer a text summary.
- MotionReduced-motion respected (8.5); nothing flashes; no auto-advancing content.
Performance budget
Measured at the 75th percentile on a mid-range phone over 4G.
| Metric | Budget | How |
|---|---|---|
| Largest Contentful Paint | ≤ 2.0 s | Hero image preloaded with fetchpriority="high", AVIF/WebP sources, server-rendered listing data |
| Interaction to Next Paint | ≤ 150 ms | No framework on content pages; search JS split and deferred |
| Cumulative Layout Shift | ≤ 0.05 | Fixed aspect ratios on every image and map; fonts with metric-matched fallbacks |
| Fonts | ≤ 130 KB | Three WOFF2 files, Latin subset, self-hosted, font-display: swap |
| JavaScript, content pages | ≤ 30 KB | Progressive enhancement; every page usable without it |
| JavaScript, search | ≤ 180 KB | Map library loaded on interaction or when the map view is chosen |
Platform layers
Built for REWAP, separated as if it were the first of many brokerages. Each layer can change without touching the others; none of this appears in the interface.
| Layer | Owns | Never knows about |
|---|---|---|
| Brand | Tokens, fonts, logo set, theme.json styles | Any brokerage’s data |
| Content | Pages, guides, community pages, market updates (block patterns) | Which IDX vendor is active |
| Brokerage | Legal name, license, offices, disclosures, principal broker | Presentation |
| People | Brokers and agents: license, role, contact, team membership | Listing feed internals |
| Teams | Team name, own mark, members, routing preferences | Brand tokens (teams keep theirs) |
| Property | Normalised listing records (9.4), media, open houses | Vendor field names |
| IDX adapter | Fetching, mapping and caching one vendor’s feed into the Property schema | Templates |
| Lead routing | Rules: listing agent → team → brokerage fallback; round robin; office hours | CRM specifics |
| CRM adapter | Pushing leads and events to the chosen CRM | Page design |
| Analytics | Consent-aware events: search, save, contact, call | Personal data beyond consent |
| Compliance | Disclosures, Fair Housing, MLS attribution and display rules, privacy, cookie consent | Nothing: every template must include it |
IDX adapter contract
Templates read only this shape. A new vendor means one new adapter class, not new templates. Field names follow the RESO Data Dictionary where one exists.
interface ListingProvider {
search(query: ListingQuery): Promise<Page<Listing>>;
get(listingKey: string): Promise<Listing | null>;
openHouses(range: DateRange, area?: AreaRef): Promise<OpenHouse[]>;
areas(prefix: string): Promise<AreaRef[]>; // towns, neighborhoods, ZIPs
attribution(listing: Listing): Attribution; // MLS + listing office text, logo rules
freshness(): Promise<{ updatedAt: string }>; // shown on every results view
}
type Listing = {
listingKey: string; mlsNumber: string;
status: 'Active' | 'ActiveUnderContract' | 'Pending' | 'Closed' | 'ComingSoon';
listPrice: number; closePrice?: number;
address: { street: string; unit?: string; city: string; state: 'MA'; postalCode: string };
geo?: { lat: number; lng: number };
propertyType: 'Residential' | 'MultiFamily' | 'Condo' | 'Land' | 'Commercial';
bedrooms?: number; bathroomsTotal?: number; livingArea?: number; lotSizeAcres?: number;
yearBuilt?: number; units?: number; taxAnnual?: { amount: number; year: number };
media: { url: string; width: number; height: number; caption?: string }[];
remarks?: string;
listOffice: { name: string; id: string }; listAgent?: { name: string; id: string };
isOwnListing: boolean; // true when listOffice is REWAP or an affiliated team
updatedAt: string;
};
Brokerage data model
Affiliation is a relationship, not a hierarchy baked into URLs, so a team can move or a second brokerage can join without rewriting pages.
URLs and search visibility
Pages exist to answer real questions people ask about Massachusetts real estate, for search engines and answer engines alike. Value before keywords.
| Path | Page | Answers |
|---|---|---|
/buy/ | Buying with REWAP | How buying works in Massachusetts, step by step, with the legal milestones |
/sell/ | Selling with REWAP | Pricing, preparation, the P&S, closing |
/invest/ | Investment advice | Multi-family, rent rolls, financing structures |
/search/ | Property search | Map and list; filters reflected in the query string |
/homes/{town}-ma/{address-slug}-{mls}/ | Listing detail | Stable even if status changes; sold listings follow MLS display rules |
/communities/{region}/{town}/ | Community guide | Housing stock, transit, schools data with sources, market snapshot |
/market/{yyyy}/{mm}/ | Market update | Monthly figures with method and source |
/guides/{slug}/ | Guides | Buying a home in Worcester, selling in Massachusetts, contingencies explained |
/teams/{slug}/ · /people/{slug}/ | Teams and people | Who they are, under which brokerage, their listings |
- Answer-readyEach guide opens with a two-sentence answer, then the detail. Questions as headings where natural; FAQ blocks only where questions are real.
- SourcedEvery market figure carries its source and date in the caption. Generative engines cite pages that cite.
- Equal regionsCommunity coverage is planned by region, so no part of the state is thin.
Structured data
Schema.org JSON-LD generated from the data layers, never hand-written per page.
| Page | Types |
|---|---|
| Site-wide | RealEstateAgent (the brokerage) with PostalAddress, areaServed = Massachusetts; WebSite with SearchAction |
| Person, team | Person with worksFor; team as Organization with parentOrganization |
| Listing | SingleFamilyResidence / Apartment / House with Offer (price, availability), Place, ImageObject |
| Community | Place with containedInPlace; BreadcrumbList |
| Guide, market update | Article with author, dates, citation; FAQPage only for real Q&A |
{
"@context": "https://schema.org",
"@type": "RealEstateAgent",
"name": "REWAP Brokerage LLC",
"url": "https://rewapbrokerage.com/",
"telephone": "+1-508-509-7759",
"address": { "@type": "PostalAddress", "streetAddress": "652 Park Ave",
"addressLocality": "Worcester", "addressRegion": "MA", "postalCode": "01603", "addressCountry": "US" },
"areaServed": { "@type": "State", "name": "Massachusetts" },
"founder": { "@type": "Person", "name": "Hong Tran", "jobTitle": "Founder, Real Estate Lawyer" },
"subOrganization": { "@type": "Organization", "name": "The Oberdorfer Group" }
}
WordPress translation
The production site is a block theme. This book maps onto it directly, with native features first and the fewest plugins possible.
- theme.jsonPalette, type scale, spacing and layout widths from tokens/theme.json (draft). Editors pick only from the palette.
- PatternsEach composition in 6.4 and component in chapter 7 becomes a locked block pattern: Index, Plate, Statement, Catalogue, Ledger, CTA.
- Content typesCustom post types for Person, Team, Community and Market Update; taxonomies for Region and Town. Listings stay in the IDX layer, cached, not as posts.
- IDXThe vendor plugin supplies data only; its widgets are not used. A small REWAP plugin implements the adapter (9.4) and renders with the theme’s blocks.
- HostingCloudways, with page caching for content and short-lived object caching for listing queries.
Compliance surfaces
Designed in from the start, styled as content (7.10), and reviewed by Hong Tran before launch. This list is a checklist, not legal advice.
- Agency disclosureThe Massachusetts Mandatory Licensee-Consumer Relationship Disclosure, linked from every contact point.
- Fair HousingEqual Housing Opportunity statement and logo in the footer; language rules in 1.9 and 5.1.
- MLS display rulesListing office attribution, data-currency notice and disclaimer on every listing view, per the MLS’s IDX rules.
- Brokerage identificationLegal name and office address on every page.
- PrivacyPrivacy policy, consent before non-essential analytics, clear opt-in for listing alerts and texts.
- Accessibility statementPublished with a contact route for problems.