ui phase 13
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# 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](../../../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 itself** — `client/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.tsx` — `next/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-only** — `ImageResponse`'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) → `307` → `Location: /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 /fa` → `200` with `<title>` =
|
||||
`"اپلیکیشن خانواده | بالینیار"` — the **customer home**, confirming an authenticated `/` is
|
||||
untouched. `GET /fa/welcome` → `307` → `/fa/` (redirected away from the marketing page). `GET
|
||||
/fa/bookings` → `200` (private route now reachable).
|
||||
6. Without the cookie: `GET /fa/bookings` → `307` → `/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](../frontend/requests/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.md` → `ui_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).
|
||||
Reference in New Issue
Block a user