尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Rider App
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
A UK motorcycle route guide — curated routes, day rides and multi-day tours with honest difficulty and bike-suitability ratings, GPX export, and hand-off to Google/Apple Maps. Riders can save rides, keep a diary and share them. Made by Dead Cylinder Co.
Built with Next.js (App Router), TypeScript, Tailwind CSS, PostgreSQL (Drizzle ORM), Auth.js and MapLibre GL.檢視完整 README ↗A UK motorcycle route guide — curated routes, day rides and multi-day tours with honest difficulty and bike-suitability ratings, GPX export, and hand-off to Google/Apple Maps. Riders can save rides, keep a diary and share them. Made by Dead Cylinder Co.
Built with Next.js (App Router), TypeScript, Tailwind CSS, PostgreSQL (Drizzle ORM), Auth.js and MapLibre GL.
/advice): guides on maintenance, camping, tool kits and gear, written in admin (drafts stay hidden until published). npm run db:advice-starters loads three starter drafts to edit./advertise): businesses send an enquiry (stored under Admin > Advertising, with a hidden-field bot trap and a limit of three per email per day). There are no payments: you reply by email, then run a sponsored listing by ticking Sponsored on that business's place under Places. Sponsored places show a clear "Sponsored" label on day ride and tour pages. The page wording is editable under Site content./faq#guidelines, /guidelines redirects there); the rules are in src/lib/guidelines.ts./blog): signed-in riders write posts (optional cover photo and link to a ride, ticking the community guidelines). Nothing is public until a moderator approves it in Admin > Blog posts, where you can approve, reject with a note the rider sees, take a live post down, or handle reports. Editing a live post sends it back for review, and each rider can have three posts waiting at once./admin): routes, day rides, tours, places, landmarks, regions, collections, advice articles, place tips, search chips, Site content (edit page wording and images), contact messages, users (free/premium tier), and the Premium waitlist.src/components/layout/account-menu.tsx) open a panel from the right with Profile, Your rides, Saved rides, Settings and Sign out.You need Node 20+ and a PostgreSQL database.
npm install
Create .env.local in the project root (it is git-ignored):
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/herepath
ADMIN_PASSWORD=choose-a-password
ADMIN_SESSION_SECRET=<long random string>
AUTH_SECRET=<long random string>
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Generate the two secrets with npx auth secret or openssl rand -base64 32.
Then set up the database and start the app:
npm run db:migrate # create the tables
npm run db:seed # load the sample routes, day rides and tours (wipes existing content)
npm run dev # http://localhost:3000
Admin lives at /admin; log in with ADMIN_PASSWORD.
| Variable | Required | What it does |
|---|---|---|
DATABASE_URL | yes | PostgreSQL connection string |
ADMIN_PASSWORD | yes | Password for /admin — use a strong one in production |
ADMIN_SESSION_SECRET | yes | Secret for the admin login cookie |
AUTH_SECRET | yes | Secret for rider sign-in sessions |
NEXT_PUBLIC_SITE_URL | yes | Public address of the site (used in share links and metadata) |
NEXT_PUBLIC_MAP_STYLE_URL | production | MapLibre style URL from a tile provider (MapTiler, Stadia…) including its API key. Without it the app falls back to OpenStreetMap's public tiles, which aren't allowed for production traffic. If it's a MapTiler URL (…/maps/streets-v2/style.json?key=…), riders also get a Map / Contour Map / Satellite switch on the maps (the outdoor-v2 and hybrid styles, same key); otherwise the switch is hidden |
BLOB_STORE_ID or BLOB_READ_WRITE_TOKEN | production | Set automatically when you connect a Vercel Blob store to the project (Storage tab). Without one, uploads save to public/uploads locally and fail on Vercel |
CRON_SECRET | production | Any long random string. Vercel sends it with the daily job that refreshes the fuel, food and stay pins (vercel.json); without it that job is refused |
ORS_API_KEY | production | A free key from openrouteservice.org. Turns on real road routing between routes in the trip planner (see Content). Without it the planner keeps its straight-line estimates in production |
ROUTING_PROVIDER | no | ors, osrm or none. Defaults to ors when ORS_API_KEY is set; otherwise the public OSRM demo server outside production (for testing only) and no routing in production |
ROUTING_DAILY_LIMIT | no | The most new road routes to ask the routing service for in a day. Default 2000, under openrouteservice's free 2,500 |
Schema lives in src/db/schema/. After changing it:
npm run db:generate # writes a new migration into drizzle/
npm run db:migrate # applies it
To run migrations or the seed against another database (for example the hosted one), set DATABASE_URL in the shell first. It takes priority over .env.local:
$env:DATABASE_URL = "postgresql://..."
npm run db:migrate
npm run db:seed deletes existing routes, day rides, tours and related content before inserting the samples, so don't run it against a database holding real content.
src/lib/site-content.ts./api/places-along through the free Overpass service and saved in the osm_place_cache table, so only the very first request for a ride can wait 10 to 25 seconds; Admin → Places → Preload does that for every ride, and a daily Vercel cron, /api/cron/refresh-places, re-asks the oldest answers older than 10 days, a few rides each day, using its 60 second limit. Riders are never shown an answer older than a month if OpenStreetMap can be reached, and if it can't they still get the old answer) and your own places in Admin → Places. A place needs a latitude and longitude to appear; tick Sponsored to label it and ring its pin. An OpenStreetMap place within 80 m of one of yours is treated as the same place and hidden.db:migrate (and db:seed if you want the sample content) against the hosted database.main.Vercel limits request bodies to about 4.5MB, so very large photo uploads will fail there.
The Android app is a Capacitor shell around the live website (capacitor.config.ts); the project is in android/. It loads https://herepath.vercel.app, so website changes reach the app without a new release. mobile-web/ holds only the offline fallback page. The app adds HerepathApp to its user agent, which the site uses to hide the "Get the app" badges inside the app. The site sends viewport-fit=cover and pads the header and bottom bar by the safe-area insets, so on Android the page's own colour fills the status bar; NativeSystemBars then sets the status/gesture bar icon colour from the rider's Light/Dark choice. These are website changes, so no new app build is needed.
The one piece of app-specific native code is android/app/src/main/java/com/herepath/SocialSharePlugin.java, which the Instagram and TikTok screens in the share pop-up use to send the ride image straight into those apps (skipping the share sheet). It only exists in app builds made after it was added; older builds, or phones without the app, fall back to the normal share sheet automatically.
To build and run it you need Android Studio (it installs Java and the Android SDK).
npm run android:sync # after changing capacitor.config.ts or adding a plugin
npm run android:open # opens the project in Android Studio; press Run to start it on an emulator or phone
To test against the dev server on this computer instead of the live site, start npm run dev and sync with the emulator's address for this computer:
$env:CAP_SERVER_URL = "http://10.0.2.2:3000"
npm run android:sync
Run npm run android:sync again without that variable before building a release. The app id (com.herepath) is permanent once published to Google Play.
npm run dev # dev server
npm run build # production build
npm run lint # ESLint
npx tsc --noEmit # type-check
npm run db:studio # browse the database
NATIONAL_HIGHWAYS_API_KEY | production | A free key from developer.data.nationalhighways.co.uk (subscribe to Road and Lane Closures). Turns on road closure warnings. Without it no closures are shown |
PREMIUM_LIVE | no | Set to on to launch Premium: riders can subscribe and the free-plan limits apply. Leave it unset and the site behaves as if there were no plans (nothing limited, no buying, Premium shows a waitlist) |
STRIPE_SECRET_KEY | for payments | Stripe secret key (sk_test_… for testing, sk_live_… for real). Without it nobody can subscribe |
STRIPE_WEBHOOK_SECRET | for payments | The signing secret (whsec_…) of the Stripe webhook that points at <site>/api/stripe/webhook |
NEXT_PUBLIC_DEAD_CYLINDER_URL | no | Link target for the "Dead Cylinder Co." mentions |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | no | Enables Google sign-in. Redirect URI: <site>/api/auth/callback/google |
src/lib/route-import.tsapplyDetailsCsvbuildDetailsTemplatesrc/lib/csv.tssrc/lib/bike-types.tssrc/lib/closures.ts, src/lib/closures/national-highways.ts): National Highways' Road and Lane Closures feed (motorways and major A roads in England) is read into the road_closures table for the next 30 days, keeping only closures where the road itself is shut (not lane closures or slip roads). A page that shows closures reads the feed again after it has been sent if what we hold is over 30 minutes old (at most one read every 5 minutes), and a daily cron (/api/cron/refresh-closures) is a backstop. A closure counts as being on a ride when at least 300 m of it (or half, if shorter) runs within 50 m of the ride's line (src/lib/closure-match.ts), so a ride that just crosses a closed road doesn't count. Matches show as a warning on route, day ride and tour pages and on the trip planner (red on the map, a badge in the route list, a warning for the trip). The feed gives only an overall start and end, not the hours, so long closures are labelled as possibly overnight only. The API allows 10 calls a minute per key; a refresh uses 2.src/lib/plans.ts, src/lib/membership.ts, src/lib/billing.ts, src/app/billing/actions.ts, src/app/api/stripe/webhook/route.ts): Free, Premium (£2.99 a month or £24.99 a year, charged through Stripe Checkout) and Premium Plus (shown as coming soon, with its own waitlist). Prices, feature lists and limits are all in plans.ts, so the pricing page and the checkout can't disagree. Stripe's webhook keeps the subscriptions table in step; a rider's level is the higher of what an admin set by hand (Admin → Users) and any paid subscription, so ending a subscription never takes away a level someone was given. Cancelling is done on Stripe's customer portal (Settings → Manage billing), and deleting an account cancels its subscription first. Nothing is limited or on sale until PREMIUM_LIVE=on. Free-plan limits: one saved trip, 20 diary entries with 3 photos each, no whole-tour GPX or printable tour sheet, no GPX import into the diary, and no roads between routes in a trip's GPX. Setting Stripe up: create a Stripe account, put its keys in the environment variables above, add a webhook endpoint for checkout.session.completed, customer.subscription.created, customer.subscription.updated and customer.subscription.deleted, and turn on the customer portal in Stripe's settings. Test with sk_test_ keys and Stripe's test card 4242 4242 4242 4242 before switching to live keys./plan): riders join published routes into their own day ride or tour, split it into days, ask for round trips from a start point, save up to 10 trips (saved_trips table), and download a GPX. A start point can be found by searching an address, postcode or place name (/api/geocode: postcodes.io for postcodes, MapTiler geocoding for other places using the key in NEXT_PUBLIC_MAP_STYLE_URL, or OpenStreetMap search when there is no MapTiler key), tapped on the map, or taken from the phone's location. The stretches between routes are real road routes when a routing service is set up (/api/route, src/lib/routing.ts: openrouteservice by default, swappable by adding a RoutingProvider; answers are cached in road_route_cache and at Vercel's edge, and the GPX includes them), and straight-line estimates when it isn't or can't find the road (src/lib/trip-planner.ts), not roads. Real road routing and a round-trip generator are scoped in docs/ROADMAP.md.src/db/sample-geometries.json, generated once by scripts/generate-snapped-geometries.ts. Uploading a real GPX to a route in admin replaces it.