A simple static holiday planning map built with Astro, TypeScript, Leaflet, and OpenStreetMap.
The app loads trip data from a CSV you upload in the browser, stores that CSV locally in localStorage, and can scrape supported listing links online when hosted on Netlify.
- Install dependencies with
npm install. - Start development with
npm run dev. - Run unit tests with
npm test. - Run browser tests with
npm run test:e2e. - Build the static site with
npm run build. - Import listing links with
npm run import:links -- --help.
If your shell does not expose Node directly, load it first with:
source ~/.nvm/nvm.sh
nvm usePrepare a CSV with this fixed header set. You can use src/data/locations.csv as an example input file for imports or manual editing:
title,type,description,latitude,longitude,link,photolinkis optional and may be empty.photois optional and may be empty.- Rows missing
titleortypeare skipped. - Rows without usable coordinates stay in the CSV and appear in a
Needs reviewlist in the sidebar.
The site no longer bundles location data into the page.
- Use the
Upload CSVcontrol in the sidebar to load a file into the app. - Use
Add linkto scrape a supported URL online and merge the result into the local browser CSV. - Use
Download CSVto export the current browser-local CSV with a dated filename. - Use
Share mapto create a public read link plus a private edit link when the local CSV contains at least one valid mapped location. - The selected CSV is stored only in your browser's
localStorageand restored on reload. - Use
Clear local datato remove the saved browser copy. - Uploaded/imported data stays in the browser until you replace or clear it.
- The sidebar header stays sticky while the sidebar content scrolls.
- Mapped locations and
Needs reviewrows scroll together in one shared sidebar scroll area. - Sidebar rows use compact cards with a media preview.
- The media preview shows
photowhen present and falls back to the location type emoji when the image is missing or fails. - Map popups show the
photoat the top when available. - On mobile, the map becomes the primary full-screen canvas and the location list moves into a collapsed bottom sheet.
- On mobile, location details open in a dedicated bottom sheet instead of a Leaflet popup.
- On mobile, upload/share/import/download actions move behind a single
Actionsbutton.
The project includes a local importer that scrapes pasted URLs, extracts metadata, and writes CSV rows.
The Netlify-hosted app also exposes /api/import-link so the website can scrape one supported link at a time and merge it into the browser-local CSV.
Website import behavior:
- Complete imports auto-save immediately when
title,type, valid coordinates, andlinkare present. - Incomplete imports open a review modal before saving.
- Imported rows are merged into the current browser-local CSV state.
Share-by-link uses Supabase-backed API routes.
- Public shared routes use
/map/{shareId}and render the shared map in read-only mode with onlyDownload CSVexposed. - Private edit links use
/map/{shareId}?edit={secret}and unlock upload, link import, clear, andUpdate shared mapfor that shared map. - Shared-map updates replace the server-stored CSV content and keep the same public and private URLs.
- Anonymous share creation is server-side rate-limited per client IP to reduce abuse of the public share endpoint.
When a page exposes a main image in metadata, the importer stores it in the photo column so the map popup can show it.
Interactive review mode:
npm run import:links -- --interactive src/data/locations.csvInteractive mode lets you:
- paste many URLs first, one per line
- auto-add rows when scraping succeeds completely
- review only rows that need manual input or correction
- edit any field before saving
- manually enter coordinates when scraping does not find them
- add rows without coordinates so they appear in
Needs review - manually create a row when a URL import fails
- choose at the end whether to replace the CSV or add lines to it
Use --dry-run first to review inferred data before writing:
npm run import:links -- --append --output src/data/locations.csv --dry-run --urls "https://www.inatur.no/hytte/62472aa2869f5c06c9506875 https://www.finn.no/reise/feriehus-hytteutleie/ad.html?finnkode=186297216"Create a new CSV file instead of appending:
npm run import:links -- --create --output data/new-trip.csv --input urls.txt --dry-runWrite the results after reviewing the dry run:
npm run import:links -- --append --output src/data/locations.csv --urls "https://www.inatur.no/hytte/62472aa2869f5c06c9506875"Importer behavior:
- The importer uses a generic extraction pipeline plus auto-discovered source strategies from
src/lib/importer/strategies/. - Specialized strategies currently exist for
inatur.no,finn.no,booking.com, and Google Maps links likemaps.app.goo.glandgoogle.com/maps/.... - To support another source such as
airbnb.com, add a new top-level strategy file insrc/lib/importer/strategies/. - It tries to extract coordinates from the page first.
- It only calls OpenStreetMap Nominatim when direct coordinates are not available.
- It infers
typefrom structured data and content heuristics. - It deduplicates by normalized
linkby default. - If coordinates still cannot be resolved, the row is written with blank
latitudeandlongitudeso it shows up inNeeds review. - Some sites may block scraping or return partial content. Those rows can still be imported, but often need manual cleanup.
Examples of specialized sources:
npm run import:links -- --dry-run --urls "https://maps.app.goo.gl/13XC3V4FeEbZSvet6"npm run import:links -- --dry-run --urls "https://www.finn.no/reise/feriehus-hytteutleie/ad.html?finnkode=186297216"The app is intended to be deployed on Netlify.
Repo-side deployment config:
netlify.tomldefines the build command, publish directory, function bundling, and Node version..github/workflows/ci.ymlruns unit tests, Playwright tests, and the Astro build on pushes and pull requests.
Recommended Netlify setup:
- Create a new Netlify site from this GitHub repository.
- Use the default production branch (
main). - Keep the build command as
npm run build. - Keep the publish directory as
dist. - Let Netlify read
netlify.tomlfor Node version and function bundling. - Netlify's default deploy URL is used automatically.
- Set
PUBLIC_SITE_URLonly if you want to override it with a custom domain.
Useful environment variables:
PUBLIC_SITE_URL="https://your-site.netlify.app" \
npm run buildOn Netlify itself, this variable is optional because the build falls back to Netlify's built-in deploy URL environment variables.
Share-map backend environment variables:
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_SECRET_KEY="your-secret-key"Hosted Supabase also needs the full repo migration set from supabase/migrations/ applied, including the shared-map schema, RPCs, and permissions in the initial shared-map migration.
The repo now includes a local Supabase project in supabase/ for real share-map testing.
Common local commands:
npx supabase start
npx supabase db reset --local
npm run test:shared-maps
npm run test:e2e:share:live
npm run verify:local-supabaseNotes:
scripts/with-local-supabase-env.mjsreadsnpx supabase status -o envand maps the local values toSUPABASE_URLandSUPABASE_SECRET_KEYautomatically.npm run test:shared-mapsruns real integration tests against the localmapsandlocationstables.npm run test:e2e:share:liveruns the share flow through the Astro app and real local Supabase instead of stubbing the API.npm run verify:local-supabaseruns the local Supabase start/reset plus both real verification commands in sequence.- The mocked Playwright suite in
npm run test:e2eremains useful for fast frontend regression coverage. - Shared-map writes now run through database RPC functions so create/update is atomic.
- Hosted Supabase should apply all repo migrations so RLS, RPC functions, and stable shared-location ordering are present.
Recommended deploy flow:
- Pull requests get GitHub CI plus Netlify preview deploys.
- Merges to
maintrigger the production deploy. - If a deploy regresses, use Netlify's previous deploy rollback.