Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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];
Expand Down
24 changes: 22 additions & 2 deletions server/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<h1>Driving history app for iPhone<\/h1>/);
assert.match(drivingHistory.body, /<p class="lede">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, /<h2>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"/);
Expand All @@ -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);
Expand All @@ -213,6 +230,7 @@ test("public information and discovery pages are accessible without an authentic
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/road-trip-journal-app<\/loc>/i);
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/road-trip-tracker-app<\/loc>/i);
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/travel-journal-app-with-photos<\/loc>/i);
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/driving-history-app<\/loc>/i);
assert.doesNotMatch(sitemap.body, /<loc>https:\/\/journeydeck\.me\/(?:beta|app|login)(?:\/|<)/i);
assert.doesNotMatch(sitemap.body, /google8e611381b21882a8/i);
const googleVerification = await runtime.app.inject({ method: "GET", url: "/google8e611381b21882a8.html" });
Expand Down Expand Up @@ -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/);
Expand Down Expand Up @@ -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 });
Expand Down
1 change: 1 addition & 0 deletions server/tests/campaign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions server/tests/verify-live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tools/verify-live/FEATURE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tools/verify-live/expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"/road-trip-journal-app",
"/road-trip-tracker-app",
"/travel-journal-app-with-photos",
"/driving-history-app",
"/support",
"/privacy",
"/terms"
Expand Down
2 changes: 1 addition & 1 deletion web/apple-music-soundtrack.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ <h3>Is JourneyDeck a navigation app?</h3>
<p>No. Set up recording before driving and do not interact with the app while operating a vehicle.</p>
</article>
</section>
<footer class="footer">© 2026 Patrick Benjamin Stewart · <a href="/">Home</a> · <a href="/road-trip-journal-app">Road trip journal</a> · <a href="/road-trip-tracker-app">Road trip tracker</a> · <a href="/travel-journal-app-with-photos">Travel journal with photos</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></footer>
<footer class="footer">© 2026 Patrick Benjamin Stewart · <a href="/">Home</a> · <a href="/road-trip-journal-app">Road trip journal</a> · <a href="/road-trip-tracker-app">Road trip tracker</a> · <a href="/travel-journal-app-with-photos">Travel journal with photos</a> · <a href="/driving-history-app">Driving history</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></footer>
</main>
</body>
</html>
2 changes: 1 addition & 1 deletion web/beta.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ <h1 id="hero-title">Take the road.<br><span class="gold">Keep the story.</span><
</section>
<section class="atlas-section section-shell section-space" aria-labelledby="atlas-title">
<div class="atlas-art" data-reveal aria-hidden="true"><div class="atlas-sheet atlas-sheet-back"></div><div class="atlas-sheet atlas-sheet-front"><div class="atlas-sheet-label"><span>ATLAS</span><span>YOUR WORLD, CONNECTED</span></div><svg viewBox="0 0 420 300"><g class="map-lines"><path d="M0 74 420 190M80 0 200 300M310 0 254 300M0 240 420 80M0 172 420 260M170 0 54 300M370 0 345 300"/></g><path class="draw-route" d="M73 208C96 195 70 143 133 160S193 239 223 175 191 70 277 91 300 156 354 81"/><g class="map-pins"><circle cx="73" cy="208" r="7"/><circle cx="223" cy="175" r="5"/><circle cx="354" cy="81" r="7"/></g></svg><div class="atlas-sheet-bottom"><span>THE PLACES YOU RETURN TO.</span><span>↗</span></div></div></div>
<div class="atlas-copy" data-reveal><p class="eyebrow">ATLAS / JOURNEYDECK PLUS</p><h2 id="atlas-title">See the pattern.<br><span class="gold">Find your perspective.</span></h2><p>Meet the bigger picture behind your everyday drives. Atlas connects your routes, familiar places, and listening habits into insights calculated on your device.</p><div class="detail-tags"><span>Route DNA</span><span>Driving rhythms</span><span>Soundtrack insights</span></div><p class="small muted">JourneyDeck Plus unlocks Atlas and your complete locally stored history. Free recording includes today’s journeys.</p></div>
<div class="atlas-copy" data-reveal><p class="eyebrow">ATLAS / JOURNEYDECK PLUS</p><h2 id="atlas-title">See the pattern.<br><span class="gold">Find your perspective.</span></h2><p>Meet the bigger picture behind your everyday drives. Atlas connects your routes, familiar places, and listening habits into insights calculated on your device.</p><div class="detail-tags"><span>Route DNA</span><span>Driving rhythms</span><span>Soundtrack insights</span></div><p class="small muted">JourneyDeck Plus unlocks Atlas and your complete locally stored history. Free recording includes today’s journeys.</p><a class="feature-cta" href="/driving-history-app">See the driving history app <span aria-hidden="true">↗</span></a></div>
</section>
<section class="idea section-shell section-space" aria-labelledby="v4-features-title">
<div class="section-heading" data-reveal><p class="eyebrow">NEW IN 4.0</p><h2 id="v4-features-title">More ways to<br><span class="gold">keep the story.</span></h2></div>
Expand Down
Loading