These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

External widget — directory structure

Annotated tree of ext/2/source/ — the active production Svelte 5 widget. ext/1/ (Svelte 4 + Leaflet legacy) is out of scope; see decisions/two-widget-versions.

The high-level init sequence (how main.js picks a runtime and calls mount) is in init-flow.md. The CDN pointer fetch and localStorage payload cache are in cdn-and-pointer.md. The snippet loader that sits one level above this directory is in snippet-loader.md.

Tree

ext/2/source/ ├── index.html # Dev test page — mounts #finder-app with a ?key= param ├── package.json # ESM project; runtime: svelte + maplibre-gl; dev: vite + @sveltejs/vite-plugin-svelte ├── vite.config.mjs # Single-file output; CSS injected at runtime (cssInjectedByJsPlugin) ├── .env.development # VITE_API_APP_URL, VITE_API_TOMTOM_KEY, R2_* keys (gitignored) ├── .env.production # Same shape; production values (gitignored) ├── scripts/ │ └── deploy-r2.mjs # Upload snippet.js → R2; purge Cloudflare cache └── src/ ├── main.js # Entry point — variant routing + Svelte mount ├── App.svelte # Root component — layout engine + preview bridge ├── Reset.svelte # CSS variables + global resets; zero DOM output ├── components/ │ ├── Search.svelte # Search bar, autocomplete dropdown, filter/sort trigger │ ├── List.svelte # Location list — split, cards, list-only, overlay variants │ ├── Map.svelte # MapLibre GL map — markers, popups, active-location sync │ ├── Refinements.svelte # Filter + sort panel (popup or drawer) │ └── MobileDetail.svelte # Full-screen location detail sheet (≤640 px) └── lib/ ├── stores.js # All Svelte writable stores + initializeFinderStores() ├── api.js # Fetch helpers — pointer, payload, autocomplete, place details ├── filtering.js # Pure filter + sort logic — no Svelte dependency ├── localization.js # i18n messages (en/es) + translate/translateCount helpers ├── mapSettings.js # Provider normalization + MapLibre style builders ├── customFields.js # Custom-field rendering, hours parsing, Field Composer ├── searchMode.js # Natural-language query parser + text normalizer ├── analytics.js # Batched analytics with sendBeacon flush ├── embed.js # finder-key extraction from DOM attribute or script src └── directions.js # Directions URL builder (Google, Bing, DuckDuckGo, MapQuest)

Root files

index.html is a local dev harness only. It creates <div id="finder-app"> and loads main.js via <script type="module">. It is not deployed.

vite.config.mjs configures a single-chunk build. cssInjectedByJsPlugin inlines all styles into the JS bundle at runtime, which avoids a separate CSS request and simplifies the customer’s embed (one <script> tag, no <link>). The output filename is snippet.js in production and snippet.dev.js in --mode development. Both land in ext/2/ (one directory up from source/), not inside source/.

scripts/

deploy-r2.mjs is a Node ESM script run after vite build via npm run deploy:02. It uploads two copies of snippet.js to Cloudflare R2:

R2 keyCache-ControlPurpose
snippet.jspublic, max-age=300Root loader reference
ext/2/snippet.jspublic, max-age=300Versioned widget reference

After upload it optionally purges the Cloudflare CDN cache for both URLs. Purge is skipped when CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN are absent, which is normal in CI environments. Requires R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_BUCKET in the environment; it falls back to reading .env.development when those variables are absent.

src/main.js — entry point

Resolves the finder key via embed.js, then:

  1. Fetches the pointer (config.json) to get the variant field.
  2. If a non-default variant is specified, dynamically imports https://cdn.locationfinders.com/ext/v2/variants/${variant}.js. If that module exports an init function it is called and the default App is skipped entirely.
  3. Falls through to mount(App, { target, props }) for the default variant or when variant load fails.

See init-flow.md for the full boot sequence including geolocation and preview-bridge wiring.

src/App.svelte — root component

App.svelte is the layout engine and preview bridge. Its responsibilities:

  • Geolocation — calls resolveUserLocation(), which tries navigator.geolocation first and falls back to ipinfo.io/json.
  • Payload loading — calls fetchFinderPayload(finderKey, pointer.hash) and passes the result to initializeFinderStores().
  • Preview bridge — reads window.__LF_PREVIEW_MODE, window.__LF_PREVIEW_SESSION, and window.__LF_PREVIEW_PAYLOAD on mount; also subscribes to the lf-preview-payload CustomEvent. Preview payloads call applyPayload(..., { isPreview: true, preserveUiState: true }) so filter state is not reset on each builder update.
  • Layout selection — reads design.app.layout from the payload and normalizes the value through normalizeLayout(). Nine named layouts are supported: split-ltr-80, split-rtl-80, split-ltr-50, split-rtl-50, map-overlay, list-only, cards-only, cards-search, cards-map. Legacy string aliases (e.g. rtl, only-map) are mapped to canonical names.
  • Mobile tab layout — at ≤640 px, split and overlay variants collapse into a two-tab Map/List layout with a bottom tab bar and a “peek” card above the map.
  • Custom CSS scopingscopeCustomCSS() rewrites bare selectors to #finder-app <selector> and :root to #finder-app, keeping customer CSS sandboxed to the widget.
  • Loading skeleton — a shimmer placeholder (four skeleton cards + abstract map shapes) renders while the payload is in flight. An error state replaces it on failure.
  • Map branding — an optional text watermark overlaid on the map, controlled by design.map.brandLabel, brandOpacity, and brandEnabled.

src/Reset.svelte — CSS variables + global resets

Reset.svelte renders no DOM. It subscribes to designStore and calls applyThemeVariables() imperatively, setting all --finder-* CSS custom properties on #finder-app (or documentElement if the element is absent). It also loads DM Sans, DM Mono, and Instrument Serif from Google Fonts via <svelte:head>. The default theme palette and border-radius values live here; every incoming design.colors.* value merges over the defaults with a compatibility alias layer for renamed keys.

src/components/

Search.svelte

The search bar component. Renders the address/name text input, a geocode submit button, a filter toggle, and a sort toggle. When the user types, it debounces and calls fetchAutocompleteSuggestions() to populate a dropdown with both geocoder suggestions (Google Places, proxied via the origin API) and local location matches. Selecting a geocoder suggestion calls fetchPlaceDetails() and then setUserLocation(), triggering a refreshVisibleLocations() pass in stores.js. Accepts filterStyle, filterButtonPlacement, filterPanelStyle, sortStyle, and sortPlacement props that control how the refinements UI is surfaced.

List.svelte

Renders the filtered location list. Accepts a variant prop ("split", "cards", "list") that adjusts card layout and density. Key behaviours:

  • Subscribes to locationsStore and activeLocationStore.
  • Renders a count badge and “nearby results” or “using your location” eyebrow.
  • Empty state when the filtered list is zero.
  • “Show more” pagination controlled by a local limit variable.
  • Quick filter chips (showQuickFilters prop) rendered from enabled tag options.
  • Per-card custom-field rows, hours badge, distance, phone, website, and directions link sourced from customFields.js.
  • Clicks set activeLocationStore, which drives the map’s active marker.

Map.svelte

MapLibre GL map. Responsibilities:

  • Resolves the map tile style via resolveMapSettings() and buildMapStyle() from mapSettings.js.
  • Renders one marker per location in locationsStore; the active marker uses a distinct style.
  • Emits markerselect and mapclick custom events for App.svelte’s mobile peek logic.
  • Popup rendered on active-location change. The popup contains location name, address, directions link, and a “View” button that sets the list’s selected item.
  • Accepts overlaySearch, suppressPopup, and bottomPadding props.
  • Re-initialises the MapLibre style (destroying and recreating the map instance) when the resolved tile-style key changes, using getMapStyleKey() as a change signal.

Refinements.svelte

The filter and sort panel. Can render as a popup (anchored below the toolbar) or a full-width drawer, controlled by the filterPanelStyle prop. Sources filter options and sort options from refinementsStore.filterOptions and refinementsStore.sortOptions, which are built by initializeFinderStores() from the payload’s rulesConfig JSON. Writes selections back to refinementsStore, which triggers a refreshVisibleLocations() pass. Supports tag filter, country filter, radius slider, hours-open toggle, rating minimum, and advanced refinement toggles.

MobileDetail.svelte

A full-screen bottom-sheet component that renders the full location detail for the active location on narrow viewports (≤640 px). Triggered by mobileDetailOpen in App.svelte when the user taps “Details” in the peek card. Emits a close event. Contains the complete location fields rendered by the same customFields.js helpers used by List.svelte.

src/lib/

stores.js

Defines every Svelte writable store consumed by the widget:

StoreTypePurpose
payloadStoreobject | nullEnriched payload (design + parsedSettings merged in)
designStoreobjectdesign.* settings keyed by sub-namespace
settingsStoreobjectsettings.* settings (unitType, searchMode, radius)
localizationStoreobject{ locale, overrides } runtime
searchQueryStorestringCurrent text input value
filterPanelOpenStorebooleanFilter panel open/close toggle
refinementsStoreobjectAll active filter + sort state
cachedLocationsStorearrayRaw locations from payload (unchanged between preview updates)
locationsStorearrayFiltered + sorted locations (derived on demand)
activeLocationStoreobject | nullThe location currently highlighted on the map
userLocationStoreobject{ lat, lng, isGeolocated, error }

initializeFinderStores(payload, options) is the main write path. It:

  1. Walks payload.finder.settings[] and splits entries into design.*, settings.*, and refinements.* namespaces.
  2. Parses design.refinements.rulesConfig (JSON) to build filterOptions and sortOptions arrays with per-option enabled/order fields.
  3. Merges the enriched objects back into the payload and writes all stores.
  4. When options.preserveUiState is true (preview updates), existing search query, filter selections, radius, and sort state are carried forward — only newly unavailable tags/countries are pruned.
  5. Calls refreshVisibleLocations() which runs applyFinderFilters() from filtering.js and writes locationsStore and activeLocationStore.

setUserLocation() and getCurrentLocations() are the two other public exports.

api.js

All network calls. Four exports:

  • fetchIpLocation() — calls https://ipinfo.io/json; returns { lat, lng, country, region, city }.
  • fetchFinderPointer(finderKey) — fetches CDN/finders/{key}/config.json; returns the pointer object ({ hash, variant, … }) or null. Short TTL (5 min on CDN).
  • fetchFinderPayload(finderKey, knownHash) — three-tier fetch: (1) localStorage key lf-cdn-v2:{key} if hash matches, (2) CDN/finders/{key}/v/{hash}.json (immutable, year TTL), (3) origin API VITE_API_APP_URL/ext/finders/details/{key} fallback. Writes successful CDN fetches back to localStorage.
  • fetchAutocompleteSuggestions() / fetchPlaceDetails() — proxy calls to VITE_API_APP_URL/mapping/autocomplete and /mapping/places. See cdn-and-pointer.md for cache architecture detail.

filtering.js

Pure functions; no Svelte imports. The main export is applyFinderFilters(), which:

  1. Annotates every location with a distance value (Haversine, mi or km) when user coordinates are available.
  2. Applies text search (matchesSimpleQuery or matchesNaturalLanguageQuery depending on searchMode).
  3. Filters by selected tags, countries, and radius.
  4. Filters by hours-open status (_hours custom field via customFields.js).
  5. Filters by minimum rating.
  6. Applies advanced refinement rules (legacy parseRefinementValue DSL or structured conditions[] format).
  7. Calls sortLocations() to produce the final ordered array.

Also exports deriveTagOptions() and deriveCountryOptions() for populating the filter panel.

localization.js

Static message tables for en and es. Exports:

  • translate(runtime, key, params) — resolves a message key, applies {token} substitution, and respects per-finder overrides.
  • translateCount(runtime, baseKey, count) — selects .one vs .other suffix.
  • formatDistance(runtime, value, unit) — locale-formatted number + unit string.
  • formatLocaleTime(runtime, minutes) — minutes-since-midnight → locale time string via Intl.DateTimeFormat.
  • buildLocalizationRuntime(localization) — creates the { locale, overrides } shape expected by all translation helpers. Called by initializeFinderStores().

Adding a new locale requires an entry in LOCALIZATION_MESSAGES and a case in normalizeLocale().

mapSettings.js

Provider abstraction for MapLibre. Exports:

  • resolveMapSettings(mapDesign) — normalises provider (default tomtom), styleVariant (standard | dark | satellite | hybrid), and tileSet. Handles legacy tile-set string aliases.
  • buildMapStyle({ provider, styleVariant, apiKey }) — returns a MapLibre style JSON object for TomTom, Mapbox, HERE, or OSM tile endpoints.
  • getResolvedMapApiKey(...) — applies customer-supplied key, falls back to env-var keys (VITE_API_TOMTOM_KEY, etc.), and returns empty string for OSM (no key required).
  • providerNeedsApiKey(provider, tileSet) — returns false for OSM and for the tomtom-finder-beta gifted tile set; true otherwise. Used in App.svelte’s hasValidMapKey() guard.

See map-providers.md for the customer-facing map-provider comparison.

customFields.js

The most complex lib file. Handles:

  • Custom field renderinggetVisibleCustomFields(payload) reads design.listings.customFieldsConfig (JSON) and resolves display order, labels, and placement zones (aboveTitle, belowAddress, etc.). getLocationCustomFieldRowsByPlacement() groups rows for a single location into placement buckets.
  • Field ComposerparseFieldComposerConfig(payload) reads design.listings.fieldComposerConfig (JSON). getComposerBuiltinZones() + getComposerZoneFields() map fields to six named zones (headerLeft, headerRight, bodyLeft, bodyRight, footerLeft, footerRight).
  • HoursparseHoursField(rawValue) JSON-parses the _hours custom field. parseTimeToMinutes(value) converts "HH:MM" strings. getLocationHours(location, localization, now) returns a { status, badge, detail, label } object used by list cards, the mobile peek card, and MobileDetail.
  • Address formattingformatCityStatePostal(address).
  • URL formattingdisplayUrl(url) strips the https?://(www.)? prefix.

searchMode.js

Two search modes coexist. default mode calls normalizeSearchText() and does a substring match on a concatenated search document. natural_language mode calls parseNaturalLanguageQuery(), which:

  1. Detects “open now”, “closed now”, “open on {day}”, “open after {time}”, “open before {time}” patterns and populates a hoursConstraints object.
  2. Detects explicit geo intent (in {place}) and ZIP codes, building a geoIntent object.
  3. Collects tag and custom-field fieldConstraints by matching normalised option values.
  4. Collects matching advancedRefinements by name.
  5. Returns remaining tokens as textTerms (stop-words filtered).

filtering.js passes the parsed result to per-location matchers rather than doing raw string comparison.

analytics.js

Exports a singleton Analytics instance. capture(event, properties) enqueues an event object. Events are flushed in batches of up to 10, or every 5 seconds, via a setInterval. On page unload, any remaining queue is sent with navigator.sendBeacon. Preview mode is detected via window.__LF_PREVIEW_MODE and suppresses all events.

embed.js

Single export: getFinderKey(). Resolution order:

  1. document.querySelector('[finder-key]').getAttribute('finder-key')
  2. document.getElementById('finder-snippet') src query param ?key=
  3. Any <script> whose src contains /ext/2/, parsed for ?key=

Returns null if none of the above match. Called once in main.js before init().

directions.js

Single export: getDirectionsUrl(location, provider). Encodes a destination string from the location’s address fields and returns a provider-specific URL. Supported providers: google (default), bing, duckduckgo, mapquest.

Build output

vite build emits a single file into ext/2/ (the parent directory). cssInjectedByJsPlugin inlines all styles into the JS bundle; no separate .css file is written.

FileContentCache-Control (after deploy)
snippet.jsJS bundle with inlined CSSpublic, max-age=300, must-revalidate

The deploy:02 npm script chains vite build and node scripts/deploy-r2.mjs. After upload the 5-minute max-age means all customer pages pick up the new build within 5 minutes without any HTML change.

Key dependencies

PackageVersion (approx.)Role
svelte5.xComponent framework
maplibre-gl5.xMap rendering
vite8.xBuild tool
@sveltejs/vite-plugin-svelte7.xSvelte Vite integration
vite-plugin-css-injected-by-js3.xInline CSS into JS bundle
@aws-sdk/client-s33.xR2 upload (dev dependency)

There is no TypeScript. All source files are .js or .svelte. No test runner is configured.