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", "/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 = {
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("/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];
Expand Down
20 changes: 18 additions & 2 deletions server/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<h1>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, /<h2>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"/);
Expand All @@ -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);
Expand All @@ -199,6 +212,7 @@ test("public information and discovery pages are accessible without an authentic
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/private-driving-journal<\/loc>/i);
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.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 @@ -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/);
Expand Down Expand Up @@ -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 });
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 @@ -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"
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 @@ -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"
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 @@ -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

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 @@ -5,6 +5,7 @@
"/private-driving-journal",
"/road-trip-journal-app",
"/road-trip-tracker-app",
"/travel-journal-app-with-photos",
"/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="/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="/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 @@ -101,7 +101,7 @@ <h1 id="hero-title">Take the road.<br><span class="gold">Keep the story.</span><
<div class="feature-grid">
<article class="feature-card" data-reveal><span class="feature-number">01 / THE ROAD</span><svg class="feature-icon route-icon" viewBox="0 0 160 94" aria-hidden="true"><path class="draw-route" d="M14 72h31c27 0-9-50 22-50h48c30 0-5 50 20 50h11"/><circle cx="14" cy="72" r="5"/><circle cx="146" cy="72" r="5"/></svg><h3>Every journey,<br>beautifully kept.</h3><p>Manually start a journey on iPhone to save your route, distance, and drive time on your device. No account required.</p><a class="feature-cta" href="/private-driving-journal">See the private driving journal overview <span aria-hidden="true">↗</span></a><a class="feature-cta" href="/road-trip-journal-app">See the road trip journal app <span aria-hidden="true">↗</span></a></article>
<article class="feature-card" data-reveal><span class="feature-number">02 / THE SOUNDTRACK</span><div class="record-symbol" aria-hidden="true"><div></div></div><h3>The song takes<br>you right back.</h3><p>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.</p><a class="feature-cta" href="/apple-music-soundtrack">Explore Apple Music soundtracks <span aria-hidden="true">↗</span></a></article>
<article class="feature-card" data-reveal><span class="feature-number">03 / THE MEMORY</span><div class="photo-symbol" aria-hidden="true"><img src="/assets/beta/grand-touring-memories.webp" width="1536" height="1024" loading="lazy" alt=""></div><h3>Make a collection<br>of good days.</h3><p>Group related drives into Memories and add your own photos and notes.</p></article>
<article class="feature-card" data-reveal><span class="feature-number">03 / THE MEMORY</span><div class="photo-symbol" aria-hidden="true"><img src="/assets/beta/grand-touring-memories.webp" width="1536" height="1024" loading="lazy" alt=""></div><h3>Make a collection<br>of good days.</h3><p>Group related drives into Memories and add your own photos and notes.</p><a class="feature-cta" href="/travel-journal-app-with-photos">See the travel journal with photos <span aria-hidden="true">↗</span></a></article>
</div>
</section>
<section class="v2-showcase section-shell section-space" id="replay" aria-labelledby="replay-title">
Expand Down
2 changes: 1 addition & 1 deletion web/private-driving-journal.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ <h2>How it works</h2>
<li>Download JourneyDeck and allow Location when you want route recording.</li>
<li>Tap <strong>Start Journey</strong> on Home (or use Siri or Apple Watch). Put the phone away; the route keeps recording while locked.</li>
<li>When parked, end the journey. Explore the route on your device.</li>
<li>Optionally connect Apple Music for a <a href="/apple-music-soundtrack">soundtrack</a>, or group drives into a Memory with photos and notes.</li>
<li>Optionally connect Apple Music for a <a href="/apple-music-soundtrack">soundtrack</a>, or group drives into a Memory with photos and notes. See the <a href="/travel-journal-app-with-photos">travel journal app with photos</a>.</li>
</ol>
</article>
<article class="card">
Expand Down
1 change: 1 addition & 0 deletions web/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<url><loc>https://journeydeck.me/private-driving-journal</loc><lastmod>2026-09-24</lastmod></url>
<url><loc>https://journeydeck.me/road-trip-journal-app</loc><lastmod>2026-10-12</lastmod></url>
<url><loc>https://journeydeck.me/road-trip-tracker-app</loc><lastmod>2026-10-12</lastmod></url>
<url><loc>https://journeydeck.me/travel-journal-app-with-photos</loc><lastmod>2026-10-12</lastmod></url>
<url><loc>https://journeydeck.me/privacy</loc><lastmod>2026-09-13</lastmod></url>
<url><loc>https://journeydeck.me/support</loc><lastmod>2026-09-13</lastmod></url>
<url><loc>https://journeydeck.me/terms</loc><lastmod>2026-09-13</lastmod></url>
Expand Down
Loading