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 — overview

The embeddable visitor-facing finder. Svelte 5 + MapLibre GL, compiled by Vite 8 into a single self-contained snippet.js, served from Cloudflare R2 via Cloudflare CDN. Customers embed it with one <div> and one <script> tag — no npm install, no build step on their side.

Sources cited throughout: dropafinder-app-external/AGENTS.md, ext/2/source/package.json, ext/2/source/vite.config.mjs, ext/2/source/src/main.js, ext/2/source/src/lib/.


Active version: ext/2/

The repository contains two widget generations:

DirectoryStackStatus
ext/1/Svelte 4 + LeafletLegacy — out of scope. Do not modify.
ext/2/Svelte 5 + MapLibre GL 5Production. All active work goes here.

The decision to build a new widget rather than upgrade in-place is documented in decisions/two-widget-versions.


Tech stack

LayerChoiceNotes
UI frameworkSvelte 5 (^5.55.1)Uses mount() API (not new App()). Compiled away at build time — zero Svelte runtime on customer pages.
Map renderingMapLibre GL (^5.22.0)Open-source WebGL map renderer. Replaces Leaflet from ext/1.
Build toolVite 8 (^8.0.5)ESM-first, fast HMR in dev.
CSS deliveryvite-plugin-css-injected-by-jsCSS is injected at runtime from JS — no separate .css file to host or version.
LanguageJavaScript (no TypeScript)Intentional. Keeps the build toolchain thin and the output predictable for a customer-hosted asset.
State managementSvelte writable storesNo Zustand, TanStack Query, or external state libraries. Keeps the runtime footprint minimal on customer pages (AGENTS.md).

Distribution model

The widget is not an npm package. It ships as a plain JavaScript file embedded via an HTML <script> tag:

<div id="finder-app" finder-key="YOUR_FINDER_TOKEN"></div> <script id="finder-snippet" type="module" src="https://cdn.locationfinders.com/snippet.js"></script>

The customer pastes this snippet once and never touches it again. All version transitions (bug fixes, feature updates, A/B variants) happen server-side by swapping what the CDN serves — the customer’s HTML stays unchanged. See snippet-loader for how the two-file loader chain makes this possible.


Source layout (ext/2/source/)

ext/2/source/ ├── src/ │ ├── main.js — entry point: key resolution, variant gate, Svelte mount │ ├── App.svelte — root component; owns layout and top-level state │ ├── Reset.svelte — CSS baseline / scoped reset │ ├── components/ │ │ ├── List.svelte — location list panel │ │ ├── Map.svelte — MapLibre GL map panel │ │ ├── MobileDetail.svelte — mobile location detail drawer │ │ ├── Refinements.svelte — filter/refinement UI │ │ └── Search.svelte — search bar + autocomplete │ └── lib/ │ ├── api.js — ALL fetch calls (pointer, payload, autocomplete, place details) │ ├── analytics.js — event tracking helpers │ ├── customFields.js — custom field rendering logic │ ├── directions.js — "get directions" URL builder │ ├── embed.js — reads finder-key from DOM / script URL │ ├── filtering.js — client-side refinement filtering │ ├── localization.js — locale/currency formatting │ ├── mapSettings.js — MapLibre style + layer configuration │ ├── searchMode.js — keyword vs. proximity search mode logic │ └── stores.js — Svelte writable stores (shared app state) ├── scripts/ │ └── deploy-r2.mjs — uploads built assets to Cloudflare R2 + purges CDN cache ├── index.html — dev harness only; not deployed ├── vite.config.mjs └── package.json

The build outputs into ext/2/ (the parent directory, outDir: '../'), producing snippet.js (production) or snippet.dev.js (development mode).


Build modes

Vite is configured with two output modes (vite.config.mjs):

CommandOutput fileVITE_API_APP_URL points to
npm run buildext/2/snippet.jsProduction API
npm run build:devext/2/snippet.dev.jsLocal API (localhost)

The snippet.dev.js build exists so that the Next.js app (dropafinder-app-nextjs) can serve it locally during development via the proxy route at src/app/api/ext/v2/snippet/route.ts. The proxy tries snippet.dev.js first, then snippet.js, then falls back to the live CDN URL — meaning local widget development does not require touching the CDN.


CSS and theming

CSS is bundled into the JS output by vite-plugin-css-injected-by-js (cssCodeSplit: false). At runtime the plugin injects a <style> tag into the document head.

The widget’s visual theme is driven entirely by CSS custom properties set on the #finder-app element (e.g. --finder-button-search-background). Components consume these properties in their <style> blocks — hex values are never hardcoded. New themeable values must be added to the token set (AGENTS.md: Design tokens).

Svelte scoped styles do not reach dynamically injected MapLibre map tiles. For those cases, custom properties are set directly on the host element with target.style.setProperty(...).

All class names follow BEM (block__element--modifier). (AGENTS.md: CSS naming.)


API boundary

All network requests originate from src/lib/api.js. Components and stores never call fetch() directly (AGENTS.md: API calls). The five exported functions are:

  • fetchFinderPointer(finderKey) — short-TTL pointer file; returns { hash, variant, published_at, … } or null
  • fetchFinderPayload(finderKey, knownHash?) — CDN hash-versioned payload with localStorage cache + origin API fallback
  • fetchIpLocation() — IP geolocation via ipinfo.io (non-critical; caught silently on failure)
  • fetchAutocompleteSuggestions(…) / fetchPlaceDetails(…) — address search via the Next.js backend API

The pointer / payload fetch chain is the CDN caching model. A full trace of this flow is in ../../../architecture/data-flow-end-user.md.


Error handling policy

Sourced from AGENTS.md:

  • Critical failures (missing finder key, invalid map key) → set the error state variable so the error UI renders.
  • Non-critical failures (geolocation, localStorage reads/writes) → catch silently, fall back gracefully. Never surface a broken UI for a convenience feature.

Deployment

npm run deploy:02 (defined in package.json) runs npm run build then scripts/deploy-r2.mjs. The deploy script:

  1. Uploads snippet.js (root loader) to the R2 bucket at key snippet.jsCache-Control: public, max-age=300, must-revalidate
  2. Uploads ext/2/snippet.js (compiled widget runtime) to key ext/2/snippet.js — same cache header
  3. Optionally purges both CDN URLs via the Cloudflare API when CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN are set

Dry-run mode (DEPLOY_DRY_RUN=1 node scripts/deploy-r2.mjs) prints file sizes without uploading.

Required environment variables: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET. Optional: R2_ENDPOINT, R2_REGION, CDN_BASE_URL, CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_TOKEN. Place them in ext/2/source/.env.deploy.local (gitignored) or export from your shell.