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
19 changes: 9 additions & 10 deletions .ai/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,30 @@

## Current objective

Ship responsive LCP hero images on `/apple-music-soundtrack` and `/private-driving-journal`. Do not merge, deploy, or change hosting, DNS, analytics, or CSP.
Ship four themed road-trip SEO landing pages in one draft PR. Do not merge or deploy.

## Checkout

- Branch `cursor/hero-lcp-srcset-952c` off `main`.
- Branch `cursor/road-trip-journal-seo-pages-2843` off `main`.

## Material changes

- Shared LCP art is `web/assets/beta/grand-touring-home.webp` (1536x1024, 315 KB), used as the `.hero` CSS background on both pages.
- Added `grand-touring-home-800.webp` (91 KB) and `grand-touring-home-1600.webp` (258 KB) beside the original, versioned with `?v=1`.
- Both pages now use an absolutely positioned `.hero-photo` with srcset/sizes, preload, and `fetchpriority="high"`. Other public pages keep the CSS background.
- `server/tests/api.test.ts` asserts 200 + immutable cache on the resized files and preload markup on both pages.
- New public pages: `/route-66-road-trip-journal`, `/pacific-coast-highway-road-trip-journal`, `/national-parks-road-trip-journal`, `/texas-hill-country-road-trip-journal`.
- Wired like #268/#270/#273/#276: `publicPaths`, `app.get` sendFile, sitemap, server tests, verify-live expectations, feature map, canonical/meta/OG/JSON-LD/Umami, plain App Store CTA plus `campaign.js`.
- Copy from the draft, minus unconfirmed personal lines, plus design-review FAQ and intro fixes. JSON-LD FAQ text matches the visible FAQ. No em/en dashes. Journeys start only when the user taps Start Journey.
- Cross-links from `/road-trip-journal-app`, `/private-driving-journal`, and the Apple Music soundtrack footer.

## Verification

- `node --test --import tsx server/tests/api.test.ts`
- `node --test --import tsx server/tests/campaign.test.ts server/tests/verify-live.test.ts`
- Playwright screenshots at 390x844 and 1280x800 (before and after). Mobile selects 800w; desktop selects 1600w.
- Desktop 1280 and mobile 390 screenshots of each page in the PR body.

## Unresolved / owner decisions

- Lab Lighthouse after-numbers still need to be recorded in the PR if the environment can run Lighthouse.
- PR 262 (verify:live) remains a separate review. Do not merge or deploy from this session.
- Keep the PR as a draft. Do not merge or deploy from this session.

## Next steps

1. Keep this hero LCP PR open for review. Do not merge or deploy.
1. Keep the four-page SEO PR open for review. Do not merge or deploy.
2. Confirm JourneyDeck CI is green.
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", "/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 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", "/route-66-road-trip-journal", "/pacific-coast-highway-road-trip-journal", "/national-parks-road-trip-journal", "/texas-hill-country-road-trip-journal", "/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 @@ -324,7 +324,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-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("/", 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("/route-66-road-trip-journal", async (_req, reply) => reply.sendFile("route-66-road-trip-journal.html")); app.get("/pacific-coast-highway-road-trip-journal", async (_req, reply) => reply.sendFile("pacific-coast-highway-road-trip-journal.html")); app.get("/national-parks-road-trip-journal", async (_req, reply) => reply.sendFile("national-parks-road-trip-journal.html")); app.get("/texas-hill-country-road-trip-journal", async (_req, reply) => reply.sendFile("texas-hill-country-road-trip-journal.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
Loading