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-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 = {
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-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];
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 @@ -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, /<h1>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, /<h2>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/);
Expand All @@ -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"/);
Expand All @@ -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);
Expand All @@ -184,6 +197,7 @@ test("public information and discovery pages are accessible without an authentic
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/terms<\/loc>/i);
assert.match(sitemap.body, /<loc>https:\/\/journeydeck\.me\/apple-music-soundtrack<\/loc>/i);
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.doesNotMatch(sitemap.body, /<loc>https:\/\/journeydeck\.me\/(?:beta|app|login)(?:\/|<)/i);
assert.doesNotMatch(sitemap.body, /google8e611381b21882a8/i);
Expand Down Expand Up @@ -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"/);
Expand Down Expand Up @@ -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 });
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 @@ -28,6 +28,7 @@ const publicPages = [
"/",
"/apple-music-soundtrack",
"/private-driving-journal",
"/road-trip-journal-app",
"/road-trip-tracker-app",
"/support",
"/privacy",
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 @@ -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",
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 @@ -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

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 @@ -3,6 +3,7 @@
"/",
"/apple-music-soundtrack",
"/private-driving-journal",
"/road-trip-journal-app",
"/road-trip-tracker-app",
"/support",
"/privacy",
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 @@ -107,7 +107,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-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="/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 @@ -97,7 +97,7 @@ <h1 id="hero-title">Take the road.<br><span class="gold">Keep the story.</span><
<section class="idea section-shell section-space" id="the-idea" aria-labelledby="idea-title">
<div class="section-heading" data-reveal><p class="eyebrow">THE EVERYDAY. WORTH REMEMBERING.</p><h2 id="idea-title">More than where you went.<br><span class="muted">How it felt to be there.</span></h2><p>That perfect song on the way home. A weekend away. The detour you’re glad you took. JourneyDeck brings the pieces back together.</p></div>
<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></article>
<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>
</div>
Expand Down
2 changes: 2 additions & 0 deletions web/private-driving-journal.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ <h1>Private driving journal for iPhone</h1>
<article class="card">
<h2>Who it’s for</h2>
<p>Drivers who want a private archive of the roads they take, everyday drives and longer trips, without signing up for a cloud tracker or handing over a continuous trip history account.</p>
<p>Looking for a <a href="/road-trip-journal-app">road trip journal app</a>, car trip log, or trip journal on iPhone? That overview stays with the same private, local-first recorder.</p>
<p>If you need a personal <a href="/driving-history-app">driving history app</a> or drive log, that page explains the journal, and that it is not a DMV record or mileage tax log.</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the link to the unserved landing page

Clicking this newly added link in the hosted site leads to the 404 page because commit 1d4585d7 contains neither a /driving-history-app route nor a corresponding HTML file; a repository-wide search at this commit finds this URL only here. Either add the page and route or remove the link so visitors are not sent to a dead destination.

Useful? React with 👍 / 👎.

</article>
<article class="card">
<h2>How it works</h2>
Expand Down
Loading