How a filter is applied
This is the page to read before you tell a customer "Gerry filtered my list but the call just dropped." The short version: a normal page load ends the call, so Gerry never does one — until something makes it impossible not to.
Why not just reload the page
Measured on our own sessions collection: the voice provider ends a call the instant the browser participant leaves the room, and every call after a navigation is closed by the provider's own report a few seconds later (endReason: 'provider_report'). The room token lives 60 seconds, so even an instant reload cannot rejoin it. A reload is the end of the conversation, not a pause in it — there is no page-load event on the other side for the assistant to pick up in.
What happens instead, in order
The engine decides the address exactly as it would for a normal link — the map's parameters, path segments, and the site's own facet links, all unchanged — and then, instead of navigating to it:
fetchthat address: same-origin, a plainGET, the visitor's own cookies, no headers of ours (one would trigger a CORS preflight and let a CDN'sVaryhand back a page the browser itself would never see). Gives up after six seconds.- Parse the answer with
DOMParser. - Replace the results region — and the filter panel too, if the map opted in — with the fetched page's own markup for it.
history.pushStatethe address, so the URL bar, sharing and the Back button all behave, and adopt the fetched page's<title>.- Dispatch a
gerry:navigateevent onwindow, so your own page script can react — there is noloadevent this time. - Read the count and the rows off the now-live document, and answer the model with them.
Back and forward do the same fetch-and-swap, from a popstate listener. Nothing is ever half-swapped: every replacement node is resolved and ready before anything on the page is removed.
When it falls back to a real page load
Anything that goes wrong falls back to location.assign — the behaviour the widget had before soft navigation existed, resume from sessionStorage and all. The ladder, checked in this order:
map_says_reload | The map's navigation is set to reload — see below. |
|---|---|
cross_origin | The address, or where a redirect points, is off site.origin. |
bad_status | The fetch didn't come back 2xx. |
not_html | The response wasn't text/html. |
no_swap_target | A region the map wants swapped is missing from either the current document or the fetched one. |
When a fallback happens, the conversation tries to continue on the new page with the same token; the visitor may need one more click on Gerry to restore audio. This is exactly the resume mechanism the widget always had — it just now only runs when soft navigation genuinely can't.
What your search page needs to look like
Two map fields decide what gets swapped, and getting them right is almost the whole job:
results.container— a selector for the one region holding the count and the rows. Name it explicitly. Left absent, it's derived as the nearest common parent of the count element and the first row, and refused outright if that guess reaches<body>or higher — because swapping the whole document would take the widget, and every listener your page has, with it.facets.container— opt-in, and deliberately so. Setting it swaps your filter panel alongside the results, so its checkboxes and links reflect the new address; leaving it out keeps your panel exactly as it was, which goes stale but is the safer of the two wrongs, because replacing it destroys every event listener your page attached to its own controls. A page that wants both — a live panel and no listeners lost — should delegate its own listeners ondocumentinstead of binding them to specific nodes;public/demo/index.htmlin the widget repo does exactly that.
If your page reads location.search or location.pathname to decide what to render — most do — add one listener for gerry:navigate (and, for completeness, popstate) that re-reads the address and repaints, synchronously. The count Gerry says out loud is read the moment that handler returns, so anything asynchronous there is read too early.
When to declare navigation: "reload" instead
Say so when your raw HTML simply doesn't contain the results: a single-page app, a list hydrated client-side, a grid fetched by your own XHR after the page loads. There is nothing in the fetched document for the engine to swap, so every filter would be refused anyway — declaring reload up front skips the wasted request and the six-second timeout on every single filter. It's also the honest choice for a results region that genuinely can't be replaced without breaking the page: listeners bound per row with no delegation, a chart or a map widget sitting among the results, an <iframe> that must not be re-created. A map that says reload keeps the old behaviour, and with it the old limitation: the call ends at the first filter, same as any voice widget that reloads the page.
Opening a listing
pages.detail marks a single-listing page, never navigated to in place as a search result would be. pages.openIn decides how open_listing gets there: page (default) uses the same soft navigation, so the conversation survives and Gerry can describe the page it just opened; newTab is for a detail template that can't be swapped in, and keeps the call running on the search page the visitor started from.
open_listing with openIn: 'page' on the demo store.Questions: hello@heygerry.io. These pages describe what is built today; nothing here is a promise about what is not.