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 targetSteps 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 parsesnippet.jsas 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.cominscript-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.jsand theext/2/snippet.jsruntime are deployed viaext/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.js — getFinderKey().
What happens: The function reads the finder key from one of two places, in this order:
- The
finder-keyattribute on<div id="finder-app">— the recommended placement - 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-keyattribute 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-95 — loadPointer(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.jsonThe 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-95 — loadPayloadByHash(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}.jsonThe 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:
- CDN cold start — a brand-new finder hasn’t been synced to R2 yet
- 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’sauthorized_urlsallowlist, 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:
- Reads design tokens from the payload and applies them as CSS custom properties on the mount root
- Initializes Svelte stores (
src/lib/stores.js) —locations,settings,user,filtering, etc. - Kicks off IP geolocation via ipinfo.io (only if the design payload requests it)
- 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
| File | Role |
|---|---|
dropafinder-app-external/snippet.js | Root loader; dynamic imports the runtime |
dropafinder-app-external/ext/2/snippet.js | Built runtime artifact (output of Vite build) |
dropafinder-app-external/ext/2/source/src/main.js | Runtime entry; orchestrates init |
dropafinder-app-external/ext/2/source/src/lib/embed.js | getFinderKey() and DOM probing |
dropafinder-app-external/ext/2/source/src/lib/api.js:1-95 | Pointer + payload fetch + origin fallback |
dropafinder-app-external/ext/2/source/src/lib/stores.js | Svelte stores initialized in step 6 |
dropafinder-app-external/ext/2/source/src/App.svelte | Root component; preview-mode bridge lives here |
dropafinder-app-backend/routes/api.php | Origin fallback endpoint registration |
Where to next
- The CDN-side detail of pointer/payload: external/cdn-and-pointer
- The dashboard’s preview-mode bridge: external/preview-bridge
- The widget’s overall directory layout: external/directory-structure
- The cross-codebase view of where the widget fits: Architecture overview
- Why the pointer/payload split exists: decisions/cdn-pointer-pattern