Architecture
System overview
βββββββββββββββββββββββββββββββββββββββ
β WordPress (bttpirineus.org) β
β β
β PΓ gina amb [map gpx="ruta.gpx"] β
β β β
β Plugin PHP β iframe src="..." β
ββββββββββββββββββ¬βββββββββββββββββββββ
β iframe request
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Next.js app (Vercel) β
β β
β /embed?gpx=https://...ruta.gpx β
β β β
β EmbedClient.tsx (orchestrator) β
β β fetches /api/gpx?url=... β
β β parseGpx() β enrichGpxData() β
β β passes enriched data to: β
β - LeafletMap (map + hover) β
β - ElevationChart (SVG) β
β - SpeedChart (SVG, if times) β
β β analytics via /api/analytics β
β β ShareDropdown + download btn β
β β
β /api/analytics (Supabase Postgres) β
β β POST {gpx, event} β INSERT β
β β GET ?gpx= β COUNT + GROUP BY β
β β
β /analytics (dashboard) β
β β password-protected β
β β PostHog HogQL queries β
βββββββββββββββββββββββββββββββββββββββData flow
- WordPress renders
[map gpx="URL"]β plugin outputs an iframe pointing tohttps://{VERCEL}/embed?gpx={encoded_URL}&chart=auto - EmbedClient fetches GPX via
/api/gpxproxy, parses withparseGpx(), enriches withenrichGpxData()(computes distances, elevation stats, speeds) - LeafletMap receives pre-parsed
enrichedDataandhoveredIndexβ renders track + hover marker synced with charts - ElevationChart / SpeedChart β custom SVG charts, hover fires
onHover(index)back to EmbedClient - Analytics β
useAnalyticshook fires view event on mount, tracks downloads/shares, fetches counters from/api/analytics
Why a proxy?
GPX files are hosted on bttpirineus.org. Browsers block cross-origin requests (CORS). The /api/gpx proxy fetches the GPX server-side and serves it to the browser. It only allows URLs from bttpirineus.org for security.
Routes
| Route | Type | Description |
|---|---|---|
/ | Page | Home β links to helper, analytics, and demo |
/embed | Page | Embeddable map viewer. Query params: gpx, chart, page, anchor, title |
/helper | Page | Shortcode generator (new, legacy reconvert, batch CSV) |
/analytics | Page | Password-protected analytics dashboard |
/demo | Page | Embed preview tool for quick validation |
/docs | Docs | This documentation site |
/api/gpx | API | GPX proxy. ?url= + optional &dl=1 |
/api/analytics | API | POST: record event. GET: ?gpx= β counters |
/api/analytics/dashboard | API | Dashboard data via PostHog HogQL |
/api/analytics/sessions | API | PostHog session recordings |
/api/analytics/ip-check | API | Internal user detection |
Key files
app/
page.tsx Home page (navigation hub)
layout.tsx Root layout (lang="ca", favicon)
embed/
page.tsx Server component β extracts ?gpx from searchParams
EmbedClient.tsx Orchestrator β fetch/parse/enrich, hover state, charts, analytics
helper/
page.tsx Server component
HelperClient.tsx Shortcode generator (3 tabs: new/legacy/batch)
analytics/
page.tsx Server component
AnalyticsClient.tsx Password gate + dashboard UI
components/ Tab components (Overview, Visits, Downloads, Actions, Sessions)
demo/
page.tsx Server component
DemoClient.tsx Embed preview tool
api/
gpx/route.ts GPX proxy β validates host, fetches .gpx
analytics/route.ts Supabase analytics (insert events, count per gpx)
analytics/dashboard/ PostHog HogQL aggregation
analytics/sessions/ PostHog session recordings
analytics/ip-check/ Internal user detection
components/
LeafletMap.tsx Map renderer β enrichedData, hover marker, layer picker
LeafletMapDynamic.tsx Dynamic import wrapper (SSR disabled)
ElevationChart.tsx Custom SVG elevation profile with hover sync
SpeedChart.tsx Custom SVG speed chart (if timestamps)
ShareDropdown.tsx Share popover (Facebook, X, WhatsApp, email, Garmin, QR)
lib/
gpx.ts GPX XML parser β { trackPoints, waypoints }
gpx-enrich.ts Enrichment: haversine distance, speed, elevation stats
analytics-events.ts Event types, share channels, summary logic
use-analytics.ts Analytics hook: dual-write Supabase + PostHog
use-layer-preference.ts localStorage-backed tile layer preference
tile-providers.ts Map tile layer definitions (Hybrid, Topo, IGN)
routes.ts Hard-coded route metadata (12 routes)
posthog-query.ts PostHog HogQL client (server-side)
posthog-server.ts PostHog Node.js client factory
wordpress/
bttpirineus-maps/
bttpirineus-maps.php WordPress plugin β [map] shortcode β iframe
content/ Documentation (MDX, rendered at /docs)Security headers
Configured in next.config.ts:
/embed:Content-Security-Policy: frame-ancestors 'self' https://bttpirineus.org https://www.bttpirineus.orgβ only allows embedding from the BTT Pirineus WordPress site- All other routes:
X-Frame-Options: DENYβ prevents embedding anywhere else