diff --git a/server/src/app.ts b/server/src/app.ts index 619aba55..5482b3bf 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -22,7 +22,7 @@ import { loadTessieRouteCoordinates } from "./tessie-route.js"; declare module "fastify" { interface FastifyRequest { principal: Principal | null } } const googleSiteVerificationPath = "/google8e611381b21882a8.html"; -const publicPaths = new Set(["/healthz", "/readyz", "/login", "/login.html", "/privacy", "/privacy.html", "/apple-music-soundtrack", "/private-driving-journal", "/driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", "/support", "/support.html", "/terms", "/terms.html", "/robots.txt", "/.well-known/api-catalog", "/openapi.json", "/api-docs", "/auth.md", "/sitemap.xml", "/manifest.webmanifest", "/favicon.ico", googleSiteVerificationPath]); +const publicPaths = new Set(["/healthz", "/readyz", "/login", "/login.html", "/privacy", "/privacy.html", "/apple-music-soundtrack", "/private-driving-journal", "/driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", "/driving-history-app", "/support", "/support.html", "/terms", "/terms.html", "/robots.txt", "/.well-known/api-catalog", "/openapi.json", "/api-docs", "/auth.md", "/sitemap.xml", "/manifest.webmanifest", "/favicon.ico", googleSiteVerificationPath]); const publicAuthPaths = new Set(["/api/auth/login", "/api/auth/passkey/options", "/api/auth/passkey/verify"]); const scheduledSyncPath = "/api/spotify/sync"; const securityHeaders = { @@ -305,7 +305,7 @@ export async function createApp(overrides: CreateAppOverrides = {}) { app.get(retiredLandingPath, async (_req, reply) => reply.code(404).header("x-robots-tag", "noindex, nofollow").sendFile("404.html")); } } - app.get("/", async (_req, reply) => reply.sendFile(cfg.mode === "web" ? "beta.html" : "index.html")); app.get("/app", async (_req, reply) => reply.sendFile("index.html")); app.get("/spotify-callback", async (_req, reply) => reply.sendFile("index.html")); app.get("/login", async (_req, reply) => reply.sendFile("login.html")); app.get("/.well-known/api-catalog", async (_req, reply) => reply.type("application/linkset+json").send(await readFile(join(cfg.webRoot, "api-catalog.json")))); app.get("/api-docs", async (_req, reply) => reply.sendFile("api-docs.html")); app.get("/privacy", async (_req, reply) => reply.sendFile("privacy.html")); app.get("/apple-music-soundtrack", async (_req, reply) => reply.sendFile("apple-music-soundtrack.html")); app.get("/private-driving-journal", async (_req, reply) => reply.sendFile("private-driving-journal.html")); app.get("/road-trip-journal-app", async (_req, reply) => reply.sendFile("road-trip-journal-app.html")); app.get("/road-trip-tracker-app", async (_req, reply) => reply.sendFile("road-trip-tracker-app.html")); app.get("/travel-journal-app-with-photos", async (_req, reply) => reply.sendFile("travel-journal-app-with-photos.html")); app.get("/driving-journal", async (_req, reply) => reply.code(301).redirect("/private-driving-journal")); app.get("/support", async (_req, reply) => reply.sendFile("support.html")); app.get("/terms", async (_req, reply) => reply.sendFile("terms.html")); app.get("/wife", async (_req, reply) => reply.sendFile("wife.html")); + app.get("/", async (_req, reply) => reply.sendFile(cfg.mode === "web" ? "beta.html" : "index.html")); app.get("/app", async (_req, reply) => reply.sendFile("index.html")); app.get("/spotify-callback", async (_req, reply) => reply.sendFile("index.html")); app.get("/login", async (_req, reply) => reply.sendFile("login.html")); app.get("/.well-known/api-catalog", async (_req, reply) => reply.type("application/linkset+json").send(await readFile(join(cfg.webRoot, "api-catalog.json")))); app.get("/api-docs", async (_req, reply) => reply.sendFile("api-docs.html")); app.get("/privacy", async (_req, reply) => reply.sendFile("privacy.html")); app.get("/apple-music-soundtrack", async (_req, reply) => reply.sendFile("apple-music-soundtrack.html")); app.get("/private-driving-journal", async (_req, reply) => reply.sendFile("private-driving-journal.html")); app.get("/road-trip-journal-app", async (_req, reply) => reply.sendFile("road-trip-journal-app.html")); app.get("/road-trip-tracker-app", async (_req, reply) => reply.sendFile("road-trip-tracker-app.html")); app.get("/travel-journal-app-with-photos", async (_req, reply) => reply.sendFile("travel-journal-app-with-photos.html")); app.get("/driving-history-app", async (_req, reply) => reply.sendFile("driving-history-app.html")); app.get("/driving-journal", async (_req, reply) => reply.code(301).redirect("/private-driving-journal")); app.get("/support", async (_req, reply) => reply.sendFile("support.html")); app.get("/terms", async (_req, reply) => reply.sendFile("terms.html")); app.get("/wife", async (_req, reply) => reply.sendFile("wife.html")); app.get(googleSiteVerificationPath, async (_req, reply) => reply.type("text/html; charset=utf-8").send(await readFile(join(cfg.webRoot, "google8e611381b21882a8.html"), "utf8"))); app.setNotFoundHandler(async (req, reply) => { const requestPath = req.url.split("?")[0]; diff --git a/server/tests/api.test.ts b/server/tests/api.test.ts index 838afb9a..76edd3da 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -160,13 +160,29 @@ test("public information and discovery pages are accessible without an authentic assert.match(travelPhotos.body, /https:\/\/apps\.apple\.com\/us\/app\/journeydeck\/id6806502526/); assert.match(travelPhotos.body, /rel="canonical" href="https:\/\/journeydeck\.me\/travel-journal-app-with-photos"/); assert.doesNotMatch(travelPhotos.body, /[\u2013\u2014]/); + const drivingHistory = await runtime.app.inject({ method: "GET", url: "/driving-history-app" }); + assert.equal(drivingHistory.statusCode, 200, drivingHistory.body); + assert.match(String(drivingHistory.headers["content-type"]), /text\/html/); + assert.match(drivingHistory.body, /

Driving history app for iPhone<\/h1>/); + assert.match(drivingHistory.body, /

A private history of the roads you choose to keep: routes, places, photos and the songs that played. All on your iPhone.<\/p>/); + assert.match(drivingHistory.body, /THE ROAD \/ FREE/); + assert.match(drivingHistory.body, /Manually start a journey on iPhone to save your route, distance, and drive time on your device/); + assert.doesNotMatch(drivingHistory.body, /ATLAS \/ JOURNEYDECK PLUS/); + assert.match(drivingHistory.body, /JourneyDeck is a personal drive journal, not a DMV driving record, not an insurance report, and not a mileage tax log/); + assert.match(drivingHistory.body, /

Frequently asked questions<\/h2>/); + assert.match(drivingHistory.body, /Is this the same as Apple Maps drive history\?/); + assert.match(drivingHistory.body, /href="\/privacy"/); + assert.match(drivingHistory.body, /href="\/apple-music-soundtrack"/); + assert.match(drivingHistory.body, /https:\/\/apps\.apple\.com\/us\/app\/journeydeck\/id6806502526/); + assert.match(drivingHistory.body, /rel="canonical" href="https:\/\/journeydeck\.me\/driving-history-app"/); + assert.doesNotMatch(drivingHistory.body, /[\u2013\u2014]/); const journalAlias = await runtime.app.inject({ method: "GET", url: "/driving-journal" }); assert.equal(journalAlias.statusCode, 301); assert.equal(journalAlias.headers.location, "/private-driving-journal"); const terms = await runtime.app.inject({ method: "GET", url: "/terms" }); assert.equal(terms.statusCode, 200, terms.body); assert.match(terms.body, /JourneyDeck Terms of Use/i); - for (const page of [privacy, support, soundtrack, journal, roadTripJournal, roadTripTracker, travelPhotos, terms]) { + for (const page of [privacy, support, soundtrack, journal, roadTripJournal, roadTripTracker, travelPhotos, drivingHistory, terms]) { assert.match(page.body, /class="site-header"/); assert.match(page.body, /class="desktop-nav" aria-label="Main navigation"/); assert.match(page.body, /href="\/apple-music-soundtrack"/); @@ -188,6 +204,7 @@ test("public information and discovery pages are accessible without an authentic assert.equal(googleRobotsAllowed(robots.body, "/road-trip-journal-app"), true); assert.equal(googleRobotsAllowed(robots.body, "/road-trip-tracker-app"), true); assert.equal(googleRobotsAllowed(robots.body, "/travel-journal-app-with-photos"), true); + assert.equal(googleRobotsAllowed(robots.body, "/driving-history-app"), true); assert.equal(googleRobotsAllowed(robots.body, "/google8e611381b21882a8.html"), true); assert.equal(googleRobotsAllowed(robots.body, "/login"), true); assert.equal(googleRobotsAllowed(robots.body, "/app"), false); @@ -213,6 +230,7 @@ test("public information and discovery pages are accessible without an authentic assert.match(sitemap.body, /https:\/\/journeydeck\.me\/road-trip-journal-app<\/loc>/i); assert.match(sitemap.body, /https:\/\/journeydeck\.me\/road-trip-tracker-app<\/loc>/i); assert.match(sitemap.body, /https:\/\/journeydeck\.me\/travel-journal-app-with-photos<\/loc>/i); + assert.match(sitemap.body, /https:\/\/journeydeck\.me\/driving-history-app<\/loc>/i); assert.doesNotMatch(sitemap.body, /https:\/\/journeydeck\.me\/(?:beta|app|login)(?:\/|<)/i); assert.doesNotMatch(sitemap.body, /google8e611381b21882a8/i); const googleVerification = await runtime.app.inject({ method: "GET", url: "/google8e611381b21882a8.html" }); @@ -260,6 +278,7 @@ test("hosted root serves the Grand Touring launch page while private routes stay assert.match(landing.body, /id="memories"/); assert.match(landing.body, /class="feature-cta" href="\/travel-journal-app-with-photos">See the travel journal with photos/); assert.match(landing.body, /id="membership"/); + assert.match(landing.body, /class="feature-cta" href="\/driving-history-app">See the driving history app/); assert.match(landing.body, /Ask JourneyDeck/); assert.match(landing.body, /Journey Markers/); assert.match(landing.body, /Start a Journey widget/); @@ -375,11 +394,12 @@ test("public marketing pages expose one title, description, canonical, Open Grap { url: "/road-trip-journal-app", canonical: "https://journeydeck.me/road-trip-journal-app" }, { url: "/road-trip-tracker-app", canonical: "https://journeydeck.me/road-trip-tracker-app" }, { url: "/travel-journal-app-with-photos", canonical: "https://journeydeck.me/travel-journal-app-with-photos" }, + { url: "/driving-history-app", canonical: "https://journeydeck.me/driving-history-app" }, { url: "/support", canonical: "https://journeydeck.me/support" }, { url: "/privacy", canonical: "https://journeydeck.me/privacy" }, { url: "/terms", canonical: "https://journeydeck.me/terms" } ] as const; - const rewrittenDescriptions = new Set(["/support", "/privacy", "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos"]); + const rewrittenDescriptions = new Set(["/support", "/privacy", "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", "/driving-history-app"]); try { for (const page of pages) { const response = await runtime.app.inject({ method: "GET", url: page.url }); diff --git a/server/tests/campaign.test.ts b/server/tests/campaign.test.ts index 46881f57..bff49b74 100644 --- a/server/tests/campaign.test.ts +++ b/server/tests/campaign.test.ts @@ -31,6 +31,7 @@ const publicPages = [ "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", + "/driving-history-app", "/support", "/privacy", "/terms" diff --git a/server/tests/verify-live.test.ts b/server/tests/verify-live.test.ts index cd232384..ffb26b01 100644 --- a/server/tests/verify-live.test.ts +++ b/server/tests/verify-live.test.ts @@ -59,6 +59,7 @@ test("expectations loader reads the checked-in file and rejects incomplete objec "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", + "/driving-history-app", "/support", "/privacy", "/terms" diff --git a/tools/verify-live/FEATURE_MAP.md b/tools/verify-live/FEATURE_MAP.md index 2de8beae..9ccd290e 100644 --- a/tools/verify-live/FEATURE_MAP.md +++ b/tools/verify-live/FEATURE_MAP.md @@ -26,12 +26,13 @@ Public HTML is served by `server/src/app.ts` in web mode. The public route allow | `/road-trip-journal-app` | `web/road-trip-journal-app.html` | `page:/road-trip-journal-app status`, `page:/road-trip-journal-app title`, `page:/road-trip-journal-app meta-description`, `page:/road-trip-journal-app canonical`, `page:/road-trip-journal-app og:title`, `page:/road-trip-journal-app og:description`, `page:/road-trip-journal-app og:url`, `page:/road-trip-journal-app og:image`, `page:/road-trip-journal-app json-ld`, `page:/road-trip-journal-app umami`, `page:/road-trip-journal-app trial-wording` | | `/road-trip-tracker-app` | `web/road-trip-tracker-app.html` | `page:/road-trip-tracker-app status`, `page:/road-trip-tracker-app title`, `page:/road-trip-tracker-app meta-description`, `page:/road-trip-tracker-app canonical`, `page:/road-trip-tracker-app og:title`, `page:/road-trip-tracker-app og:description`, `page:/road-trip-tracker-app og:url`, `page:/road-trip-tracker-app og:image`, `page:/road-trip-tracker-app json-ld`, `page:/road-trip-tracker-app umami`, `page:/road-trip-tracker-app trial-wording` | | `/travel-journal-app-with-photos` | `web/travel-journal-app-with-photos.html` | `page:/travel-journal-app-with-photos status`, `page:/travel-journal-app-with-photos title`, `page:/travel-journal-app-with-photos meta-description`, `page:/travel-journal-app-with-photos canonical`, `page:/travel-journal-app-with-photos og:title`, `page:/travel-journal-app-with-photos og:description`, `page:/travel-journal-app-with-photos og:url`, `page:/travel-journal-app-with-photos og:image`, `page:/travel-journal-app-with-photos json-ld`, `page:/travel-journal-app-with-photos umami`, `page:/travel-journal-app-with-photos trial-wording` | +| `/driving-history-app` | `web/driving-history-app.html` | `page:/driving-history-app status`, `page:/driving-history-app title`, `page:/driving-history-app meta-description`, `page:/driving-history-app canonical`, `page:/driving-history-app og:title`, `page:/driving-history-app og:description`, `page:/driving-history-app og:url`, `page:/driving-history-app og:image`, `page:/driving-history-app json-ld`, `page:/driving-history-app umami`, `page:/driving-history-app trial-wording` | | `/support` | `web/support.html` | `page:/support status`, `page:/support title`, `page:/support meta-description`, `page:/support canonical`, `page:/support og:title`, `page:/support og:description`, `page:/support og:url`, `page:/support og:image`, `page:/support json-ld`, `page:/support umami`, `page:/support trial-wording` | | `/privacy` | `web/privacy.html` | `page:/privacy status`, `page:/privacy title`, `page:/privacy meta-description`, `page:/privacy canonical`, `page:/privacy og:title`, `page:/privacy og:description`, `page:/privacy og:url`, `page:/privacy og:image`, `page:/privacy json-ld`, `page:/privacy umami`, `page:/privacy trial-wording` | | `/terms` | `web/terms.html` | `page:/terms status`, `page:/terms title`, `page:/terms meta-description`, `page:/terms canonical`, `page:/terms og:title`, `page:/terms og:description`, `page:/terms og:url`, `page:/terms og:image`, `page:/terms json-ld`, `page:/terms umami`, `page:/terms trial-wording` | | `/login` | `web/login.html` | `page:/login status`, `page:/login umami` | -JSON-LD blocks live in `web/beta.html`, `web/apple-music-soundtrack.html`, `web/private-driving-journal.html`, `web/road-trip-journal-app.html`, `web/road-trip-tracker-app.html`, `web/travel-journal-app-with-photos.html`, and `web/support.html`. Privacy and terms have none today. Every `application/ld+json` block that is present must parse. +JSON-LD blocks live in `web/beta.html`, `web/apple-music-soundtrack.html`, `web/private-driving-journal.html`, `web/road-trip-journal-app.html`, `web/road-trip-tracker-app.html`, `web/travel-journal-app-with-photos.html`, `web/driving-history-app.html`, and `web/support.html`. Privacy and terms have none today. Every `application/ld+json` block that is present must parse. ## Umami tag diff --git a/tools/verify-live/expectations.json b/tools/verify-live/expectations.json index 2d50e6c0..45e39103 100644 --- a/tools/verify-live/expectations.json +++ b/tools/verify-live/expectations.json @@ -6,6 +6,7 @@ "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos", + "/driving-history-app", "/support", "/privacy", "/terms" diff --git a/web/apple-music-soundtrack.html b/web/apple-music-soundtrack.html index c6c22952..40ddb014 100644 --- a/web/apple-music-soundtrack.html +++ b/web/apple-music-soundtrack.html @@ -108,7 +108,7 @@

Is JourneyDeck a navigation app?

No. Set up recording before driving and do not interact with the app while operating a vehicle.

- + diff --git a/web/beta.html b/web/beta.html index 24490290..5847b22d 100644 --- a/web/beta.html +++ b/web/beta.html @@ -124,7 +124,7 @@

Take the road.
Keep the story.<
-

ATLAS / JOURNEYDECK PLUS

See the pattern.
Find your perspective.

Meet the bigger picture behind your everyday drives. Atlas connects your routes, familiar places, and listening habits into insights calculated on your device.

Route DNADriving rhythmsSoundtrack insights

JourneyDeck Plus unlocks Atlas and your complete locally stored history. Free recording includes today’s journeys.

+

ATLAS / JOURNEYDECK PLUS

See the pattern.
Find your perspective.

Meet the bigger picture behind your everyday drives. Atlas connects your routes, familiar places, and listening habits into insights calculated on your device.

Route DNADriving rhythmsSoundtrack insights

JourneyDeck Plus unlocks Atlas and your complete locally stored history. Free recording includes today’s journeys.

See the driving history app

NEW IN 4.0

More ways to
keep the story.

diff --git a/web/driving-history-app.html b/web/driving-history-app.html new file mode 100644 index 00000000..813e8c3e --- /dev/null +++ b/web/driving-history-app.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Driving history app for iPhone | JourneyDeck + + + + + + +
+
+

YOUR PERSONAL DRIVE JOURNAL

+

Driving history app for iPhone

+

A private history of the roads you choose to keep: routes, places, photos and the songs that played. All on your iPhone.

+ +
+
+

THE ROAD / FREE

Every journey, beautifully kept.

Manually start a journey on iPhone to save your route, distance, and drive time on your device. No account required.

+
JourneyDeck 4.0 screenshot showing a saved journey, route map, soundtrack, and replay control
A saved journey in JourneyDeck 4.0
+
+
+
+

A personal drive log

+

If you searched for a driving history app or a drive log app, JourneyDeck keeps a journal you start by hand. You decide which journeys to record. The archive starts on your iPhone.

+
+
+

How a drive gets into your history

+
    +
  1. Download JourneyDeck and allow Location when you want route recording.
  2. +
  3. Tap Start Journey before you set off, or start from the Start a Journey widget, Siri, or Apple Watch.
  4. +
  5. When parked, end the journey. Explore the route on your device.
  6. +
  7. Optionally connect Apple Music for a soundtrack, or group drives into a Memory with photos and notes.
  8. +
+
+
+

What you can look back on

+
    +
  • Route, distance, and drive time for journeys you recorded.
  • +
  • Places and patterns Atlas can show when JourneyDeck Plus is active, calculated on your device.
  • +
  • Photos, notes, and an optional soundtrack kept with the drive.
  • +
  • Today's journeys in the free experience. Complete locally stored history with JourneyDeck Plus. Older local journeys are not deleted if membership expires.
  • +
+

Read the private driving journal overview and the privacy policy for how the archive stays on your device.

+
+
+

Frequently asked questions

+

What is a driving history app?

+

A private history of the roads you choose to keep: routes, places, photos and the songs that played. All on your iPhone.

+

Is this the same as Apple Maps drive history?

+

No. JourneyDeck is not Apple Maps. It is a separate iPhone driving journal. You tap Start Journey when you want to keep a drive, then reopen that journal on your device.

+

Is JourneyDeck a DMV record or mileage tax log?

+

No. JourneyDeck is a personal drive journal, not a DMV driving record, not an insurance report, and not a mileage tax log.

+

Does it log every drive automatically?

+

No. You tap Start Journey when you want to keep a drive. Tesla owners can optionally connect Tessie to import vehicle drive routes. That requires JourneyDeck Plus and Tessie access.

+

Can I look back at older drives?

+

Free recording includes today's journeys. JourneyDeck Plus unlocks Atlas and your complete locally stored history. Older local journeys are not deleted if membership expires.

+
+
+ +
+ + diff --git a/web/sitemap.xml b/web/sitemap.xml index de929126..4b48953d 100644 --- a/web/sitemap.xml +++ b/web/sitemap.xml @@ -6,6 +6,7 @@ https://journeydeck.me/road-trip-journal-app2026-10-12 https://journeydeck.me/road-trip-tracker-app2026-10-12 https://journeydeck.me/travel-journal-app-with-photos2026-10-12 + https://journeydeck.me/driving-history-app2026-10-12 https://journeydeck.me/privacy2026-09-13 https://journeydeck.me/support2026-09-13 https://journeydeck.me/terms2026-09-13