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