External widget — snippet loader
The snippet loader is a two-file chain that gives DropAFinder the ability to update, A/B test, or roll back the widget runtime without requiring any change to the HTML snippet a customer has already pasted. The customer always loads the same stable URL; all routing decisions happen server-side or inside the thin loader script.
Sources cited throughout: dropafinder-app-external/snippet.js, ext/2/snippet.js (minified build artifact), ext/2/source/src/main.js, dropafinder-app-nextjs/src/app/api/ext/v2/snippet/route.ts.
The two-file chain
Customer's page
└── <script src="https://cdn.locationfinders.com/snippet.js"> ← root loader
└── import('/ext/2/snippet.js') ← compiled widget runtime
└── main.js: key → pointer → variant? → mountFile 1: root loader (snippet.js)
dropafinder-app-external/snippet.js is a single line:
import('/ext/2/snippet.js');That is the entire file. It performs one dynamic import, resolving relative to cdn.locationfinders.com, and delegates everything else to the ext/2/ runtime. No configuration parsing, no DOM access, no feature flags at this layer.
File 2: ext/2/snippet.js
The compiled Vite output of ext/2/source/src/. Contains the full Svelte 5 widget, MapLibre GL, all component styles (injected as a <style> tag by vite-plugin-css-injected-by-js), and the main.js initialization logic described below.
Why this indirection layer exists
The customer pastes one snippet and never touches it again:
<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 root loader’s Cache-Control header is public, max-age=300, must-revalidate (5 minutes). That means:
- Customers are never more than 5 minutes behind any deployment.
- The same short TTL applies to
ext/2/snippet.js— both files cache for 5 minutes. - Swapping the
ext/2/snippet.jstarget (e.g. to a new version or a variant) takes effect globally within one cache cycle without any customer action.
If the root loader embedded the widget code directly, updating the runtime would require contacting every customer to update their <script> tag — impractical at scale.
CDN caching semantics
Both files are deployed to Cloudflare R2 and served through Cloudflare CDN.
| File | R2 key | Cache-Control |
|---|---|---|
| Root loader | snippet.js | public, max-age=300, must-revalidate |
| Widget runtime | ext/2/snippet.js | public, max-age=300, must-revalidate |
On deploy, scripts/deploy-r2.mjs purges both URLs from Cloudflare’s edge cache (when CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN are set), so changes propagate immediately regardless of the 5-minute TTL.
The 5-minute TTL is a deliberate trade-off: short enough to limit stale-widget exposure, long enough to avoid overwhelming the CDN origin on high-traffic sites.
main.js initialization sequence
After ext/2/snippet.js executes, the first code that runs is src/main.js. The full sequence:
1. Mount-target guard
const target = document.getElementById('finder-app');
if (!target) {
throw new Error('Missing #finder-app mount element.');
}The widget hard-requires #finder-app in the DOM. If it is absent, execution halts with a thrown error rather than silently failing. This is the correct behavior for a critical dependency (AGENTS.md: Error surfacing).
2. Finder key resolution
const finderKey = getFinderKey() ?? '';getFinderKey() (in src/lib/embed.js) checks two locations in priority order:
- The
finder-keyattribute on any element in the document (typically#finder-appitself) - A
?key=query-string parameter on the<script id="finder-snippet">tag, or any<script>whosesrccontains/ext/2/
Returning an empty string (rather than throwing) when no key is found lets the widget mount in an error state instead of crashing ungracefully.
3. Pointer fetch + variant gate
async function init() {
let pointer = null;
if (finderKey) {
pointer = await fetchFinderPointer(finderKey);
const variant = pointer?.variant;
if (variant && variant !== 'default') {
try {
const mod = await import(
/* @vite-ignore */ `${CDN_BASE}/ext/v2/variants/${variant}.js`
);
if (typeof mod.init === 'function') {
mod.init(target, finderKey, pointer);
return; // variant handled — skip default App
}
} catch {}
// variant load failed — fall through to default App
}
}
mount(App, { target, props: { finderKey, pointer } });
}fetchFinderPointer(finderKey) fetches https://cdn.locationfinders.com/finders/{finderKey}/config.json. This is the short-TTL pointer file that also carries a variant field alongside the content hash. See ../../../architecture/data-flow-end-user.md for the full pointer/payload cache chain.
4. Default mount
If there is no variant (or if the variant fails to load), the default App.svelte is mounted:
mount(App, { target, props: { finderKey, pointer } });mount() is the Svelte 5 API. The pointer is passed as a prop so App.svelte can read the content hash directly rather than re-fetching it.
Variant infrastructure
The variant system is built around the pointer file’s variant field and a CDN path convention.
Pattern: https://cdn.locationfinders.com/ext/v2/variants/{variant}.js
Contract: a variant module must export an init(target, finderKey, pointer) function. If the module exists but does not export init, or if the network request fails for any reason, the catch block silences the error and main.js falls through to mount the default App.
This means variant failures are non-fatal by design: a broken variant deployment degrades to the default widget rather than a blank page.
🔴 [NEEDS CLARIFICATION: Are variants in active use today, or is this infrastructure only? No variant modules were observed in the CDN path or the source repository at the time of writing.]
ext/2/snippet.dev.js — local development build
Running npm run build:dev in ext/2/source/ produces ext/2/snippet.dev.js. This build is identical to snippet.js except that VITE_API_APP_URL is set to the local API base URL (.env.development), allowing the widget to call a locally-running Next.js server.
The Next.js proxy route at src/app/api/ext/v2/snippet/route.ts serves this file in development:
const candidates =
process.env.NODE_ENV === 'development'
? ['../dropafinder-app-external/ext/2/snippet.dev.js',
'../dropafinder-app-external/ext/2/snippet.js']
: [];Resolution order:
snippet.dev.js— local dev build with local API URL (preferred)snippet.js— local production build with production API URL (fallback)- Live CDN — proxied to avoid browser CORS restrictions (final fallback when no local build exists)
The proxy serves all responses with Cache-Control: no-cache, preventing the browser from caching a stale local build during development.
In production NODE_ENV === 'production', so candidates is empty and the proxy always fetches from the CDN. The proxy is a development convenience only.
Customer embed reference
The minimum viable embed:
<!-- Mount element — finder-key is the public finder token -->
<div id="finder-app" finder-key="YOUR_FINDER_TOKEN"></div>
<!-- Loader script — type="module" is required for dynamic import() -->
<script id="finder-snippet" type="module"
src="https://cdn.locationfinders.com/snippet.js"></script>The id="finder-snippet" attribute on the <script> tag is used by getFinderKey() as one of two fallback mechanisms for resolving the finder key. Omitting it forces the function to scan all <script> tags for a matching src pattern — functionally equivalent but slightly less efficient.
💡 Tip: The finder-key attribute can also go on any other element — the embed.js logic checks document.querySelector('[finder-key]') first, before inspecting the script tag. This allows edge cases like server-rendered pages where the script tag is defined in a layout template and the key is injected separately.
Related pages
- init-flow — deeper walkthrough of what happens inside
App.svelteafter mount - overview — widget codebase stack, source layout, deployment
- ../../../architecture/overview.md — system-wide context
- ../../../architecture/data-flow-end-user.md — full end-user request trace including pointer/payload cache chain