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

How to — add a feature (widget)

Recipe for adding a feature to the embeddable Svelte widget (dropafinder-app-external/ext/2/source/). Most features are payload-driven — the backend controls what the widget renders, and the dashboard controls what the user configures. Think payload-shape first.

Decide: payload-driven or pure-client?

  • Payload-driven (recommended): The backend writes a value into the Finder.design JSON (or settings array). The widget reads it from designStore on boot and reacts to changes via the Live Preview bridge. Use this for anything the dashboard user can toggle.
  • Pure-client: Widget reads directly from the DOM or from a VITE_* env var. Use only for widget-internal concerns (animation timing, a/b flags) that don’t belong in the builder UI.

The rest of this guide assumes payload-driven. For the full payload flow see content/internal/architecture/data-flow-builder.md.

Step 1 — Extend the design payload (if needed)

If the feature adds a configurable value, it becomes a field in the design object inside the finder’s settings array. The widget reads settings via initializeFinderStores() in ext/2/source/src/lib/stores.js. That function splits the flat settings array by prefix — entries named design.* become nested keys in designStore.

No backend migration is needed — design is a JSON column on the finders table. But coordinate the field name with the backend so PUT /finders/{id} stores it correctly.

Verify the field name you’ll use (e.g., design.search.autoclosePanelOnSelect) is distinct from existing keys:

grep -r "design\." \ "/Users/codydavis/Local Sites/dropafinder-app-external/ext/2/source/src/lib/stores.js"

Step 2 — Add the Svelte component (or extend an existing one)

Components live in ext/2/source/src/components/:

src/components/ ├── List.svelte # Location list panel ├── Map.svelte # MapLibre map ├── MobileDetail.svelte # Mobile detail view ├── Refinements.svelte # Filter / refinement drawer └── Search.svelte # Search bar

For a new standalone surface, create a new .svelte file here. For a change to an existing surface, edit the relevant component.

Example — reading a new design value from the store inside a component:

<script> import { designStore } from '../lib/stores.js'; // Reactive binding to the store $: autoclosePanelOnSelect = $designStore?.search?.autoclosePanelOnSelect ?? false; </script>

Step 3 — Update the stores if new state is needed

If the feature requires widget-local state (not from the payload), add a new writable store to ext/2/source/src/lib/stores.js:

import { writable } from 'svelte/store'; export const myFeatureStore = writable(null);

Export it, then import it in the components that need it. Do not add payload-derived values as separate stores — read them reactively from designStore or payloadStore.

Step 4 — Wire the Live Preview bridge

The Finder Builder’s Live Preview passes an updated payload to the widget via window.__LF_PREVIEW_PAYLOAD and a previewSession query-parameter. App.svelte reads this on every render cycle and calls initializeFinderStores() with the new payload.

No changes to the bridge are needed unless the feature adds a new payload path that is not a design.* or settings.* key. If it is, the bridge picks it up automatically because initializeFinderStores() processes the whole payload.

If you add a new top-level payload key (unusual), update initializeFinderStores() in ext/2/source/src/lib/stores.js to handle it. See the advanced_refinements branch in that function as a model.

Step 5 — Mount the component in App.svelte

If the component is entirely new (not just an augmented existing one), import and mount it inside ext/2/source/src/App.svelte:

<script> import MyFeature from './components/MyFeature.svelte'; </script> {#if showMyFeature} <MyFeature /> {/if}

The showMyFeature condition should be derived from the design store, not local state, so it responds to Live Preview updates.

Step 6 — Test in dev

cd "/Users/codydavis/Local Sites/dropafinder-app-external/ext/2/source" npm run dev # Vite dev server with HMR

Set VITE_API_APP_URL=http://127.0.0.1:8000/api in .env.local to point at the local backend.

Alternatively, run a one-shot dev build and verify through the dashboard’s Live Preview:

npm run build:dev # produces ext/2/snippet.dev.js

Then open http://localhost:3000 in the dashboard and navigate to the Finder Builder. The Live Preview picks up snippet.dev.js automatically.

Step 7 — Build and deploy

npm run deploy:02

This runs npm run build (Vite production build → ext/2/snippet.js) followed by scripts/deploy-r2.mjs, which uploads two files to Cloudflare R2:

  • snippet.js — the root-level alias (full CDN URL: https://cdn.locationfinders.com/snippet.js)
  • ext/2/snippet.js — versioned path

deploy-r2.mjs reads AWS/R2 credentials from .env.development. After the upload, the 5-minute CDN TTL means production visitors see the new bundle within 5 minutes of deploy.

See deploy for the full deploy ritual and cache-purge checklist.

Common mistakes

  • Reading directly from payloadStore instead of designStore → the Live Preview bridge updates designStore separately from payloadStore; reading only payloadStore means the preview won’t update in real time.
  • Hardcoding a design value instead of reading from the store → the feature won’t respond to builder changes.
  • Forgetting npm run build:dev after editing → the dashboard Live Preview continues loading the stale artifact.
  • Adding a VITE_* env var that should be in the payload → the value won’t be per-finder; every visitor on every finder gets the same value.

Where to next