Files
baya-monorepo/dev/shared-working-context/reports/ui-phase-13-report.md
T
2026-07-20 01:35:15 +03:30

11 KiB
Raw Blame History

UI Phase 13 — Public Front Door — Report (2026-07-19)

The §3.1 decision (recorded first, per the phase's own gate)

Framed the three guest-browse tiers and decided ship (a) landing + (b) static public pages now; defer (c) guest search + public nurse profiles as a REQ-gated follow-up. Full rationale recorded in product/notes/open-questions.md ("Decided — public guest-browse depth" section). Tier (c)'s two endpoints are filed as REQ-066/REQ-067 (see Contracts below) — nothing in this phase calls them; no guest search/profile screen was built.

What was built

  • The guest front door itselfclient/middleware.ts: after the existing next-intl 307/308 early-return, an unauthenticated exact match on pathWithoutLocale === '/' is NextResponse.rewrite()d to /{locale}/welcome (never a redirect — the browser URL and SEO canonical stay /). An authenticated hit on /welcome redirects to /. Every other path's behavior (locale detection, the private-route → /login?next= redirect, the returnUrl capture, the copied next-intl hreflang headers) is untouched — confirmed by direct testing, not just code reading (see "What is now testable").
  • ROUTES.WELCOME (/welcome) added to src/constants/routes.ts and to PUBLIC_PATHS. A comment on PUBLIC_PATHS (and in the middleware) calls out the startsWith/'/' trap explicitly for the next agent who touches either file.
  • The landing at client/src/app/[locale]/(public-routes)/welcome/:
    • page.tsx — thin RSC, generateMetadata (description, alternates.canonical/{locale}, openGraph). Deliberately does not override title — it inherits the root layout's default ("بالین‌یار" / "Balinyaar"), which is also how view-source tells it apart from the customer home's own shell.customer_app title when both serve at the same /{locale} URL depending on auth state.
    • WelcomeScreen.tsx — an async Server Component, not a 'use client' screen (no page in this app has needed that treatment before; here it's the point — zero query hooks anywhere in the tree). Sections, in order: hero (BrandMark + tagline + subtitle + CTA to /login), a static 5-tile category grid (CategoryTile, static i18n labels — see the CategoryTile change below), a 3-step how-it-works (step 2 renders the actual <EscrowNotice /> component, never a paraphrase), a static trust/verification explainer (identity/license/INO/bank — written from product/business/02-nurse-verification.md, since VerificationPanel requires a live nurseId-keyed badge fetch and can't be reused on a zero-data page), a nurse-recruitment CTA linking to /login?role=nurse, and a footer (terms/privacy links, a static contact note, no duplicate locale switcher — PublicLayout's corner strip already provides one for every public route including this one).
    • opengraph-image.tsxnext/og's ImageResponse, a brand-mark composition (teal background, the logo shape reproduced in plain divs, terracotta accent dot, "Balinyaar" wordmark) at 1200×630. Latin-onlyImageResponse's bundled fallback font doesn't cover Persian glyphs, and embedding a Mikhak font buffer wasn't verified working in the time available; flagged as a follow-up, not silently skipped.
  • CategoryTile gained an href prop (client/src/components/CategoryTile/CategoryTile.tsx): when given, the tile renders via ButtonBase's component swap to AppLink (the tile itself becomes the anchor) instead of a click handler — the same polymorphic pattern AppButton already uses, so there's no button nested inside a link (invalid HTML) and no new client wrapper component needed just to make a static tile navigate. Existing onClick/selected callers are unaffected. Test coverage added (CategoryTile.test.tsx): renders as a link with the given href, and no button role is present when href is set.
  • SEO/metadata infrastructure:
    • SITE_URL constant (client/src/config.ts, from NEXT_PUBLIC_SITE_URL, falls back to http://localhost:3000) — the root layout's generateMetadata now sets metadataBase: new URL(SITE_URL), so every child page's relative OG/canonical URLs resolve absolutely.
    • src/app/robots.ts replaces the old contradictory static public/robots.txt (deleted): allows the public surface, disallows every private route root (nurse, admin, partner, bookings, patients, addresses, wallet, profile, support, notifications, select-role, onboarding, search) for both locales via a wildcard pattern, points at sitemap.xml.
    • src/app/sitemap.ts — public routes only (/, /login, /terms, /privacy) × both locales, with hreflang alternates.

What is now testable (and exactly how)

Important caveat first: the dev server (npm run dev, Turbopack) in this sandbox did not reliably invoke middleware for the literal root path / — verified down to a minimal matcher: ['/'] control middleware that unconditionally returned a JSON body and still never fired for / (a sibling /ping path with the identical matcher also silently fell back to a stale cached render instead of hitting the handler). This reproduced even after full .next wipes and process restarts, and is not specific to this phase's logic — a production build (npm run build + npm run start) exhibits none of it and confirms the intended behavior end-to-end (below). Treat this as a known dev-server quirk in this Next 16.2.9/Turbopack build, not a defect in the shipped code; a human should re-confirm with npm run dev on their own machine before assuming it's universal.

Verified against a production server (NEXT_PUBLIC_API_URL=... npm run build && npm run start):

  1. GET / (no cookies) → 307Location: /fa (next-intl's own redirect, untouched).
  2. GET /fa (no cookies) → 200, response header x-middleware-rewrite: /fa/welcome; body's <title> is the bare default "بالین‌یار" (not the customer app's "اپلیکیشن خانواده | بالین‌یار") and contains the hero copy/CTA — the landing, not the customer home.
  3. GET /fa/welcome directly → 200, same landing content.
  4. GET /en (no cookies) → 200, lang="en" dir="ltr", no Mikhak reference, title "Balinyaar".
  5. With a valid, unexpired access_token cookie (forged payload for testing — isTokenAlive only checks exp, never the signature, by design): GET /fa200 with <title> = "اپلیکیشن خانواده | بالین‌یار" — the customer home, confirming an authenticated / is untouched. GET /fa/welcome307/fa/ (redirected away from the marketing page). GET /fa/bookings200 (private route now reachable).
  6. Without the cookie: GET /fa/bookings307/fa/login?next=%2Fbookings — the private-route gate and returnUrl capture are unchanged.
  7. GET /robots.txt → the new allow/disallow rules + Sitemap: .../sitemap.xml.
  8. GET /sitemap.xml → the 4 public paths × 2 locales with hreflang alternates.
  9. GET /fa view-source → og:title, og:description, og:image (an absolute URL), og:image:width/height (1200×630), canonical/fa.
  10. GET the opengraph-image route directly → 200, Content-Type: image/png; verified the bytes are a real 1200×630 PNG (PNG image data, 1200 x 630, 8-bit/color RGBA).

npm run check (type + lint + lint:copy) is green. npm run test:ci — all 115 suites / 527 tests pass, including the extended CategoryTile.test.tsx.

Not independently verified in this session (no running backend, no browser): the actual OTP login→RoleRouter round trip after tapping the hero CTA (the login screen itself is unchanged by this phase); visual dark-mode/RTL inspection of the landing (code follows the same token/RTL conventions as every other screen, but wasn't eyeballed in a real browser).

What is mocked / waiting on a real service

Nothing new. Tiers (a)+(b) are 100% static content — no service calls, no mock flags, no services/{domain} seam touched.

Contracts

  • Consumed: none (no data fetching on this page).
  • Filed (proposals only, not built against):
    • REQ-066 — public (anonymous), rate-limited nurse-search read, for a future tier-(c) guest search screen.
    • REQ-067 — public (anonymous), privacy-reviewed nurse-profile read, for a future tier-(c) guest profile screen.
    • Both in for-backend.md, status open; each explicitly says the frontend has not built anything against it.

Docs updated

  • product/notes/open-questions.md — the §3.1 decision + rationale (and the docs HTML was regenerated: cd product && node build-docs.mjs).
  • client/CLAUDE.md:
    • Project Structure — middleware.ts's description (the rewrite + the bare-root matcher-entry gotcha), src/app/robots.ts/sitemap.ts, the whole (public-routes)/welcome/ subtree, and CategoryTile's new href mode.
    • Per-page metadata section — metadataBase/SITE_URL and the opengraph-image.tsx convention.
    • i18n section — the welcome namespace description.
    • Route Constants section — the PUBLIC_PATHS/'/' trap, spelled out again at the point a future agent is most likely to be editing.

Follow-ups for later phases

  • Tier (c) (guest search + public nurse profiles) — REQ-066/067 need a backend phase plus an explicit privacy sign-off on the nurse-profile field list before any guest-facing search/profile screen is built. Do not build against a guessed shape.
  • OG image Persian variant — currently Latin-only by deliberate scope cut (see opengraph-image.tsx's own comment). A follow-up could embed a Mikhak font buffer (fs.readFile the existing src/app/fonts/Mikhak-Bold.woff2) and verify Satori/next/og renders Persian glyphs correctly in a real running server before shipping it — not verified in this session.
  • Dev-server root-path quirk — if a future phase also needs middleware to act on the literal / segment, re-confirm on a real (non-sandboxed) npm run dev whether the matcher gotcha reproduces there too; if so, the matcher: ['/', '/((?!_next|_vercel|api|.*\\..*).*)'] fix already applied here covers it, but worth a second look outside this session's environment.
  • Footer legal links — phase 3 had already shipped /terms//privacy by the time this phase ran, so the footer links to both (no graceful-omission gap to report).
  • Verification-explainer reuse — phase 4/8 had already shipped, but their VerificationPanel needs a live nurseId badge query and can't be reused as-is on a zero-fetch landing; the trust section here is independently written static copy sourced from the same product doc. A future pass could extract a shared static label set (step_identity_kyc, step_moh_competency_license, … already exist in the verification namespace) if keeping the two copies in sync becomes a problem.

Save memory note

See MEMORY.mdui_phase_13_public_front_door.md for the durable cross-session note (the rewrite-not-redirect mechanic, the PUBLIC_PATHS/'/' trap, the dev-server root-path quirk, tier (c) status).