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.
-
+