diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index 77d4b63..c5154b9 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -6,12 +6,17 @@ import { layers, namedTheme } from 'protomaps-themes-base'
import { useStore } from '../store'
import { decodePolyline } from '../utils/decode'
import { fetchReverse } from '../api'
+import { getConfig } from '../config'
const ROUTE_SOURCE = 'route-source'
const ROUTE_LAYER_PREFIX = 'route-layer-'
/** Build a full MapLibre style object for the given theme */
function buildStyle(themeName) {
+ const config = getConfig()
+ const tileUrl = config?.tileset?.url || '/tiles/na.pmtiles'
+ const attribution = config?.tileset?.attribution || 'Protomaps \u00a9 OSM'
+
return {
version: 8,
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
@@ -19,9 +24,8 @@ function buildStyle(themeName) {
sources: {
protomaps: {
type: 'vector',
- url: 'pmtiles:///tiles/na.pmtiles',
- attribution:
- 'Protomaps | OSM',
+ url: `pmtiles://${tileUrl}`,
+ attribution,
},
},
layers: layers('protomaps', namedTheme(themeName), { lang: 'en' }),
@@ -68,8 +72,11 @@ const MapView = forwardRef(function MapView(_, ref) {
const protocol = new Protocol()
maplibregl.addProtocol('pmtiles', protocol.tile)
- const DEFAULT_CENTER = [-114.6066, 42.5736]
- const DEFAULT_ZOOM = 10
+ const config = getConfig()
+ const DEFAULT_CENTER = config?.defaults?.center
+ ? [config.defaults.center[1], config.defaults.center[0]] // config is [lat,lon], MapLibre wants [lon,lat]
+ : [-114.6066, 42.5736]
+ const DEFAULT_ZOOM = config?.defaults?.zoom || 10
const initialTheme = document.documentElement.getAttribute('data-theme') || 'dark'
currentThemeRef.current = initialTheme
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..ce0e0a8
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,84 @@
+/**
+ * Deployment config loader.
+ *
+ * Fetches /api/config on startup and caches the result.
+ * Falls back to hardcoded defaults matching the home profile if the
+ * API is unavailable (backend restart, network issue).
+ */
+
+const FALLBACK_CONFIG = {
+ profile: 'home',
+ region_name: 'North America',
+ tileset: {
+ url: '/tiles/na.pmtiles',
+ bounds: [-168, 14, -52, 72],
+ max_zoom: 15,
+ attribution: 'Protomaps © OSM',
+ },
+ services: {
+ geocode: '/api/geocode',
+ reverse: '/api/reverse',
+ address_book: '/api/address_book',
+ valhalla: '/valhalla',
+ },
+ features: {
+ has_nominatim_details: false,
+ has_kiwix_wiki: false,
+ has_hillshade: false,
+ has_3d_terrain: false,
+ has_traffic_overlay: false,
+ has_landclass: false,
+ has_address_book_write: false,
+ },
+ defaults: {
+ center: [42.5736, -114.6066],
+ zoom: 10,
+ },
+}
+
+let _config = null
+let _configPromise = null
+
+/**
+ * Fetch config from backend. Returns cached config on subsequent calls.
+ * Falls back to FALLBACK_CONFIG if API fails.
+ */
+export function loadConfig() {
+ if (_configPromise) return _configPromise
+
+ _configPromise = fetch('/api/config', { signal: AbortSignal.timeout(3000) })
+ .then((resp) => {
+ if (!resp.ok) throw new Error(`Config API returned ${resp.status}`)
+ return resp.json()
+ })
+ .then((data) => {
+ _config = data
+ console.log('[navi] Config loaded:', data.profile, `(${data.region_name})`)
+ console.log('[navi] Feature flags:', data.features)
+ return data
+ })
+ .catch((err) => {
+ console.warn('[navi] Config API unavailable, using fallback:', err.message)
+ _config = FALLBACK_CONFIG
+ return FALLBACK_CONFIG
+ })
+
+ return _configPromise
+}
+
+/**
+ * Get the current config synchronously. Returns null if not yet loaded.
+ */
+export function getConfig() {
+ return _config
+}
+
+/**
+ * Check a feature flag from the loaded config.
+ * @param {string} flag - Feature flag name (e.g. 'has_hillshade')
+ * @returns {boolean}
+ */
+export function hasFeature(flag) {
+ if (!_config) return false
+ return Boolean(_config.features?.[flag])
+}
diff --git a/src/hooks/useConfig.js b/src/hooks/useConfig.js
new file mode 100644
index 0000000..006e37d
--- /dev/null
+++ b/src/hooks/useConfig.js
@@ -0,0 +1,29 @@
+import { useState, useEffect } from 'react'
+import { loadConfig, getConfig, hasFeature } from '../config'
+
+/**
+ * Hook that returns the deployment config, loading it if needed.
+ * Components using this will re-render once config is loaded.
+ */
+export function useConfig() {
+ const [config, setConfig] = useState(getConfig)
+
+ useEffect(() => {
+ if (!config) {
+ loadConfig().then(setConfig)
+ }
+ }, [config])
+
+ return config
+}
+
+/**
+ * Hook to check a single feature flag.
+ * @param {string} flag - e.g. 'has_hillshade'
+ * @returns {boolean}
+ */
+export function useFeature(flag) {
+ const config = useConfig()
+ if (!config) return false
+ return Boolean(config.features?.[flag])
+}
diff --git a/src/main.jsx b/src/main.jsx
index ef272ce..d452a6d 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,23 +1,27 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Toaster } from 'react-hot-toast'
+import { loadConfig } from './config'
import './index.css'
import App from './App.jsx'
-createRoot(document.getElementById('root')).render(
-
-
-
- ,
-)
+// Load deployment config before rendering — non-blocking (fallback kicks in on failure)
+loadConfig().then(() => {
+ createRoot(document.getElementById('root')).render(
+
+
+
+ ,
+ )
+})