Skip to content

Real-world example: Explore Julian

Explore Julian is the destination site for Julian, Pennsylvania, and the Bald Eagle Valley. Every page on it shares one Wander site embed, mounted once in the site’s layout. Each page then tells that map what it is about. The map never reloads as a visitor moves around the site.

This page walks through how it is built, with excerpts from its source. Use it as a reference for your own integration. The excerpts are lightly trimmed, and they use these IDs:

WhatID
Map (mapId)c2025939-af64-4371-a281-c6818b2cd96e
Namespaceexplorejulian
Bald Eagle Creek (path POI)16aa9696-cae3-4e87-9dcd-25eee5570527
Keystone Select section of the creek1a19623c-085c-4c68-8ea5-5deb4bf57382
PageWhat it sends
HomesetContext in emphasize mode. As the visitor scrolls the story chapters, it draws Bald Eagle Creek with setLines and animateLine, highlights a path section, raises the terrain and dims the map for the evening chapter.
Hub pages (/fish, /stay, /parks, /history)The hub’s places in only mode, with fit: true.
Article pages (/{hub}/{slug})The places the article mentions, as featured items with numbered badges. Callout buttons call highlightSection.
Place pages (/place/{poiId})The place and its getting-there spots in only mode, then select and a camera move once the context has applied.
ItinerariesEach itinerary’s route as a line, with setLines.
An itinerary, such as Trout water on Bald Eagle CreekA story route with storyRoute, its progress with storyRouteProgress, and the camera with flyTo({ …, duration: 0 }) on every animation frame.
Trail pages, such as Bald Eagle Creekselect on the path POI, which opens Trail View. The site sends no camera move.
EventsThe event venues; an event page selects its first venue. The page also offers the events calendar embed as a month view.
Getting hereAn empty context. The page shows the getting-here widget as a server-rendered iframe, followed by its attribution link.
Put the valley on your websiteEvery trail as featured in emphasize mode. The page itself is a kit of copyable embed snippets with live previews; see Every embed, ready to copy.

Explore Julian pins a versioned SDK file with its integrity hash, so a new SDK release can never change the live site by surprise. It sets integrity and crossOrigin before src, so the very first request is the checked one. It checks the version it got, and it reports a failed load to its own analytics:

const WANDER_SDK_URL = 'https://developer.wandermaps.com/sdk/wander-embed-0.11.0.js';
const WANDER_SDK_INTEGRITY = 'sha384-iRDbk8kR8xUGKppKBZNAOIlGe0dFxdvVm4CW24EqQMQ35qsxhj8ZOyC8FDr8KKZF';
const script = document.createElement('script');
script.addEventListener('load', () => {
if (window.Wander?.siteMap) setSdkLoaded(true);
else track('map_sdk_error', { kind: 'version', version: window.Wander?.VERSION ?? null });
});
// A network failure and an integrity mismatch look the same from here.
script.addEventListener('error', () => track('map_sdk_error', { kind: 'script' }));
script.integrity = WANDER_SDK_INTEGRITY; // before src
script.crossOrigin = 'anonymous'; // integrity needs CORS mode
script.src = WANDER_SDK_URL;
script.async = true;
document.head.appendChild(script);

The page also preloads the same URL, with the same integrity, from its <head>. Moving to a new release means changing both strings together. Explore Julian keeps 0.10.0 and its hash as a rollback pin, which is why it subscribes to the 0.11.0 Trail View events inside a try: older SDKs throw on event names they don’t know.

The map is mounted in the layout that wraps every page, never inside a page, so client-side navigation keeps it:

const map = window.Wander.siteMap(mapEl.current, {
mapId: 'c2025939-af64-4371-a281-c6818b2cd96e',
namespace: 'explorejulian',
mode: 'emphasize',
showControls: true,
associationsInCard: false, // the sidebar lists related places itself
trailView: true, // path POIs open in Trail View (SDK 0.11.0)
controlsPosition: 'bottom-right',
padding: padding(), // the parts of the map the sidebar covers
title: 'Explore Julian map — Julian, Pennsylvania',
fill: true,
lazy: false, // the map is above the fold
background: '#ECE5D6' // the site's paper colour, until tiles paint
});
// The SDK has no failure event: report a map that never becomes ready.
const readyTimer = setTimeout(() => track('map_ready_timeout', { sdkVersion: window.Wander.VERSION }), MAP_READY_TIMEOUT_MS);
map.on('ready', () => clearTimeout(readyTimer));

The SDK builds https://web.wander-app.com/siteembed/c2025939-af64-4371-a281-c6818b2cd96e?ns=explorejulian&mode=emphasize&…&trailView=1&…&sdk=0.11.0. The site never writes iframe HTML itself.

Telling the visitor’s clicks from its own

Section titled “Telling the visitor’s clicks from its own”

The site opens a place’s sidebar when a visitor taps a pin. Its own select and focus calls come back as poiSelected too, with source: 'host', so it ignores those:

map.on('poiSelected', ({ poiId, source }) => {
if (source === 'host') return; // our own select/focus/flyTo({ select: true })
openFromMap(poiId);
});

A place page replaces the context, then selects the place and frames it with its parking and trailheads. It waits for contextApplied, because fitBounds and flyTo find places by looking in the current context. It then waits for the place’s associations, up to 1.5 s, so the final camera move is the last one:

const start = () => {
map.off('contextApplied', start);
let moved = false;
const move = () => {
if (moved) return;
moved = true;
map.off('associationsLoaded', onLoaded);
if (fitIds) map.fitBounds(fitIds, padding()); // the place and its getting-there spots
else map.flyTo({ poiId: placeId, zoom: 15 });
};
const onLoaded = ({ poiIds }) => { if (poiIds?.includes(placeId)) move(); };
map.on('associationsLoaded', onLoaded);
setTimeout(move, 1500);
map.select(placeId);
};
map.on('contextApplied', start);
map.setContext(items, { mode: 'only', fit: false, padding: padding() });

The home page’s chapters use the story-map features. Each one is feature-detected, and each promise is caught, so the page still works against an embed that lacks one:

const can = (name) => map.hasCapability?.(name) === true;
// Fish chapter: draw the creek as the visitor arrives.
if (can('lines')) {
map.setLines([{ lineId: 'creek', pathId: '16aa9696-cae3-4e87-9dcd-25eee5570527', color: '#8FD0FF', glow: true, progress: 0 }])
.then(() => map.animateLine('creek', { to: 1, duration: 2600 }))
.catch(() => {});
}
// Leaving the story: put everything back.
if (can('lines')) map.clearLines().catch(() => {});
if (can('terrain')) map.resetTerrain({ duration: 600 }).catch(() => {});
if (can('appearance')) map.clearAppearance({ duration: 600 }).catch(() => {});

It also passes reveal: { stagger: 0 } and zero durations when the visitor’s system asks for reduced motion. The embed does the same by itself inside the iframe.

Article callouts about the creek’s regulation stretches call highlightSection. If the embed hasn’t seen the creek yet, it answers error { scope: 'section', reason: 'unknown_section' }. The site then shows the whole valley once and asks again:

function highlightWithRetry(map, sectionId) {
let retried = false;
const onError = (e) => {
if (e?.scope !== 'section' || e.sectionId !== sectionId || e.reason !== 'unknown_section') return;
map.off('error', onError);
if (retried) return;
retried = true;
map.flyTo({ center: { lng: -77.9, lat: 40.87 }, zoom: 10.5, duration: 0 });
setTimeout(() => map.highlightSection(sectionId, { fit: true }), 1500);
};
map.on('error', onError);
setTimeout(() => map.off('error', onError), 5000);
map.highlightSection(sectionId, { fit: true });
}
highlightWithRetry(map, '1a19623c-085c-4c68-8ea5-5deb4bf57382'); // Keystone Select

The current embed fetches a section’s trail when it hasn’t loaded it (see Path sections), so unknown_section now means the ID isn’t on the map at all. The retry is harmless and still covers older embeds.

Explore Julian’s four path POIs open in Trail View, on the same shared map:

Three rules make it work:

  1. Only select. A trail page sends select(trailId) and nothing else. Trail View flies the camera itself, and a flyTo or fitBounds sent while it opens cancels that flight.
  2. Let Trail View drive the layout. On trailViewOpened, a desktop page widens the map so the embed can dock its panel. The panel needs at least 640 px of visible map between the padding. On a phone, the page hides its own sheet and tab bar, so the embed’s sheet has the screen. trailViewClosed puts the layout back, except for reason: 'select_other', when the next trail is already opening.
  3. Hold the page’s context while its trail is open. setContext closes Trail View (reason: 'context'). A page about the open trail keeps its context until trailViewClosed, then sends it without a select and without a fit.
map.on('trailViewOpened', ({ poiId, lengthM }) => {
openTrail = poiId;
layout.enterTrailMode(); // wider map on desktop, no host sheet on phones
track('trail_view_opened', { poiId, miles: Math.round(lengthM / 160.9344) / 10 });
});
map.on('trailViewClosed', ({ poiId, reason }) => {
if (openTrail === poiId) openTrail = null;
if (reason === 'select_other') return; // another trail is opening: keep the layout
layout.leaveTrailMode();
if (held && held.select?.poiId === poiId) { // the page context held back while its trail was open
map.setContext(held.items, { mode: held.mode, fit: false, padding: padding() });
held = null;
}
});
// A trail page:
if (map.hasCapability('trailView')) map.select(trailId);

Explore Julian’s version of this is in components/shell/site-shell.tsx and lib/wander/trail-view.ts.

explorejulian.com/embed is a partner page for local businesses, organisers and writers. It offers each Wander embed with a live preview and code to copy, built from the site’s own data:

EmbedWhat the page offers
A trail in Trail ViewWander.map(el, { mapId, trailView: true, height: 560, autoResize: false }), then map.on('ready', () => map.select(trailId))
A day’s routeThe list embed: ?pois=…&route=full&routeType=driving&showNumbers=true&showAllPOIs=false
One businessThe simple embed with ?poiId=
The events calendarThe events calendar embed with ?view=month
Getting hereWander.gettingHere, with its attribution link
The whole mapThe full map embed with ?hideSignIn=true

Each iframe snippet ends with the attribution line from Wander’s share links. The previews share the page with the site’s own map, and each one has its own namespace (see Namespaces).

An itinerary page sends its stops once, then streams progress and the camera on every animation frame:

map.storyRoute(stops.map((s) => [s.lng, s.lat]), { profile: 'driving' });
map.on('storyRouteLoaded', ({ legs, fallback }) => renderLegs(legs, fallback));
// Each frame, while the visitor scrolls:
map.storyRouteProgress(routeAt); // legs completed + fraction of the current leg
map.flyTo({ center: { lng, lat }, zoom, pitch, bearing, duration: 0 });
// Leaving the page: the map outlives it, so take the route away and flatten the camera.
map.clearStoryRoute();
map.flyTo({ center: { lng, lat }, zoom, pitch: 0, bearing: 0, duration: 0 });

Explore Julian passes nothing about the visitor into the iframe: only its namespace. Its own “Powered by Wander” links carry campaign parameters that name the site and the slot:

url.searchParams.set('utm_source', 'explorejulian');
url.searchParams.set('utm_medium', 'referral');
url.searchParams.set('utm_campaign', 'powered_by_wander');
url.searchParams.set('utm_content', slot); // 'footer_badge', 'footer_dmo_cta'

The footer also links to the full map at https://web.wander-app.com/c2025939-af64-4371-a281-c6818b2cd96e. See Attribution & analytics for how Wander credits embed traffic.

  1. Mount one site map in your layout and keep it across navigation. Give any other embed on the same page its own namespace.
  2. Pin the SDK with its integrity hash, and check Wander.VERSION after it loads.
  3. Set background to your page colour and lazy: false for a map above the fold.
  4. Time out on ready yourself.
  5. Act on poiSelected only when source === 'user'.
  6. Move the camera after contextApplied, not straight after setContext.
  7. Feature-detect story-map features with hasCapability, and catch their promises.