← back to Homesonspec
stanley-martin: DEFERRED — Vite/React SPA, facts via runtime-resolved XHR only (RECON.md)
ef74e6c51ae211420260d68dbeaca8a023fc0088 · 2026-08-10 21:59:33 -0700 · Steve
robots-allowed, honest-UA GET returns 200 but a 1.6KB empty SPA shell with zero facts
on every home + community URL (FL/SC/NC/VA verified). Sitemap has 1,400 clean per-home
/new-homes/{numericId}/{addr} detail URLs. JSON XHR endpoint not statically discoverable
(no api host/route in the 3.6MB bundle; ~25 same-origin/subdomain probes all return the
shell); browser network observation forbidden by TK-10001 rules + blocked by classifier.
Same class as richmond-american. No adapter built.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A collectors/stanley-martin/RECON.md
Diff
commit ef74e6c51ae211420260d68dbeaca8a023fc0088
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 10 21:59:33 2026 -0700
stanley-martin: DEFERRED — Vite/React SPA, facts via runtime-resolved XHR only (RECON.md)
robots-allowed, honest-UA GET returns 200 but a 1.6KB empty SPA shell with zero facts
on every home + community URL (FL/SC/NC/VA verified). Sitemap has 1,400 clean per-home
/new-homes/{numericId}/{addr} detail URLs. JSON XHR endpoint not statically discoverable
(no api host/route in the 3.6MB bundle; ~25 same-origin/subdomain probes all return the
shell); browser network observation forbidden by TK-10001 rules + blocked by classifier.
Same class as richmond-american. No adapter built.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
collectors/stanley-martin/RECON.md | 170 +++++++++++++++++++++++++++++++++++++
1 file changed, 170 insertions(+)
diff --git a/collectors/stanley-martin/RECON.md b/collectors/stanley-martin/RECON.md
new file mode 100644
index 00000000..89b73352
--- /dev/null
+++ b/collectors/stanley-martin/RECON.md
@@ -0,0 +1,170 @@
+# Stanley Martin Homes — RECON (2026-08-11, TK-10001)
+
+**Status: DEFERRED — architecture-fit blocker (SPA / facts-via-XHR only). Same
+class as `richmond-american` (Blazor) — NOT bucket-D, NOT a robots/bot-wall block.**
+No adapter was built. `stanleymartin.com` is robots-ALLOWED and reachable under the
+honest UA, but **every user-facing URL returns an empty ~1.6 KB Vite/React SPA shell
+with zero facts** — price/beds/baths/sqft/address all materialize only via client-side
+XHR to a runtime-resolved API host that could not be discovered by static analysis, and
+per the TK-10001 hard rules browser/Playwright is barred (I confirmed the block). This
+file documents the exact verified path so a future browser-enabled shared fetcher — OR
+a discovered JSON endpoint — can adopt it quickly.
+
+Builder: Stanley Martin Homes (slug `stanley-martin`), Mid-Atlantic / Southeast
+(VA, MD, WV, NC, SC, GA, FL). Site https://www.stanleymartin.com.
+
+---
+
+## Why it's deferred
+
+`www.stanleymartin.com` is a **client-rendered Vite + React SPA on an ASP.NET / Azure
+backend** with **no server-side prerender**:
+
+- A plain honest-UA `GET` of a per-home detail page returns a **1,619-byte empty app
+ shell**: `<div id="root"></div>`, one hashed Vite bundle
+ (`/assets/index-CDMdppgq.js`), a stylesheet, GTM, and a generic
+ `<title>Stanley Martin Homes - New Home Builder</title>`. **No JSON-LD, no
+ `__PRELOADED_STATE__` / `__INITIAL_STATE__` data island, no price/beds/baths/sqft/
+ address anywhere in the delivered HTML.** (Verified on FL + SC + NC + VA homes and on
+ an 8-segment community/floorplan URL — all return the identical 1,619-byte shell.)
+- The facts (and the page's schema.org `RealEstateListing` / `PropertyValue` JSON-LD)
+ are **built client-side in the browser from a fetched payload**. `Server: ASP.NET`,
+ `x-azure-ref` present, `x-cache: CONFIG_NOCACHE`.
+
+The shared `@homesonspec/collectors-common` `LiveFetcher` is **HTTP-only** (`GET` /
+`postJson` via `fetch()`, no browser). With no facts in the HTML and no discoverable
+plain-fetch JSON endpoint (see below), there is nothing an HTTP-only adapter can
+extract. Building a Playwright render into this adapter is explicitly forbidden by the
+TK-10001 hard rules AND was denied by the environment classifier when attempted for
+recon — so this stays deferred, exactly like `richmond-american`.
+
+**No bot-wall / no circumvention involved.** The honest-UA GET returns HTTP 200 with the
+real shell (no 401/403/429, no Turnstile/CHEQ). The blocker is purely that the facts are
+never in the HTTP response body — not that access is denied.
+
+---
+
+## robots.txt — the data path IS allowed (verified)
+
+`https://www.stanleymartin.com/robots.txt`:
+
+```
+User-agent: *
+Allow: /
+Disallow: /smhWeb/session/
+Disallow: /smhweb/session/
+Disallow: /wp-
+Disallow: /xmlrpc.php
+Sitemap: https://www.stanleymartin.com/sitemap.xml
+```
+
+Wide-open `Allow: /`. The per-home detail URLs and the sitemap are ALLOWED (only
+`/smhWeb/session/`, `/wp-*`, `/xmlrpc.php` are disallowed — none match the data path).
+Verified against the framework's longest-match precedence with UA token `homesonspecbot`.
+
+---
+
+## VERIFIED discovery path (sitemap → per-home inventory URL)
+
+The sitemap crawl is clean and is the correct feed once a render/JSON path exists.
+
+1. **Sitemap** (single flat urlset, allowed): `https://www.stanleymartin.com/sitemap.xml`
+ — **2,474 `<loc>` URLs**. URL families:
+ - **Per-home inventory detail** (the feed we want): **1,400 URLs**, always the
+ 5-segment shape
+ **`/{st}/{metro}/new-homes/{listingId}/{street-address-slug}`**
+ where `{st}` is the 2-letter state slug (`va`/`md`/`wv`/`nc`/`sc`/`ga`/`fl`) and
+ `{listingId}` is a **numeric per-home id** (→ `builderInventoryId`). Filter:
+ `/new-homes/\d+/` in a `<loc>`. Verified examples:
+ - `…/fl/orlando/new-homes/164568/1409chamborddr`
+ - `…/sc/aiken-north-augusta/new-homes/209852/5641moncriefcircle`
+ - `…/nc/charlotte/new-homes/135723/9069grennanroad`
+ - `…/va/charlottesville/new-homes/172488/186sophiekathryndrive`
+ - **Community / metro landing** (full-state-name prefix, e.g.
+ `/florida/orlando`, `/southcarolina/charleston`) — SKIP.
+ - **Floorplan / plan-level** aggregate 8-segment URLs
+ (`/florida/orlando/orlando/avo/avalonparkorlando/floorplan/003421v00/archerii`)
+ — SKIP (plan pages, not per-home).
+ - Marketing pages (`/blog`, `/about-us`, …) — SKIP.
+
+ So the per-home feed is ~1,400 numeric-id detail URLs, cleanly regex-selectable.
+ Recommended `isDetailUrl`: `/^https?:\/\/www\.stanleymartin\.com\/[a-z]{2}\/[^/]+\/new-homes\/\d+\/[^/]+$/`.
+
+2. **Fetch each per-home URL** — **returns only the SPA shell (no facts).** This is the
+ deferral point: the address slug + listingId are in the URL, but price/beds/baths/
+ sqft/geo/plan/status are NOT in the HTML.
+
+---
+
+## What was tried to find a plain-fetch JSON endpoint (all negative)
+
+Per the feed-first discipline (a plain-fetchable JSON API would make this buildable via
+`LiveFetcher.postJson`/`fetch`, NOT a defer), I hunted for the XHR endpoint statically —
+no browser. **None found:**
+
+- **Main bundle static analysis** (`/assets/index-CDMdppgq.js`, ~3.6 MB): contains **no
+ API host, no `/api/...` route string, no `baseURL`/`axios.create`, no Controller/Action
+ route literals, no `VITE_*`/`import.meta.env` URL, no hashed lazy-chunk filename list.**
+ The only external hosts are Google Maps, `ik.imagekit.io/p40fshsib/` (image CDN), and
+ Syncfusion (the grid lib). The org JSON-LD is assembled from a runtime context value
+ (`${contextBase}appAssets/…`); the data API host is injected at runtime (built off
+ `window.location.host`) and lives in a `__vitePreload` lazy chunk that only resolves
+ when the app executes — unreachable by static grep.
+- **CSP `connect-src`** on the doc response = `'self'` (+ analytics/maps/livechat only).
+ That points XHR at `www.stanleymartin.com`, but **every `/api/...` path there returns
+ the SPA shell** (SPA catch-all), so the JSON endpoint is not a guessable same-origin
+ path. Probed ~25 candidates (`/api/listings/{id}`, `/api/listing/{id}`,
+ `/smhWeb/api/Listing/GetListingById?id=`, `/smhWeb/Listing/GetListing/{id}`,
+ `/sitecore/api/…`, `/umbraco/api/…`, `/DesktopModules/api/…`, `?id=` variants) — **all
+ 200 → 1,619-byte HTML shell.**
+- **Subdomains** in the CSP: `api.stanleymartin.com` and `homes.stanleymartin.com` both
+ **301 → `https://www.stanleymartin.com/`** (not an open API host);
+ `cloud.newhome.stanleymartin.com` serves an unrelated 200 HTML page;
+ `newhome.stanleymartin.com` does not resolve. Endpoint probes against
+ `api.stanleymartin.com` also collapse to the shell after the redirect.
+
+**Conclusion:** the JSON endpoint (host + path) is discoverable only by observing the
+app's live network traffic (Playwright/DevTools) — which the TK-10001 rules forbid and
+the environment classifier blocked. Until then, this is a browser-render deferral.
+
+---
+
+## What to do to ship it
+
+Two viable future paths — either unblocks a thin adapter over the already-verified
+1,400-URL sitemap feed:
+
+1. **Discover the JSON XHR endpoint** (preferred — keeps it plain-fetch). With a
+ one-time browser network-capture (or a green-lit Playwright recon pass), record the
+ request the SPA fires on a per-home page: host, path, method, and body. If it is a
+ plain JSON `GET`/`POST` with the honest UA (no auth cookie / signed token), wire it
+ through `LiveFetcher.fetch`/`postJson` keyed by the sitemap `{listingId}` — mirroring
+ the mattamy/pulte JSON-feed adapters. The extractor is then a straight JSON→
+ `ExtractedRecord` map (address, price, beds, full/half baths, sqft, stories, garage,
+ geo lat/lon, status, plan name, `builderInventoryId={listingId}`, sales phone),
+ images OMITTED, `mediaRights=NONE`.
+
+2. **Shared browser-render fetcher** (same fix `richmond-american` is waiting on). Add a
+ `RenderFetcher` to `collectors-common` (same robots + rate-limit + honest-UA rails,
+ Playwright page render returning rendered HTML/JSON-LD as the `RawPage` body). Then
+ this adapter is a thin sitemap crawler (regex above) + a JSON-LD/DOM `extract()` — the
+ rendered page carries a `RealEstateListing`/`PropertyValue` JSON-LD block assembled
+ client-side, which is the natural extraction target.
+
+Emission (either path) mirrors `smith-douglas`/`mi-homes`: emit **community FIRST**
+(`canonicalHints {builderSlug:"stanley-martin", communityName}`) then **inventory_home**
+(`{builderSlug, communityName, address, builderInventoryId:{listingId}, planName}`);
+`homeType:"SINGLE_FAMILY"`, `images:[]`. Null every field genuinely absent — never
+fabricate.
+
+## Verified evidence summary
+- robots.txt: `Allow: /`, sitemap declared, data path allowed — VERIFIED.
+- sitemap: 2,474 URLs; **1,400** per-home `…/new-homes/{numericId}/{addr-slug}` detail
+ URLs across FL/SC/NC/VA/GA/MD/WV — VERIFIED, cleanly regex-selectable.
+- per-home page HTML: **1,619-byte empty Vite/React SPA shell, zero facts** on
+ FL/SC/NC/VA samples + a floorplan community URL — VERIFIED.
+- honest-UA GET: HTTP **200** (no 401/403/429, no bot-wall) — VERIFIED (not bucket-D).
+- JSON endpoint: **not discoverable via static analysis**; ~25 same-origin/subdomain API
+ probes all return the shell — VERIFIED negative.
+- browser observation to find the endpoint: **forbidden by TK-10001 hard rules + blocked
+ by the environment classifier** — hence DEFERRED.
← 8d0fbaf1 clayton-properties: DEFER recon — scope mismatch (manufactur
·
back to Homesonspec
·
TK-10001: build M/I Homes + Smith Douglas adapters (national d4ac2fbd →