diff --git a/server/src/app.ts b/server/src/app.ts index 02a42f11..8d02faa1 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-tracker-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 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", "/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-tracker-app", async (_req, reply) => reply.sendFile("road-trip-tracker-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("/", 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("/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 bc85e9c1..dc64fbc0 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -124,6 +124,18 @@ test("public information and discovery pages are accessible without an authentic assert.match(journal.body, /fetchpriority="high"/); assert.match(journal.body, /srcset="/); assert.doesNotMatch(journal.body, /class="hero-photo"[^>]*loading="lazy"/); + const roadTripJournal = await runtime.app.inject({ method: "GET", url: "/road-trip-journal-app" }); + assert.equal(roadTripJournal.statusCode, 200, roadTripJournal.body); + assert.match(String(roadTripJournal.headers["content-type"]), /text\/html/); + assert.match(roadTripJournal.body, /

Road trip journal app for iPhone<\/h1>/); + assert.match(roadTripJournal.body, /A private road trip journal for iPhone\. Start a journey, and JourneyDeck keeps the route, the miles and the music\./); + assert.match(roadTripJournal.body, /

Frequently asked questions<\/h2>/); + assert.match(roadTripJournal.body, /What is a road trip journal app\?/); + assert.match(roadTripJournal.body, /href="\/privacy"/); + assert.match(roadTripJournal.body, /href="\/apple-music-soundtrack"/); + assert.match(roadTripJournal.body, /https:\/\/apps\.apple\.com\/us\/app\/journeydeck\/id6806502526/); + assert.match(roadTripJournal.body, /rel="canonical" href="https:\/\/journeydeck\.me\/road-trip-journal-app"/); + assert.doesNotMatch(roadTripJournal.body, /[\u2013\u2014]/); const roadTripTracker = await runtime.app.inject({ method: "GET", url: "/road-trip-tracker-app" }); assert.equal(roadTripTracker.statusCode, 200, roadTripTracker.body); assert.match(String(roadTripTracker.headers["content-type"]), /text\/html/); @@ -142,7 +154,7 @@ test("public information and discovery pages are accessible without an authentic 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, roadTripTracker, terms]) { + for (const page of [privacy, support, soundtrack, journal, roadTripJournal, roadTripTracker, 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"/); @@ -161,6 +173,7 @@ test("public information and discovery pages are accessible without an authentic assert.doesNotMatch(robots.body, /Disallow: \/google8e611381b21882a8\.html/i); assert.equal(googleRobotsAllowed(robots.body, "/apple-music-soundtrack"), true); assert.equal(googleRobotsAllowed(robots.body, "/private-driving-journal"), true); + 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, "/google8e611381b21882a8.html"), true); assert.equal(googleRobotsAllowed(robots.body, "/login"), true); @@ -184,6 +197,7 @@ test("public information and discovery pages are accessible without an authentic assert.match(sitemap.body, /https:\/\/journeydeck\.me\/terms<\/loc>/i); assert.match(sitemap.body, /https:\/\/journeydeck\.me\/apple-music-soundtrack<\/loc>/i); assert.match(sitemap.body, /https:\/\/journeydeck\.me\/private-driving-journal<\/loc>/i); + 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.doesNotMatch(sitemap.body, /https:\/\/journeydeck\.me\/(?:beta|app|login)(?:\/|<)/i); assert.doesNotMatch(sitemap.body, /google8e611381b21882a8/i); @@ -226,6 +240,7 @@ test("hosted root serves the Grand Touring launch page while private routes stay assert.match(landing.body, /href="\/apple-music-soundtrack"/); assert.match(landing.body, /class="feature-cta" href="\/apple-music-soundtrack"/); assert.match(landing.body, /class="feature-cta" href="\/private-driving-journal">See the private driving journal overview/); + assert.match(landing.body, /class="feature-cta" href="\/road-trip-journal-app">See the road trip journal app/); assert.match(landing.body, /id="replay"/); assert.match(landing.body, /class="feature-cta" href="\/road-trip-tracker-app">See the road trip tracker app/); assert.match(landing.body, /id="memories"/); @@ -342,12 +357,13 @@ test("public marketing pages expose one title, description, canonical, Open Grap { url: "/", canonical: "https://journeydeck.me/" }, { url: "/apple-music-soundtrack", canonical: "https://journeydeck.me/apple-music-soundtrack" }, { url: "/private-driving-journal", canonical: "https://journeydeck.me/private-driving-journal" }, + { 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: "/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-tracker-app"]); + const rewrittenDescriptions = new Set(["/support", "/privacy", "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-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 915eccd6..6b017921 100644 --- a/server/tests/campaign.test.ts +++ b/server/tests/campaign.test.ts @@ -28,6 +28,7 @@ const publicPages = [ "/", "/apple-music-soundtrack", "/private-driving-journal", + "/road-trip-journal-app", "/road-trip-tracker-app", "/support", "/privacy", diff --git a/server/tests/verify-live.test.ts b/server/tests/verify-live.test.ts index eadec603..98a7f578 100644 --- a/server/tests/verify-live.test.ts +++ b/server/tests/verify-live.test.ts @@ -56,6 +56,7 @@ test("expectations loader reads the checked-in file and rejects incomplete objec "/", "/apple-music-soundtrack", "/private-driving-journal", + "/road-trip-journal-app", "/road-trip-tracker-app", "/support", "/privacy", diff --git a/tools/verify-live/FEATURE_MAP.md b/tools/verify-live/FEATURE_MAP.md index 5679553b..f8b74fef 100644 --- a/tools/verify-live/FEATURE_MAP.md +++ b/tools/verify-live/FEATURE_MAP.md @@ -23,13 +23,14 @@ Public HTML is served by `server/src/app.ts` in web mode. The public route allow | `/` | `web/beta.html` via `server/src/app.ts` `GET /` | `page:/ status`, `page:/ title`, `page:/ meta-description`, `page:/ canonical`, `page:/ og:title`, `page:/ og:description`, `page:/ og:url`, `page:/ og:image`, `page:/ json-ld`, `page:/ umami`, `page:/ trial-wording` | | `/apple-music-soundtrack` | `web/apple-music-soundtrack.html` | `page:/apple-music-soundtrack status`, `page:/apple-music-soundtrack title`, `page:/apple-music-soundtrack meta-description`, `page:/apple-music-soundtrack canonical`, `page:/apple-music-soundtrack og:title`, `page:/apple-music-soundtrack og:description`, `page:/apple-music-soundtrack og:url`, `page:/apple-music-soundtrack og:image`, `page:/apple-music-soundtrack json-ld`, `page:/apple-music-soundtrack umami`, `page:/apple-music-soundtrack trial-wording` | | `/private-driving-journal` | `web/private-driving-journal.html` | `page:/private-driving-journal status`, `page:/private-driving-journal title`, `page:/private-driving-journal meta-description`, `page:/private-driving-journal canonical`, `page:/private-driving-journal og:title`, `page:/private-driving-journal og:description`, `page:/private-driving-journal og:url`, `page:/private-driving-journal og:image`, `page:/private-driving-journal json-ld`, `page:/private-driving-journal umami`, `page:/private-driving-journal trial-wording` | +| `/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` | | `/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-tracker-app.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`, 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 ccaa0a6c..f3f1b534 100644 --- a/tools/verify-live/expectations.json +++ b/tools/verify-live/expectations.json @@ -3,6 +3,7 @@ "/", "/apple-music-soundtrack", "/private-driving-journal", + "/road-trip-journal-app", "/road-trip-tracker-app", "/support", "/privacy", diff --git a/web/apple-music-soundtrack.html b/web/apple-music-soundtrack.html index 31347636..6620bb50 100644 --- a/web/apple-music-soundtrack.html +++ b/web/apple-music-soundtrack.html @@ -107,7 +107,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 56a18be7..da4b22b0 100644 --- a/web/beta.html +++ b/web/beta.html @@ -97,7 +97,7 @@

Take the road.
Keep the story.<

THE EVERYDAY. WORTH REMEMBERING.

More than where you went.
How it felt to be there.

That perfect song on the way home. A weekend away. The detour you’re glad you took. JourneyDeck brings the pieces back together.

- +
02 / THE SOUNDTRACK

The song takes
you right back.

Record a drive, then revisit it with an optional soundtrack matched from your Apple Music listening history. Hear a song you want to keep? Tap Identify Song to recognize it with ShazamKit. The microphone listens briefly for that request; audio is never saved.

Explore Apple Music soundtracks
03 / THE MEMORY

Make a collection
of good days.

Group related drives into Memories and add your own photos and notes.

diff --git a/web/private-driving-journal.html b/web/private-driving-journal.html index 6a185201..511958d8 100644 --- a/web/private-driving-journal.html +++ b/web/private-driving-journal.html @@ -87,6 +87,8 @@

Private driving journal for iPhone

Who it’s for

Drivers who want a private archive of the roads they take, everyday drives and longer trips, without signing up for a cloud tracker or handing over a continuous trip history account.

+

Looking for a road trip journal app, car trip log, or trip journal on iPhone? That overview stays with the same private, local-first recorder.

+

If you need a personal driving history app or drive log, that page explains the journal, and that it is not a DMV record or mileage tax log.

How it works

diff --git a/web/road-trip-journal-app.html b/web/road-trip-journal-app.html new file mode 100644 index 00000000..055b1f6f --- /dev/null +++ b/web/road-trip-journal-app.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Road trip journal app for iPhone | JourneyDeck + + + + + + +
+
+

YOUR ROADS, YOUR ARCHIVE

+

Road trip journal app for iPhone

+

A private road trip journal for iPhone. Start a journey, and JourneyDeck keeps the route, the miles and the music.

+ +
+
+

THE ROAD / JOURNEYDECK 4.0

Every journey, kept.

Manually start a journey on iPhone to save your route, distance, and drive time on your device. No account required. Optional Apple Music soundtracks and Memories with photos and notes sit with the road.

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

Who it is for

+

Drivers who want a private archive of the roads they take, everyday drives and longer trips, without signing up for a cloud tracker or handing over a continuous trip history account.

+
+
+

How it works

+
    +
  1. Download JourneyDeck and allow Location when you want route recording.
  2. +
  3. Tap Start Journey on Home (or use Siri, the Start a Journey widget, or Apple Watch). Put the phone away; the route keeps recording while locked.
  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 stays with each trip

+
    +
  • The route, distance, and drive time you recorded.
  • +
  • Places along the drive, including Home, Work, or School labels you choose to save.
  • +
  • Photos and notes you add to a Memory or a Journey Marker.
  • +
  • An optional soundtrack from your authorized Apple Music listening history, or Identify Song with ShazamKit. Audio is never saved.
  • +
+
+
+

What private and local-first mean

+
    +
  • Core recording works offline, without an account or subscription.
  • +
  • Journey data starts on your iPhone and remains there until you delete it, delete the relevant profile, or remove the app. Private iCloud continuity is optional.
  • +
  • Sign in with Apple is optional.
  • +
  • Journeys never start automatically. You start and finish intentionally.
  • +
+

Read the JourneyDeck privacy policy and the private driving journal overview for more.

+
+
+

Frequently asked questions

+

What is a road trip journal app?

+

A road trip journal app keeps a personal archive of trips you choose to record. JourneyDeck is a private driving journal for iPhone: you tap Start Journey before you set off, and your route, distance, and drive time are saved on your device.

+

Is JourneyDeck a road trip log or car trip log app?

+

Yes, as a manual journal. You start and finish each drive yourself. It is not an automatic mileage tracker or a cloud trip log that records every drive in the background.

+

Does JourneyDeck need an account?

+

No. Core recording works without an account or subscription. Sign in with Apple and private iCloud sync are optional.

+

Does it record every trip automatically?

+

No. You tap Start Journey when you want to keep a drive.

+

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/sitemap.xml b/web/sitemap.xml index e0490bbb..fb56ea3a 100644 --- a/web/sitemap.xml +++ b/web/sitemap.xml @@ -3,6 +3,7 @@ https://journeydeck.me/2026-09-16 https://journeydeck.me/apple-music-soundtrack2026-09-23 https://journeydeck.me/private-driving-journal2026-09-24 + https://journeydeck.me/road-trip-journal-app2026-10-12 https://journeydeck.me/road-trip-tracker-app2026-10-12 https://journeydeck.me/privacy2026-09-13 https://journeydeck.me/support2026-09-13