From b15d93bc372c9516f9f9532236c1dca3f1de02e9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 12 Oct 2026 14:22:52 +0000 Subject: [PATCH 1/2] Add /road-trip-journal-app public landing page Publish a query-focused road trip journal page modeled on the existing public journal and soundtrack pages, with sitemap, public-path, and canonical coverage. Co-authored-by: Patrick --- server/src/app.ts | 4 +- server/tests/api.test.ts | 20 +++- server/tests/campaign.test.ts | 1 + server/tests/verify-live.test.ts | 1 + tools/verify-live/FEATURE_MAP.md | 3 +- tools/verify-live/expectations.json | 1 + web/apple-music-soundtrack.html | 2 +- web/beta.html | 2 +- web/private-driving-journal.html | 1 + web/road-trip-journal-app.html | 147 ++++++++++++++++++++++++++++ web/sitemap.xml | 1 + 11 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 web/road-trip-journal-app.html diff --git a/server/src/app.ts b/server/src/app.ts index 8a9ce010..87cbb13c 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", "/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", "/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("/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("/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 3f1350a5..fa18da13 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -110,13 +110,25 @@ test("public information and discovery pages are accessible without an authentic assert.match(journal.body, /href="\/apple-music-soundtrack"/); assert.match(journal.body, /https:\/\/apps\.apple\.com\/us\/app\/journeydeck\/id6806502526/); assert.match(journal.body, /rel="canonical" href="https:\/\/journeydeck\.me\/private-driving-journal"/); + 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, /JourneyDeck is a road trip journal app for iPhone/); + 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 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, terms]) { + for (const page of [privacy, support, soundtrack, journal, roadTripJournal, 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"/); @@ -135,6 +147,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, "/google8e611381b21882a8.html"), true); assert.equal(googleRobotsAllowed(robots.body, "/login"), true); assert.equal(googleRobotsAllowed(robots.body, "/app"), false); @@ -157,6 +170,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.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" }); @@ -198,6 +212,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, /id="memories"/); assert.match(landing.body, /id="membership"/); @@ -310,11 +325,12 @@ 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: "/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"]); + const rewrittenDescriptions = new Set(["/support", "/privacy", "/private-driving-journal", "/road-trip-journal-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 acb457dc..a37353d9 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", "/support", "/privacy", "/terms" diff --git a/server/tests/verify-live.test.ts b/server/tests/verify-live.test.ts index 1d5b1b57..6e2b8d40 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", "/support", "/privacy", "/terms" diff --git a/tools/verify-live/FEATURE_MAP.md b/tools/verify-live/FEATURE_MAP.md index cff3b677..5b18608f 100644 --- a/tools/verify-live/FEATURE_MAP.md +++ b/tools/verify-live/FEATURE_MAP.md @@ -23,12 +23,13 @@ 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` | | `/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`, 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`, 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 f96adb97..e218c42d 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", "/support", "/privacy", "/terms" diff --git a/web/apple-music-soundtrack.html b/web/apple-music-soundtrack.html index a91d97d2..601b2434 100644 --- a/web/apple-music-soundtrack.html +++ b/web/apple-music-soundtrack.html @@ -105,7 +105,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 7714b1ec..aae09402 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 05b647c7..b3f9affb 100644 --- a/web/private-driving-journal.html +++ b/web/private-driving-journal.html @@ -85,6 +85,7 @@

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.

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..83ecec6c --- /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

+

JourneyDeck is a road trip journal app for iPhone, and a private car trip log you start by hand. Tap Start Journey before you set off; JourneyDeck saves your route, distance, and drive time on your device. You can keep places, photos, notes, and an optional Apple Music soundtrack with each drive, then group related trips into a Memory. It is a manual trip journal, not an automatic cloud tracker.

+ +
+
+

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 7c620909..ca10bf4b 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/privacy2026-09-13 https://journeydeck.me/support2026-09-13 https://journeydeck.me/terms2026-09-13 From b9f26e603fea1cc51eccd35982ff26ac368d9664 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 12 Oct 2026 14:48:16 +0000 Subject: [PATCH 2/2] Shorten the road trip journal hero intro Use the reviewed phone intro and keep the meta description aligned. Co-authored-by: Patrick --- server/tests/api.test.ts | 2 +- web/road-trip-journal-app.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/tests/api.test.ts b/server/tests/api.test.ts index fa18da13..dda3b155 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -114,7 +114,7 @@ test("public information and discovery pages are accessible without an authentic 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, /JourneyDeck is a road trip journal app for iPhone/); + 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"/); diff --git a/web/road-trip-journal-app.html b/web/road-trip-journal-app.html index 83ecec6c..055b1f6f 100644 --- a/web/road-trip-journal-app.html +++ b/web/road-trip-journal-app.html @@ -4,12 +4,12 @@ - + - + @@ -87,7 +87,7 @@

YOUR ROADS, YOUR ARCHIVE

Road trip journal app for iPhone

-

JourneyDeck is a road trip journal app for iPhone, and a private car trip log you start by hand. Tap Start Journey before you set off; JourneyDeck saves your route, distance, and drive time on your device. You can keep places, photos, notes, and an optional Apple Music soundtrack with each drive, then group related trips into a Memory. It is a manual trip journal, not an automatic cloud tracker.

+

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