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.designJSON (orsettingsarray). The widget reads it fromdesignStoreon 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 barFor 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 HMRSet 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.jsThen 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:02This 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
payloadStoreinstead ofdesignStore→ the Live Preview bridge updatesdesignStoreseparately frompayloadStore; reading onlypayloadStoremeans 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:devafter 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
- Backend changes for this feature: add-feature-backend
- Dashboard builder controls for this feature: add-customization-option
- Ship to production: deploy