Purpose
Filter the Beno (beno.com) Dubai yacht-charter catalog by yacht size (length in feet) and return the matching yachts with their size, guest capacity, cabins, price, and detail-page link. Read-only: it constrains the listing and extracts results; it never books or pays.
When to Use
- A user wants only yachts within a size range, e.g. "show me large yachts (60 ft and up)" or "yachts between 80 and 120 feet" on beno.com.
- A user wants the count and details of Beno yachts that meet a minimum/maximum length.
- As a first step before comparing or shortlisting yachts by capacity (guests scale roughly with length).
Workflow
The reliable method is the on-page Size slider in the Filters panel. Beno is an Angular SSR app whose listing is populated by XHR to apps-api.beno.com/v3/deals/products/yacht. There is no usable API or URL shortcut: the listing API returns 403 to any out-of-app request (an HTTP interceptor injects an auth header), and the ?sizes=min,max deep-link is broken on direct navigation (see Gotchas). Drive the slider.
Keep the whole navigate → open-panel → set-range → apply → extract sequence inside one browserless_agent call's commands array — the session persists across separate calls, keyed by the call's proxy/profile config, so this is a convenience that saves round-trips and avoids accidentally dropping that config (repeat the same proxy on every call to stay in the same warmed session). Beno is UAE-geo, so use a top-level proxy: { proxy: "residential", proxyCountry: "ae" } (plain requests likely also work, but the proxy egress was used).
Navigate to the plain listing — no query string:
{ "method": "goto", "params": { "url": "https://www.beno.com/yachts", "waitUntil": "load", "timeout": 45000 } }, { "method": "waitForTimeout", "params": { "time": 4000 } }The page shows ~10 skeleton "Loading…" cards first, then real yacht cards appear once the XHR resolves.
Open the Filters panel. Click the element whose visible text is "Filters". It opens an Angular CDK overlay (
.cdk-overlay-container) containing Sort By, Harbors, Bedrooms, Type, Brands, and Size. Reliable click via JS:{ "method": "evaluate", "params": { "content": "(()=>{const e=[...document.querySelectorAll('button,a,[role=button],div')].filter(b=>/^Filters$/i.test((b.textContent||'').trim()));for(const x of e){const r=x.getBoundingClientRect();if(r.width>0){x.click();return 'ok';}}return 'no';})()" } }Set the Size range. "Size" is a dual-handle range slider (feet) with two inputs inside the overlay:
input[name="minValueInput"]— min=20, max=200, default 20input[name="maxValueInput"]— min=20, max=200, default 200
The panel is long and a
{ "method": "snapshot" }truncates it, so do not try to drag handles by pixel coordinates. Set the value with the native setter + Angularinput/changeevents, then click Apply. Example for "≥ 60 ft" (set the min handle to 60, leave max at 200):{ "method": "evaluate", "params": { "content": "(()=>{const ov=document.querySelector('.cdk-overlay-container');const minI=ov.querySelector('input[name=minValueInput]');const set=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;set.call(minI,'60');minI.dispatchEvent(new Event('input',{bubbles:true}));minI.dispatchEvent(new Event('change',{bubbles:true}));const a=[...ov.querySelectorAll('button,[role=button],div')].find(b=>/^Apply$/i.test((b.textContent||'').trim()));a&&a.click();return 'applied';})()" } }For a bounded range (e.g. 80–120 ft) set
maxValueInputthe same way before clicking Apply.Wait for refresh:
{ "method": "waitForTimeout", "params": { "time": 5000 } }The app fires
GET .../v3/deals/products/yacht?...&sizes[]=<min>-<max>&...(dash-delimited, e.g.sizes[]=60-200) → 200 OK, and the listing re-renders. The browser URL updates tohttps://www.beno.com/yachts?sizes=<min>,<max>and a count badge ("1") appears on the Filters button.Extract results with an
evaluate(ortext) command in the samecommandsarray. Each card is ana[href*="/yachts/"]whose text contains"<n> Length"(size in feet),"<n> Guests","<n> Cabins", and a per-hour/day price. The yacht's name is the card heading (or the first slug segment of the href, e.g./yachts/santorini/e8Kb3W→ "Santorini"). Verify every returned card's Length is within the requested range. Page 1 returns up to 10 cards; the listing lazy-loads more pages (row=10&page=N) as you scroll — issue{ "method": "scroll", "params": { "direction": "down" } }commands to the bottom and re-extract to collect the full filtered set.
Site-Specific Gotchas
?sizes=min,maxdeep-link is broken on fresh load. Navigating directly tohttps://www.beno.com/yachts?sizes=60,200makes the app re-serialize the value with a comma (sizes[]=60,200), which the backend rejects with HTTP 500 — the listing hangs forever on "Loading…". Only the in-app slider + Apply produces the working dash form (sizes[]=60-200). Always start from the plain/yachtsURL and use the slider; never hand-craft the?sizes=URL.- Listing API is not callable directly.
GET https://apps-api.beno.com/v3/deals/products/yachtreturns{"message":"Forbidden"}(403) to a plain fetch /browserless_agentrequest, and even an in-pagefetch()fails — the Angular app adds a required auth header via an HTTP interceptor. Don't waste time scripting the API; drive the UI. - "Size" = length in feet. The filter section is titled "Size"; the value shown on each card is labeled "Length". Slider bounds are 20–200 ft.
- Cards load via XHR after hydration. Expect ~10 "Loading…" skeletons for a couple of seconds on first paint and after each filter apply. Wait 4–5 s before extracting.
- Slider can't be dragged reliably. The CDK overlay is taller than the viewport and the accessibility snapshot truncates it. Set
minValueInput/maxValueInputprogrammatically (native value setter + dispatchedinputandchangeevents) instead of coordinate dragging. - Pagination. Results are paged at 10/row; scroll to trigger
page=2,3,…to get the complete filtered list before reporting a total. - Anti-bot: none observed. Pages and the SSR HTML load with no captcha/login wall, so
browserless_agent's built-in stealth is enough. The successful run used a residential proxy (proxy: { proxy: "residential", proxyCountry: "ae" }); plain (proxy-less) requests likely also work.
Expected Output
Success — minimum size applied (e.g. ≥ 60 ft):
{
"success": true,
"filter_applied": "size >= 60 ft",
"filter_mechanism": "dual range slider (input[name=minValueInput]/maxValueInput, 20-200 ft) -> Apply; app calls apps-api with sizes[]=60-200",
"result_url": "https://www.beno.com/yachts?sizes=60,200",
"result_count": 29,
"yachts": [
{
"name": "Jude",
"size_ft": 74,
"guests": 27,
"cabins": 3,
"price": "9,250 AED",
"href": "/yachts/jude/Zmo18r"
},
{
"name": "Julia",
"size_ft": 64,
"guests": 21,
"cabins": 3,
"price": "1,950 AED",
"href": "/yachts/julia/PkQ6mq"
},
{
"name": "Santorini",
"size_ft": 115,
"guests": 80,
"cabins": 5,
"price": "8,750 AED",
"href": "/yachts/santorini/e8Kb3W"
},
{
"name": "Encore",
"size_ft": 131,
"guests": 30,
"cabins": 5,
"price": null,
"href": "/yachts/encore/L8JQX3"
}
],
"error_reasoning": null
}Bounded range (e.g. 80–120 ft) — same shape, filter_applied: "size 80-120 ft", result_url ends ?sizes=80,120, and every size_ft falls within the band.
No matches (range excludes all yachts):
{
"success": true,
"filter_applied": "size >= 190 ft",
"result_count": 0,
"yachts": [],
"error_reasoning": null
}Failure (e.g. the broken deep-link was used and the listing hung):
{
"success": false,
"filter_applied": "size >= 60 ft",
"result_count": 0,
"yachts": [],
"error_reasoning": "Navigated directly to ?sizes=60,200; app sent sizes[]=60,200 (comma) -> apps-api 500; listing stuck on 'Loading...'. Use the in-app slider + Apply instead."
}