From 1468d5a96aa6996daf40ea5c17e737689e428ec6 Mon Sep 17 00:00:00 2001 From: Eth-Interchained Date: Tue, 7 Jul 2026 17:56:18 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20canonical=20Phorest=20booking=20URL=20?= =?UTF-8?q?=E2=80=94=20and=20booking=20CTAs=20become=20data-driven?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark's correction from the live site: the real booking link is https://www.phorest.com/salon/mintontheavenue — the request-appointment page is being phased out. - seed: bookingUrl confirmed (closes the TO_CONFIRM item), provenance note records who confirmed and when - pages.ts: the hardcoded booking URL dies — ctaRow reads entity.bookingUrl (https-validated); a salon without one simply gets no book button, call/text remain. The generic-platform way. - live suite asserts the CTA targets the canonical Phorest URL, encoded through /go tracking The correction is itself provenance: the changed seed file yields a new content-addressed run id; the salon doc's next version chains to it. TRACE shows the booking URL changed, when, and on whose word. Verified: tsc clean · unit 30/30 · live 4/4 vs nedbd 2.6.1 · build clean. Co-Authored-By: Claude Fable 5 --- data/seeds/mint-on-the-avenue.json | 4 ++-- src/server/pages.ts | 9 ++++++--- test/api.test.ts | 4 ++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/data/seeds/mint-on-the-avenue.json b/data/seeds/mint-on-the-avenue.json index 6866f2c..f867570 100644 --- a/data/seeds/mint-on-the-avenue.json +++ b/data/seeds/mint-on-the-avenue.json @@ -48,7 +48,8 @@ ], "priceRange": "$$-$$$", "bookingPlatform": "Phorest", - "bookingUrl": "TO_CONFIRM — 'Book Now via Phorest' target URL not captured from static HTML", + "bookingUrl": "https://www.phorest.com/salon/mintontheavenue", + "$bookingNote": "Confirmed by Mark 2026-07-07 — the canonical Phorest booking page. mintontheavenue.com/request-appointment is being phased out.", "gbp": { "listedName": "Mint On The Avenue Aveda Salon", "placeId": "ChIJQfwznhBw54gRZHRHaFdWJxA", @@ -229,7 +230,6 @@ ], "toConfirmWithMark": [ - "Exact Phorest booking URL (Book Now target)", "Social links (Instagram/Facebook) — not present in crawled static HTML", "Canonical text number (833 vs 830 — 833 presumed, matches homepage + blog)", "Canonical price source (service pages vs homepage widget)", diff --git a/src/server/pages.ts b/src/server/pages.ts index 2af2090..1e42164 100644 --- a/src/server/pages.ts +++ b/src/server/pages.ts @@ -36,6 +36,7 @@ interface SalonDoc { cityId?: string | null; entity?: SalonEntityLike & { textLine?: string; + bookingUrl?: string; positioning?: { tagline?: string; heroLine?: string; philosophy?: string }; serviceInclusions?: string; stylistLevels?: string; @@ -60,15 +61,17 @@ function track(db: NedbClient, event: Record): void { }); } -/** Tracked CTA row — every conversion path goes through /go. */ +/** Tracked CTA row — every conversion path goes through /go. The booking + * URL is DATA (entity.bookingUrl from the seed) — a salon without one + * simply doesn't get the button; call/text remain. */ function ctaRow(salon: SalonDoc, origin: string): string { const e = salon.entity ?? {}; const tel = telHref(e.nap?.phone); const sms = smsHref(e.textLine); - const book = "https://mintontheavenue.com/request-appointment/"; + const book = typeof e.bookingUrl === "string" && /^https:\/\//.test(e.bookingUrl) ? e.bookingUrl : null; const go = (kind: string, to: string) => `/go/${kind}?to=${encodeURIComponent(to)}`; const parts: string[] = []; - parts.push(`Book an appointment`); + if (book) parts.push(`Book an appointment`); if (sms) parts.push(`Text ${esc(e.textLine ?? "")}`); if (tel) parts.push(`Call ${esc(e.nap?.phone ?? "")}`); if (e.gbp?.mapsUrl) parts.push(`Directions`); diff --git a/test/api.test.ts b/test/api.test.ts index 8ffc11c..85c6ef8 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -180,6 +180,10 @@ test("orlando surfaces: server-rendered pages from real engine data", async () = assert.ok(!homeHtml.includes("aggregateRating"), "no self-marked-up Google rating"); assert.ok(homeHtml.includes(`