Itineraries & listing maps
The list embed covers two integrations that come up on almost every partner site: a multi-day itinerary and a listing map whose pins are whatever the surrounding page happens to be showing. Both work as a plain iframe; both get better with the SDK, which lets the surrounding page and the map stay in sync without reloads.
Live, working versions of both:
- Multi-day itinerary demo — host-rendered day tabs driving one embed
- Listing map demo — a filtered directory page feeding the embed its POI set
Multi-day itineraries
Section titled “Multi-day itineraries”An itinerary is a set of POIs plus a day for each one. There are two ways to supply it.
Curated in Wander. Build the itinerary as a list on your map, assign each stop a day, and reference the list. Days and stop order come from the list itself:
<iframe src="https://web.wander-app.com/listembed/{mapId}?sb={listId}&route=full&showNumbers=true" width="100%" height="520" style="border: 0" allow="geolocation" title="Three days in Wake County"></iframe>Assembled by your site. Pass the POI ids and a positionally aligned days list. Nothing has to exist in Wander beyond the POIs themselves, so this is the shape to use when the itinerary lives in your CMS, is built by a trip planner, or is generated per visitor:
<iframe src="https://web.wander-app.com/listembed/{mapId}?pois=31d0358a-…,e3ea831f-…,45fbf0d2-…&days=1,2,3&route=full&showNumbers=true" width="100%" height="520" style="border: 0" allow="geolocation" title="Three days in Wake County"></iframe>days is aligned by position: pois=a,b,c&days=1,1,2 puts the first two stops on day 1 and the third on day 2. Omit days and every POI lands on day 1. When the highest day is 2 or more, the embed shows its own day chip strip, dims the other days’ pins, and reframes to the selected day.
A POI id repeated in pois is a real, separate stop, not a mistake — that is how a loop route that returns to its starting point is expressed, and how one place appears on more than one day:
?pois=TR,a,b,TR&days=1,1,1,1 # out-and-back, route closes properly?pois=a,b,a,c&days=1,1,2,2 # `a` appears on both daysDriving days from your own UI
Section titled “Driving days from your own UI”With the SDK, pass the itinerary as stops and let your page own the day tabs. Build them from itineraryChanged rather than from your CMS rows, so the tabs describe what the map actually loaded:
<div id="map"></div><script src="https://developer.wandermaps.com/sdk/wander-embed.js"></script><script> const map = Wander.map('#map', { mapId: '{mapId}', stops: [ { poiId: '31d0358a-…', day: 1 }, { poiId: '32474970-…', day: 1 }, { poiId: 'e3ea831f-…', day: 2 }, { poiId: '45fbf0d2-…', day: 3 } ], route: 'full', routeType: 'driving', showNumbers: true, showAllPois: false });
map.on('itineraryChanged', ({ poiCount, maxDay, days }) => { // days: [{ day: 1, poiIds: [...] }, { day: 2, poiIds: [...] }, …] renderDayTabs(days); });
// Fires for the embed's own day chips as well as your setActiveDay calls, // so mirror this event instead of only tracking your own clicks. map.on('dayChanged', ({ day }) => markActiveTab(day));
dayTab.onclick = () => map.setActiveDay(day); // null shows every day stopCard.onclick = () => map.flyTo({ poiId, zoom: 15 }); // camera only</script>flyTo moves the camera only, and for a host-driven jump that is usually what you want. select: true additionally opens the embed’s own POI card, which covers most of a small or rail-sized map with detail your page is already showing in the stop or listing it just tapped. Prefer leaving it off and letting the visitor open the card by tapping the pin; reach for it only when the map is large and your page has nothing of its own to show.
The embed renders its own day chip strip whenever the itinerary spans two or more days, so a host that also renders day tabs shows the control twice. The two stay in sync either way. There is currently no parameter to suppress the embed’s strip — contact your Wander partner manager if you need one.
Listing and directory maps
Section titled “Listing and directory maps”A listing page — where to stay, member directory, “18 breweries in the county”, search results — already knows which records it is rendering. Hand the embed the same ids and it becomes that page’s map, with no list to maintain in Wander:
<iframe src="https://web.wander-app.com/listembed/{mapId}?pois=31d0358a-…,88171840-…,32474970-…&showAllPOIs=false" width="100%" height="460" style="border: 0" allow="geolocation" title="Downtown Raleigh listings"></iframe>showAllPOIs=false is what makes it a listing map rather than a destination map: only the ids you passed get pins.
If the ids are known when the page renders, that iframe is the entire integration — no JavaScript required. Wander.embedUrl() builds the same URL if you would rather not concatenate it yourself:
const src = Wander.embedUrl({ mapId, pois: listings.map(l => l.wanderPoiId) });Keeping the map in step with filters
Section titled “Keeping the map in step with filters”When the visitor filters, sorts, or pages the listings, hand the embed the new set:
const map = Wander.map('#map', { mapId: '{mapId}', pois: visible.map(l => l.wanderPoiId), showAllPois: false});
function onFilterChange() { map.setPois(visible.map(l => l.wanderPoiId));}
// Card → pincard.onclick = () => map.flyTo({ poiId: card.dataset.poi, zoom: 16 });
// Pin → cardmap.on('poiSelected', ({ poiId }) => highlightCard(poiId));setPois reloads the iframe — the embed reads its POI set at render time — so it is the one operation here that is not free. It returns immediately when the set is unchanged, which makes it safe to call from a live-filtering input on every keystroke.
Checking that your ids resolved
Section titled “Checking that your ids resolved”itineraryChanged reports the POIs the embed actually loaded. Comparing that against your own row count is the cheapest way to catch content drift — a listing whose POI was deleted or moved to another map simply will not come back:
map.on('itineraryChanged', ({ poiCount }) => { if (poiCount < visible.length) { console.warn(`${visible.length - poiCount} listing(s) did not resolve on this map`); }});POI ids from a different map under the same publisher resolve fine. Ids belonging to another publisher’s map do not — if a set of embeds mixes maps, confirm which map each id actually lives on before assuming the pins are missing for another reason.
Mobile: put the map in a drawer
Section titled “Mobile: put the map in a drawer”On a phone, a map parked below a long itinerary or listing grid is effectively invisible: the visitor taps a stop, nothing appears to happen, and they have to scroll to find out that it did. Both demos above solve this the same way — the map is a bottom sheet that slides up on tap, with a grab handle, a scrim, a back button, and a persistent Map button to reopen it.
Two things make this work, and both are easy to get wrong:
Never re-parent the iframe. Moving an iframe in the DOM reloads the document inside it, which means a fresh map load and a dropped bridge connection every time the drawer opens. Use one node in one place and restyle it per breakpoint:
/* desktop: a column beside the content */.map-dock { position: sticky; top: 66px; }
@media (max-width: 820px) { /* mobile: the same node, as a sheet */ .map-dock { position: fixed; left: 0; right: 0; bottom: 0; height: 78vh; transform: translateY(100%); transition: transform .28s cubic-bezier(.32,.72,0,1); display: flex; flex-direction: column; } .map-dock.open { transform: translateY(0); } .map-dock iframe { height: 100% !important; }}Hide the closed sheet with transform, not display: none — a display-hidden iframe stops rendering, so re-opening it costs a full remount.
Give the desktop container a real height. With autoResize off the iframe is height: 100%, which resolves against its parent — and a parent with no height collapses the map to a couple of hundred pixels. That is worse than it looks: the embed fits its POIs with roughly 150px of vertical padding, so on a short map there is almost no room left and the camera zooms far out to compensate. A spread of stops across one county can end up framing half the state.
.map-frame { /* leaves room for whatever you put under the map */ height: clamp(380px, calc(100vh - 420px), 660px);}@media (max-width: 820px) { /* inside the sheet the flex column owns the height instead */ .map-frame { flex: 1; min-height: 0; height: auto; }}Turn off autoResize when the sheet owns the height. The embed reports a recommended height through dimensionChanged, and the SDK applies it to the iframe by default. Inside a flex sheet that fights your layout, so let the sheet size the map:
const map = Wander.map('#map', { mapId, pois, autoResize: false, height: 460 });map.frame.style.height = '100%';Then opening the drawer is just a class toggle, and tapping a listing does both at once:
function focusListing(poiId) { map.flyTo({ poiId, zoom: 16, duration: 1000 }); if (window.matchMedia('(max-width: 820px)').matches) dock.classList.add('open');}Give visitors more than one way back out — a back button, the scrim, Escape, and a downward swipe on the grab handle — and, in the listing demo, prev/next arrows inside the sheet so they can step through the filtered set without closing the map at all.