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 — init flow

Six steps from <script> load to a mounted Svelte component. Each step has a failure mode worth knowing because it shapes the visitor’s experience and the integrator’s debugging path. This page walks the steps and notes how each one fails.

For the higher-level architectural view of how the widget fits into the three-codebase system, see Architecture overview. For the embed contract from the customer’s perspective, see embedding/script-tag.

The six steps

1. Script loads → main.js executes 2. getFinderKey() → reads <div id="finder-app" finder-key="..."> 3. Pointer fetch → GET cdn.../finders/{key}/config.json 4. Payload fetch → GET cdn.../finders/{key}/v/{hash}.json 5. Origin fallback → GET api.../ext/finders/details/{key} (only on CDN failure) 6. Mount Svelte App → renders into mount target

Steps 1–5 happen in src/main.js and src/lib/api.js. Step 6 hands off to src/App.svelte, which then initializes Svelte stores and triggers the actual UI render.

Step 1 — Script loads, main.js runs

Where: Customer’s host page loads <script type="module" src="https://cdn.locationfinders.com/snippet.js">.

What happens: The browser fetches snippet.js (the root loader at the repo root, not under ext/2/). The root loader is a thin wrapper that reads the pointer (step 3, ahead-of-time) and dynamic-imports the appropriate runtime — typically ext/2/snippet.js, but a pointer.variant value can route to an alternative variant build (variant infrastructure exists but no variants observed in production).

The runtime entry is src/main.js. It runs synchronously on import.

Failure modes:

  • type="module" missing on the script tag — the browser silently fails to parse snippet.js as ES module syntax. No error in the Console; the widget just never initializes.
  • CSP blocks the script source — the host page’s Content Security Policy doesn’t allow https://cdn.locationfinders.com in script-src. Visible as a CSP violation in the Console.
  • CDN unavailable — rare; the script tag fails to load. Browser shows a network error.

🔒 Internal only: The root snippet.js and the ext/2/snippet.js runtime are deployed via ext/2/source/scripts/deploy-r2.mjs. Both have a 5-minute Cloudflare cache TTL; runtime updates take up to 5 minutes to roll across edges.

Step 2 — getFinderKey()

Where: src/lib/embed.jsgetFinderKey().

What happens: The function reads the finder key from one of two places, in this order:

  1. The finder-key attribute on <div id="finder-app"> — the recommended placement
  2. The ?key=... query parameter on the script URL — legacy fallback for backwards compatibility

If neither is present, the function returns null and the init flow short-circuits before any fetch.

Failure modes:

  • Mount target missing<div id="finder-app"> doesn’t exist in the DOM at the time the script runs. The script may briefly poll (clarify) or just bail.
  • Both sources empty — no finder-key attribute and no ?key= query param. The widget logs a console warning and stops.

🔴 [NEEDS CLARIFICATION: Confirm whether the script polls for the mount target if it isn’t present at script-load time, or whether it bails immediately. Affects the recommendation on script tag placement (head vs. before-</body>) in customer docs.]

Step 3 — Pointer fetch

Where: src/lib/api.js:1-95loadPointer(finderKey).

What happens: The widget fetches the pointer — a small JSON object that names the current payload version:

GET https://cdn.locationfinders.com/finders/{key}/config.json

The pointer’s shape (logical):

{ "hash": "abc123def...", "variant": null }

The hash field is what step 4 uses to construct the payload URL. The variant field, if non-null, instructs the loader to use an alternative runtime build — but in practice this is unused today.

The pointer is cached at the CDN edge for 5 minutes (the cache directive is set by deploy-r2.mjs on upload).

Failure modes:

  • 404 on pointer — the finder key doesn’t correspond to any pointer in R2. The widget falls through to step 5 (origin API fallback).
  • CDN unavailable — same as above; falls through to origin API.
  • Malformed pointer JSON — extremely rare; would indicate a corrupted R2 object. The widget logs and falls through.

Step 4 — Payload fetch

Where: src/lib/api.js:39-95loadPayloadByHash(finderKey, hash).

What happens: Using the hash from the pointer, the widget fetches the actual payload:

GET https://cdn.locationfinders.com/finders/{key}/v/{hash}.json

The payload contains the full finder design (color tokens, layout, copy) and locations data. It’s cached at the CDN edge forever (the URL changes whenever the data changes — see decisions/cdn-pointer-pattern).

Before the network fetch, the widget checks localStorage for a previously-loaded payload at the same hash. A localStorage hit returns instantly — no fetch needed. This is the per-visitor warm cache.

Failure modes:

  • 404 on payload — the hash named by the pointer doesn’t exist in R2 (corrupted pointer or partial deploy). Widget falls through to step 5.
  • JSON parse error — payload corruption; falls through to step 5.
  • localStorage corrupted — the cached entry doesn’t deserialize; widget re-fetches from CDN.

Step 5 — Origin API fallback

Where: src/lib/api.js — fallback path triggered when steps 3 or 4 fail.

What happens: The widget fetches from the backend’s origin endpoint:

GET https://api.locationfinders.com/api/ext/finders/details/{key}

This endpoint (registered in the backend’s routes/api.php under the /ext prefix) returns the same finder config + locations payload, but live from the database rather than the CDN. It’s slower but always returns the latest version.

This fallback exists for two cases:

  1. CDN cold start — a brand-new finder hasn’t been synced to R2 yet
  2. CDN unavailable — Cloudflare R2 is down or unreachable

The origin endpoint is public (no JWT) and authenticates via the {key} URL parameter, the same way the CDN-served pointer does.

Failure modes:

  • 404 on origin — the finder key doesn’t correspond to any Finder in the database. The widget logs an error and stops; the mount div stays empty.
  • 5xx on origin — backend is down or erroring. Widget logs and stops.
  • Authorized URLs check fails — the backend may also enforce origin allowlist server-side at this point. Returns a specific error code that the widget can surface.

🔴 [NEEDS CLARIFICATION: Confirm whether the backend’s /ext/finders/details/{key} endpoint enforces the Finder’s authorized_urls allowlist, or whether that’s a widget-only check. Different security postures for each.]

Step 6 — Mount Svelte App

Where: src/main.js (mount call) → src/App.svelte (root component).

What happens: With a payload in hand, the widget instantiates the Svelte App component, passing finderKey and pointer as props. The App’s onMount:

  1. Reads design tokens from the payload and applies them as CSS custom properties on the mount root
  2. Initializes Svelte stores (src/lib/stores.js) — locations, settings, user, filtering, etc.
  3. Kicks off IP geolocation via ipinfo.io (only if the design payload requests it)
  4. Renders the visitor-facing UI (Search, Map, List, Refinements components)

Once mounted, the widget is interactive. Visitor interactions update Svelte stores reactively; UI re-renders follow.

Failure modes:

  • Payload schema mismatch — the payload has fields the runtime doesn’t recognize, or is missing required fields. The widget tries to render with defaults; some surfaces may be blank.
  • Geolocation rejected — the visitor declined the browser prompt. Distance-based refinements degrade gracefully (no current location → no radius filter).
  • Map provider tile request blocked — host page CSP doesn’t allow the map tile origin. The map area renders empty; the rest of the widget works.

Preview-mode bypass

If window.__LF_PREVIEW_MODE is true at init time (the dashboard sets this when embedding the widget in Live Preview), steps 3–5 are skipped. Instead:

  • The widget reads the payload directly from window.__LF_PREVIEW_PAYLOAD
  • Origin allowlist checks are bypassed
  • Analytics emission is suppressed

See external/preview-bridge for the full preview-mode contract.

Sequence summary

[Browser loads <script>] [main.js runs] [getFinderKey()] ──── nothing → console warn, stop [Preview mode?] ──── yes → use __LF_PREVIEW_PAYLOAD, mount ↓ no [Pointer fetch] ──── 404/error → Origin fallback (step 5) ↓ ok [Payload fetch] ──── 404/error → Origin fallback (step 5) ↓ ok [Mount App] [App.onMount: tokens → stores → geolocation → render]

Files in this flow

FileRole
dropafinder-app-external/snippet.jsRoot loader; dynamic imports the runtime
dropafinder-app-external/ext/2/snippet.jsBuilt runtime artifact (output of Vite build)
dropafinder-app-external/ext/2/source/src/main.jsRuntime entry; orchestrates init
dropafinder-app-external/ext/2/source/src/lib/embed.jsgetFinderKey() and DOM probing
dropafinder-app-external/ext/2/source/src/lib/api.js:1-95Pointer + payload fetch + origin fallback
dropafinder-app-external/ext/2/source/src/lib/stores.jsSvelte stores initialized in step 6
dropafinder-app-external/ext/2/source/src/App.svelteRoot component; preview-mode bridge lives here
dropafinder-app-backend/routes/api.phpOrigin fallback endpoint registration

Where to next