Skip to content

Repository files navigation

fieldWorks

An offline-first PWA for handheld walking tours — hillforts, henges, barrows, and heritage trails. Tours are authored in Markdown, work without a network connection once installed, and install directly to an iOS or Android home screen.

Built with Svelte 5 · Vite 7 · vite-plugin-pwa / Workbox · three-way Light / Dark / Night theming.


Quick start

Requires Node.js 20+.

npm install          # install dependencies
npm run demo:seed    # generate placeholder media for the demo tour
npm run dev          # dev server at http://localhost:5173
npm test             # run unit tests (vitest)
npm run check        # type-check (svelte-check + tsc)
npm run build        # production build → dist/
npm run preview      # serve dist/ locally to test PWA

Or in one command: npm run demo (seed + dev server).


Project structure

content/                      # Tour content — edit this to add tours
├── routes/
│   └── cissbury-ring/
│       ├── tour.yaml         # Route manifest (stop order, metadata)
│       └── stops/
│           ├── cissbury-entrance.md
│           └── cissbury-summit.md

public/                       # Static assets copied verbatim to dist/
├── CNAME                     # Custom domain — do not delete
├── icon-192x192.png
├── icon-512x512.png
└── tours/
    └── cissbury-ring/        # Demo media — gitignored, generated by npm run demo:seed
        └── …                 # Real tour media goes here and IS committed

src/
├── styles/
│   └── brand.css             # All colour + font tokens (edit to rebrand)
├── lib/
│   ├── content/
│   │   └── vite-plugin.ts    # Build-time content pipeline + base-path rewriting
│   ├── geo/
│   │   └── store.ts          # Geolocation + Haversine proximity
│   ├── theme/
│   │   └── store.ts          # Light / Dark / Night theme store
│   ├── types.ts              # Shared TypeScript types
│   ├── TourLibrary.svelte    # Home screen — tour cards
│   ├── RouteMap.svelte       # Route overview — inline SVG map
│   ├── TourStop.svelte       # Stop screen — content + media + proximity
│   ├── HengeLogo.svelte      # Brand mark
│   └── ThemeToggle.svelte    # Light → Dark → Night toggle
├── App.svelte                # Hash router + visited-stops tracking
└── main.ts                   # Entry point + PWA registration

.github/workflows/deploy.yml  # GitHub Pages auto-deploy (push to main)
scripts/seed-demo.mjs         # Generates placeholder media for the demo tour
vite.config.ts                # Vite + content plugin + PWA config

Authoring a tour / whitelabel branding

See AUTHORING.md for the full step-by-step guide covering:

  • Setting your app identity (name, manifest, title)
  • Rebranding in one file (src/styles/brand.css — Light, Dark, Night token sets)
  • Tour folder layout, tour.yaml fields, stop frontmatter reference
  • Inline media syntax (![caption](file.ext) → right element by extension)
  • Demo tour: npm run demo:seed generates placeholder media for every type (image, audio, video, 3D)

Themes

Three themes — Light, Dark, Night — toggled by the sun/moon button in the header. Night mode uses red-amber only to preserve dark-adapted vision in the field.

All colour and font values live in src/styles/brand.css. Edit that file to rebrand; no component files need to change. The active theme persists to localStorage.


Deployment

The build outputs a static site in dist/. It works on any static host.

Base-path awareness

The default build serves from / (root). If you ever need to host under a sub-path, set BASE_PATH at build time — the content pipeline will prefix all media paths automatically:

# Root-domain or custom subdomain (default)
npm run build

# Sub-path host (e.g. /myapp/ on a shared server)
BASE_PATH=/myapp/ npm run build

GitHub Pages (auto-deploy)

A workflow is included at .github/workflows/deploy.yml. It triggers on every push to main, generates demo media, builds, and deploys to Pages.

One-time setup: go to Settings → Pages → Source → GitHub Actions.

Custom domain: add your domain in Settings → Pages → Custom domain, and ensure a CNAME file containing your domain name exists in the public/ folder (already present as public/CNAME). This prevents the custom domain from being wiped on each deploy.

After that, every git push origin main deploys automatically.

Netlify

Drag-and-drop (instant, no account connection needed):

npm run demo:seed && npm run build
# Drop the dist/ folder onto app.netlify.com/drop

Repo-connected auto-deploy:

  • Build command: npm run demo:seed && npm run build
  • Publish directory: dist
  • No base-path override needed (Netlify serves at root).

No _redirects file is needed — the PWA uses hash routing.

Vercel

Drag-and-drop:

npm run demo:seed && npm run build
npm i -g vercel
vercel --prod   # point at the dist/ folder when prompted

Repo-connected: connect the GitHub repo in the Vercel dashboard, build preset Vite, build command npm run demo:seed && npm run build. No base-path override needed.


Installing to a phone

Once deployed, any visitor can install the app to their home screen. It then launches full-screen with no browser chrome, and works completely offline.

iOS (Safari)

  1. Open the tour URL in Safari (Chrome on iOS cannot install PWAs).
  2. Tap the Share button (the box with an upward arrow) in the bottom toolbar.
  3. Scroll down and tap Add to Home Screen.
  4. Edit the name if you like, then tap Add.

The app icon appears on the home screen. Open it to trigger the first offline cache (this requires a connection). After that, it works without signal.

Tip for tour leaders: Share the URL with participants before the walk and ask them to install it on Wi-Fi. The app caches all stops, fonts, and media — around 1–3 MB for a typical tour.

Android (Chrome)

  1. Open the tour URL in Chrome.
  2. Tap the ⋮ menu (top-right) and choose Add to Home screen (or Install app).
  3. Tap Install.

On some Android devices Chrome shows an install banner at the bottom of the screen automatically after a few visits.

Testing offline

  1. Install the app or open it in Chrome desktop.
  2. Open DevTools → Application → Service Workers — confirm the SW is registered and active.
  3. Open DevTools → Network → Offline (tick the checkbox).
  4. Reload and navigate — all stops and media should load from cache.

Adding a tour / starting from scratch

See AUTHORING.md → Starting a new tour from scratch for the full skeleton: a minimal tour.yaml, a stub stop file with every required/optional frontmatter field annotated, and notes on the media folder.

Quick summary:

  1. Create content/routes/<tour-id>/tour.yaml and stops/<stop-id>.md.
  2. Add media (images, audio, .pmtiles basemap) to public/tours/<tour-id>/commit these.
  3. Rebuild: npm run build && npm run preview.

The tour appears automatically in the library — no code changes needed.

Don't copy the Cissbury folder as a starting point. Its content is a demo and carries demo:seed-generated placeholders. Use the AUTHORING.md skeleton instead.


Importing a survey export

Survey export zips are produced by the companion field-survey app, survey-tool — use it to capture stations (GPS position, notes, photos, voice notes) in the field, then export a session as a zip. If your content comes from that app (session.geojson + photos/ + audio/ per session), you don't have to build the tour skeleton by hand:

npm run tour:import -- ./my-survey-export.zip

You'll be prompted for a human-readable tour name, a route id, an optional subtitle, and an optional one-line tour description, then it generates content/routes/<id>/tour.yaml and one stop .md per surveyed observation. Yes, it imports the photos — every photo for a stop is copied into public/tours/<id>/ as a real file ready to commit (not a placeholder) and appended inline in the stop's body. There's no separate hero image: on phone, the hero plate is a live locator map, not a photo, so a "hero" photo would be invisible where the tour is actually walked — see AUTHORING.md → Importing a survey export for why and for the full field-by-field mapping. Voice notes are reported and skipped (not imported).

Editing what it generates

Nothing it produces is finished — every field that needs prose is a literal TODO, except description if you supplied one at the prompt. Open the generated files and fill in:

  • tour.yamldescription (if you skipped the prompt), subtitle, total_distance, duration, and (once you have a .pmtiles file) uncomment the map: block.
  • Each stop file — rename the placeholder title: "Stop 1" to something real, then fill in era, walk_time, evidence, interpretation, and photo captions. The body opens with the surveyor's field note as a first draft — rewrite it into proper stop prose.

Then npm run check && npm run test — the new route is picked up by the existing content tests automatically — and npm run dev to see it in the library.

Gotchas — renaming things after import

The importer bakes several values into the generated files at import time. Renaming things afterwards can quietly break what it wrote:

  • Renaming the route id (the content/routes/<id> / public/tours/<id> folder name) breaks every generated path — inline photo src and (once uncommented) map.basemap all point at /tours/<old-id>/.... Rename both folders together and find/replace the old id across every stop file.
  • Renaming a stop's id: (e.g. stop-1 → something descriptive) must be mirrored in tour.yaml's stops: list entry and ideally the NN-<id>.md filename — the usual three-places rule for any stop.
  • Renaming a stop's title: (e.g. Stop 1 → a real name) does not rename its id:, its filename, or its photo files — those stay stop-1 / 01-stop-1.md / 01-stop-1-N.jpg regardless of title. This is cosmetic only (the body's photo paths still point at the right file), but can be surprising if you go looking for a file matching the current title.
  • Re-running the importer on the same zip with --force regenerates every file from scratch — it overwrites, it does not merge. Any hand-edits (prose, renamed ids/titles, an uncommented map: block) are lost, and because filenames are derived from ordinal position rather than content, a session that has gained or lost a feature since your last import can shift which photos land under which stop number. Only safe before you've started editing.
  • route_name (the tour's display title) is freely editable with no knock-on effect — nothing else derives from it after generation.

Tests

npm test           # run once
npx vitest         # watch mode
npm run check      # type check only
File Covers
src/lib/geo/store.test.ts Haversine distance · accuracy-aware proximity · createProximityStore
src/lib/content/content.test.ts Windows-safe routeId · YAML manifest parsing · numbered-prefix regex

License

MIT

About

PWA for tour guides

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages