diff --git a/server/src/app.ts b/server/src/app.ts index 8d02faa1..619aba55 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", "/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", "/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("/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-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 32b214bf..838afb9a 100644 --- a/server/tests/api.test.ts +++ b/server/tests/api.test.ts @@ -148,13 +148,25 @@ test("public information and discovery pages are accessible without an authentic assert.match(roadTripTracker.body, /https:\/\/apps\.apple\.com\/us\/app\/journeydeck\/id6806502526/); assert.match(roadTripTracker.body, /rel="canonical" href="https:\/\/journeydeck\.me\/road-trip-tracker-app"/); assert.doesNotMatch(roadTripTracker.body, /[\u2013\u2014]/); + const travelPhotos = await runtime.app.inject({ method: "GET", url: "/travel-journal-app-with-photos" }); + assert.equal(travelPhotos.statusCode, 200, travelPhotos.body); + assert.match(String(travelPhotos.headers["content-type"]), /text\/html/); + assert.match(travelPhotos.body, /

Travel journal app with photos for iPhone<\/h1>/); + assert.match(travelPhotos.body, /Group your drives into Memories, then add your own photos and notes\. The route, the pictures and the soundtrack stay together, privately on your iPhone\./); + assert.match(travelPhotos.body, /

Frequently asked questions<\/h2>/); + assert.match(travelPhotos.body, /Is there a travel journal app with photos for iPhone\?/); + assert.match(travelPhotos.body, /href="\/privacy"/); + assert.match(travelPhotos.body, /href="\/apple-music-soundtrack"/); + 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 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, terms]) { + for (const page of [privacy, support, soundtrack, journal, roadTripJournal, roadTripTracker, travelPhotos, 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"/); @@ -175,6 +187,7 @@ test("public information and discovery pages are accessible without an authentic 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, "/travel-journal-app-with-photos"), true); assert.equal(googleRobotsAllowed(robots.body, "/google8e611381b21882a8.html"), true); assert.equal(googleRobotsAllowed(robots.body, "/login"), true); assert.equal(googleRobotsAllowed(robots.body, "/app"), false); @@ -199,6 +212,7 @@ test("public information and discovery pages are accessible without an authentic 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.match(sitemap.body, /https:\/\/journeydeck\.me\/travel-journal-app-with-photos<\/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" }); @@ -244,6 +258,7 @@ test("hosted root serves the Grand Touring launch page while private routes stay 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"/); + 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, /Ask JourneyDeck/); assert.match(landing.body, /Journey Markers/); @@ -359,11 +374,12 @@ test("public marketing pages expose one title, description, canonical, Open Grap { 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: "/travel-journal-app-with-photos", canonical: "https://journeydeck.me/travel-journal-app-with-photos" }, { 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"]); + const rewrittenDescriptions = new Set(["/support", "/privacy", "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", "/travel-journal-app-with-photos"]); 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 6b017921..46881f57 100644 --- a/server/tests/campaign.test.ts +++ b/server/tests/campaign.test.ts @@ -30,6 +30,7 @@ const publicPages = [ "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", + "/travel-journal-app-with-photos", "/support", "/privacy", "/terms" diff --git a/server/tests/verify-live.test.ts b/server/tests/verify-live.test.ts index 98a7f578..cd232384 100644 --- a/server/tests/verify-live.test.ts +++ b/server/tests/verify-live.test.ts @@ -58,6 +58,7 @@ test("expectations loader reads the checked-in file and rejects incomplete objec "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", + "/travel-journal-app-with-photos", "/support", "/privacy", "/terms" diff --git a/tools/verify-live/FEATURE_MAP.md b/tools/verify-live/FEATURE_MAP.md index f8b74fef..2de8beae 100644 --- a/tools/verify-live/FEATURE_MAP.md +++ b/tools/verify-live/FEATURE_MAP.md @@ -25,12 +25,13 @@ Public HTML is served by `server/src/app.ts` in web mode. The public route allow | `/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` | +| `/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` | | `/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`, 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`, 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 f3f1b534..2d50e6c0 100644 --- a/tools/verify-live/expectations.json +++ b/tools/verify-live/expectations.json @@ -5,6 +5,7 @@ "/private-driving-journal", "/road-trip-journal-app", "/road-trip-tracker-app", + "/travel-journal-app-with-photos", "/support", "/privacy", "/terms" diff --git a/web/apple-music-soundtrack.html b/web/apple-music-soundtrack.html index e54a9ebc..c6c22952 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 2777cf5d..24490290 100644 --- a/web/beta.html +++ b/web/beta.html @@ -101,7 +101,7 @@

Take the road.
Keep the story.<
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 bffb5e42..ad87b868 100644 --- a/web/private-driving-journal.html +++ b/web/private-driving-journal.html @@ -97,7 +97,7 @@

How it works

  • Download JourneyDeck and allow Location when you want route recording.
  • Tap Start Journey on Home (or use Siri or Apple Watch). Put the phone away; the route keeps recording while locked.
  • When parked, end the journey. Explore the route on your device.
  • -
  • Optionally connect Apple Music for a soundtrack, or group drives into a Memory with photos and notes.
  • +
  • Optionally connect Apple Music for a soundtrack, or group drives into a Memory with photos and notes. See the travel journal app with photos.
  • diff --git a/web/sitemap.xml b/web/sitemap.xml index fb56ea3a..de929126 100644 --- a/web/sitemap.xml +++ b/web/sitemap.xml @@ -5,6 +5,7 @@ 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/travel-journal-app-with-photos2026-10-12 https://journeydeck.me/privacy2026-09-13 https://journeydeck.me/support2026-09-13 https://journeydeck.me/terms2026-09-13 diff --git a/web/travel-journal-app-with-photos.html b/web/travel-journal-app-with-photos.html new file mode 100644 index 00000000..eb998ede --- /dev/null +++ b/web/travel-journal-app-with-photos.html @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Travel journal app with photos for iPhone | JourneyDeck + + + + + + +
    +
    +

    PHOTOS, NOTES, THE ROAD

    +

    Travel journal app with photos for iPhone

    +

    Group your drives into Memories, then add your own photos and notes. The route, the pictures and the soundtrack stay together, privately on your iPhone.

    + +
    +
    +

    MEMORIES / JOURNEYDECK 4.0

    Keep the whole story.

    Bring related drives into a Memory, then add your own photos and notes. Review photo suggestions before adding anything to your archive.

    +
    JourneyDeck 4.0 screenshot showing a Memory with photos, notes, and saved journeys
    JourneyDeck 4.0 Memories on iPhone
    +
    +
    +
    +

    Photos sit with the drive

    +

    JourneyDeck is a private driving journal first. You tap Start Journey on iPhone, save the route, then keep pictures of the day with that road. Group related journeys into Memories. Add notes. Replay the map of the roads you have driven.

    +
    +
    +

    How photos get into the journal

    +
      +
    1. Record a journey on iPhone. Your route, distance, and drive time stay on your device.
    2. +
    3. When safely parked, add notes and photos during a drive with Journey Markers, or add them later.
    4. +
    5. Bring related drives into a Memory and add your own photos and notes.
    6. +
    7. Review photo suggestions before adding anything to your archive.
    8. +
    +
    +
    +

    A travel diary with a map, for drives

    +

    If you searched for a travel diary app with a map, JourneyDeck shows the route you recorded, then lets photos and notes live with that journey. Optional Apple Music soundtracks match songs from your authorized listening history. Create a journey share card when you choose; sensitive Home, Work, and School places can be protected in the shared view.

    +

    Read the private driving journal overview and how Apple Music soundtracks work.

    +
    +
    +

    Frequently asked questions

    +

    Is there a travel journal app with photos for iPhone?

    +

    Yes. Group your drives into Memories, then add your own photos and notes. The route, the pictures and the soundtrack stay together, privately on your iPhone. It is a travel journal built around the drives you record on iPhone, not a general diary for flights or hotel stays.

    +

    Does JourneyDeck include a map?

    +

    Yes. After you record a journey, you can revisit its route on your device. Memories keep photos with the roads you have driven.

    +

    Can I add photos during a drive?

    +

    Add notes and photos during a drive with Journey Markers when you are safely parked. You can also add your own photos after the drive when you group journeys into a Memory. Review photo suggestions before adding anything to your archive.

    +

    Is this a general travel diary app?

    +

    No. JourneyDeck is a private driving journal. Photos, notes, and maps stay with the journeys you start on iPhone.

    +

    Do photos leave my device?

    +

    Your archive is stored on your device first. Optional private iCloud sync keeps your library available across your iPhone and iPad. Sign-in and iCloud sync are optional. You review photo suggestions before anything is added.

    +
    +
    + +
    + +