Files
baya-monorepo/dev/post-phase/ui/ui-phase-13-public-front-door.md
T
2026-07-17 13:22:04 +03:30

277 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI Phase 13 — Public front door (optional)
> **Mission (this phase is OPTIONAL — the app is complete without it):** today the entire anonymous web
> surface of a trust-first marketplace is `/login`. A family evaluating Balinyaar cannot see a single
> service, trust signal, or explanation of escrow before creating an account; there is no marketing page,
> no per-page metadata, and the `robots.txt` is starter junk. This phase builds the public front door —
> a landing at `/` for unauthenticated visitors, honest how-it-works and trust sections, and a real
> SEO/metadata surface — and it starts by **framing the guest-browse product decision** (how deep the
> anonymous experience goes) so scope is decided, not drifted into.
>
> **Track:** frontend · **Depends on:** [Phases 02](ui-phase-2-shells-and-navigation.md) · a product
> decision on guest browse (§3.1) · **Unlocks:** marketing, SEO, and an acquisition funnel that starts
> before login
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**
## 1. Context — where this sits
Balinyaar's product *is* trust — verified nurses, escrow-held payments, payout only after a confirmed
check-out — yet none of that story exists outside an authenticated session. Diagnosed current state
(all verified in code):
1. **There is no public surface.** `client/src/app/[locale]/(public-routes)/` contains exactly two
files: `layout.tsx` and `login/page.tsx`. Home, search, results, and nurse profiles are all wrapped
in `RoleGuard(customer)` via `(customer)/layout.tsx` ([audit/customer-storefront.md](audit/customer-storefront.md), problem #1).
2. **The middleware sends every guest to `/login`.** `client/middleware.ts:23-30` checks
`PUBLIC_PATHS.some((p) => pathWithoutLocale.startsWith(p))` and `client/src/constants/routes.ts:169`
defines `PUBLIC_PATHS: string[] = [ROUTES.LOGIN]` — so an unauthenticated hit on `/` 307s to
`/{locale}/login`. Note the **`startsWith` matching**: `'/'` can never be added to `PUBLIC_PATHS`
(every path starts with `/`, which would silently un-gate the whole app).
3. **`/` belongs to the customer app.** The `(customer)` route group has no URL segment, so
`(customer)/page.tsx` *is* the root route. A second `page.tsx` cannot also resolve to `/` (Next
parallel-page collision), so the landing must be served by a **middleware rewrite**, not a sibling page.
4. **One static `<title>` for ~60 routes.** The only metadata export in the app is
`src/app/[locale]/layout.tsx:54-58` (`'Balinyaar | بالین‌یار'` + placeholder description). No
`generateMetadata`, no OpenGraph, anywhere ([audit/cross-cutting-ux.md](audit/cross-cutting-ux.md)).
5. **`client/public/robots.txt` exists but is contradictory starter content** — two `User-agent: *`
blocks, `Disallow: /private/` (a path that does not exist in this app) followed by `Allow: /`. There
is no sitemap.
**What already exists (do not rebuild):**
- The **brand mark, theme pass, and de-startered public shell** from
[Phase 0](ui-phase-0-design-language.md) and [Phase 2](ui-phase-2-shells-and-navigation.md); the
primitives kit + per-route metadata groundwork from [Phase 1](ui-phase-1-primitives-and-states.md).
- `CategoryTile` (`client/src/components/CategoryTile/CategoryTile.tsx`) — takes `label` + `iconKey`
props with a safe icon fallback (`KNOWN_CATEGORY_ICONS`: `elderly`, `post_surgery`, `infant`,
`chronic`, `companionship`); it works with **static i18n labels**, no API needed.
- `EscrowNotice` (`client/src/components/EscrowNotice/EscrowNotice.tsx`) — the **product-mandated
verbatim escrow copy** («مبلغ به‌صورت امانی نزد بالین‌یار می‌ماند…»), and `TrustBadge`'s three
honest states.
- The brand tagline is already a key: `common.brand_tagline` = «مراقبت مطمئن در خانه»
(`client/messages/fa.json:54`).
- The `[locale]` root layout already renders honest `lang`/`dir` per locale and loads Mikhak fa-only
(`src/app/[locale]/layout.tsx`) — this phase builds **on top of** it, never above it.
- The auth machinery: middleware gate, `RoleRouter`/`resolveRoleDestination`
(`client/src/services/auth/routing.ts` — a customer resolves to `ROUTES.HOME` = `/`), `RoleGuard` on
the four shells (refinement phase 2). None of it changes semantics in this phase.
- If [Phase 3](ui-phase-3-auth-and-first-run.md) has already run: the terms/privacy pages and the
login `returnUrl` capture. If [Phase 4](ui-phase-4-customer-storefront.md) has run: the
verification-explainer content (whatever component name its report gives it). Reuse both; see §3.2.
## 2. Required reading (do this first)
- [audit/customer-storefront.md](audit/customer-storefront.md) — problem #1 (no public storefront) and
the "Public landing + guest browse" opportunity this phase implements; the **Keep** list you must not
regress (token discipline, four data states, trust honesty, money handling).
- [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the metadata/404/route-chrome findings and
the "Public landing + public nurse profiles" opportunity; its **Keep** list (RTL habits, Mikhak
fa-only loading, locale/dir wiring reasoning).
- Code: `client/middleware.ts` (the whole file — the i18n 307/308 early-return, the `PUBLIC_PATHS`
check, the locale header), `client/src/constants/routes.ts` (`ROUTES`, `PUBLIC_PATHS`),
`client/src/app/[locale]/layout.tsx` (root metadata + the "why no layout above [locale]" comment),
`client/src/app/[locale]/(public-routes)/layout.tsx` (client layout wrapping `PublicLayout` — RSC
children still render server-side), `(customer)/page.tsx` (the authenticated home that owns `/`),
`client/src/services/auth/routing.ts` + `client/src/components/auth/RoleRouter.tsx`,
`client/src/components/CategoryTile/CategoryTile.tsx`, `client/src/components/EscrowNotice/EscrowNotice.tsx`.
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
the design contract (invoke the skill; §7 non-negotiables all apply here).
- Product: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
(the four ground truths — everything the landing claims must trace to them) and
[../../../product/business/index.md](../../../product/business/index.md) (verification + escrow rules,
so marketing copy is honest). [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
is where §3.1's decision gets recorded.
- The REQ tracker: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
— REQ-001…038 existed before this chain; earlier UI phases may have appended more. **Check the file
for the next free number before filing.**
## 3. Scope — build this
### 3.1 The product decision — do this FIRST, and write it down
Frame guest-browse depth as three tiers and get an explicit decision before building:
- **(a) Landing only** — a marketing page at `/`; zero data, zero new endpoints.
- **(b) Landing + static public pages** — (a) plus category/how-it-works content pages; still zero
data (the category grid is static i18n content, not the catalog API).
- **(c) Guest search + public nurse profiles** — read-only anonymous variants of the phase-4 search
results and nurse-profile screens. **Requires public read endpoints that do not exist** (all search
and profile reads sit behind the auth middleware and cookie-bearing `clientFetch`), so tier (c) is
REQ-gated backend work plus a privacy review of which nurse fields may be exposed logged-out.
**Recommendation this phase encodes: ship (a)+(b) now; frame (c) as a REQ-gated follow-up** (file the
REQs as proposals, §3.4 — do not build guest search against endpoints that don't exist). Record the
decision (chosen tier, rationale, what tier (c) would need) in this phase's report **and** append it to
[../../../product/notes/open-questions.md](../../../product/notes/open-questions.md) (edit the `.md`;
the `.html` view is generated — `cd product && node build-docs.mjs`).
### 3.2 Public landing at `/` for unauthenticated visitors
**Routing mechanics (get this exactly right):**
- Create the landing as an **RSC** at `client/src/app/[locale]/(public-routes)/welcome/page.tsx`
(the client `(public-routes)/layout.tsx` is fine — RSC children of a client layout still render on
the server).
- In `client/middleware.ts`, after the i18n 307/308 early-return: if the request is **unauthenticated
and `pathWithoutLocale === '/'`**, `NextResponse.rewrite()` to `/{locale}/welcome` — the visitor sees
the landing **at the URL `/`** (good for SEO canonical). Do **not** redirect.
- Add `/welcome` (a named constant in `ROUTES`) to `PUBLIC_PATHS`. **Never add `'/'`** — the
`startsWith` match would make every route public (§1.2).
- An **authenticated** user hitting `/welcome` directly → redirect to `/` (a signed-in customer lands
on their home, exactly as today). Authenticated `/` is untouched — `(customer)/page.tsx` keeps
serving it, `RoleGuard`/`RoleRouter` behavior unchanged.
- Preserve the middleware's existing behavior for every other path: the locale-header injection, the
next-intl `Link: alternate` hreflang headers it copies through, and the redirect-to-login for private
paths (including the `returnUrl` capture if Phase 3 has added it — test that flow after your change).
**Landing sections (mobile-first, in order):**
1. **Hero** — brand mark (phase 0), the existing tagline `common.brand_tagline`
(«مراقبت مطمئن در خانه»), one supporting sentence, and a primary CTA to `/login` (label e.g.
«شروع کنید»). No carousel, no stock photography of fake nurses.
2. **Category grid** — reuse `CategoryTile` with **static i18n labels** keyed to the five
`KNOWN_CATEGORY_ICONS` keys (elderly / post_surgery / infant / chronic / companionship). Each tile
links to `/login` (tier a+b: intent capture, not guest search). This section needs a small client
wrapper only if tiles navigate via router — prefer plain `AppLink`-wrapped tiles to keep the page RSC.
3. **How it works — 3 steps:** «جستجوی پرستار تاییدشده» → «پرداخت امن امانی» → «مراقبت با خیال راحت».
Step 2's supporting text reuses the `EscrowNotice` verbatim copy (same i18n key or the component
itself) — never a paraphrase.
4. **Verification/trust explainer** — the "what we verify" story (identity, nursing license, INO
membership). If Phase 4 has shipped its verification-explainer content, reuse those keys/components;
if this phase runs before Phase 4, write the section from
[../../../product/business/index.md](../../../product/business/index.md)'s pipeline and note in the
report that Phase 4 should fold its explainer into the same keys.
5. **Nurse recruitment** — «پرستار هستید؟ به بالین‌یار بپیوندید» with a secondary CTA to `/login` with
the nurse intent the login screen already supports (the A1/B1 role switch from f1/phase 3).
6. **Footer** — links to the terms/privacy pages if Phase 3 has shipped them (omit gracefully and note
it in the report if not), a contact affordance, and the locale switcher from the phase-2 public shell.
### 3.3 SEO & metadata for the public surface
- `generateMetadata` on the landing (and login + any phase-3 public pages if present): **localized**
title/description via `getTranslations`, a title template `%s | بالین‌یار`, and
`alternates.canonical` pointing at `/{locale}` for the landing (the rewrite means `/welcome` and `/`
serve the same content — canonicalize on `/`).
- **OpenGraph:** `og:title`/`og:description` per locale + one static OG image (1200×630) at
`client/public/og/balinyaar-og.png`, built from the phase-0 brand mark on the brand teal/cream. Set
`metadataBase` from an env constant (e.g. `NEXT_PUBLIC_SITE_URL`) — never a hard-coded origin.
- **Replace** the contradictory `client/public/robots.txt` with an app-router `src/app/robots.ts`
(delete the static file), allowing the public routes and disallowing the private roots
(`/*/nurse`, `/*/admin`, `/*/partner`, `/*/bookings`, …); add `src/app/sitemap.ts` listing **public
routes only** (`/fa`, `/en`, login, terms/privacy when they exist) with locale alternates.
- `lang`/`dir` per locale is already correct in `src/app/[locale]/layout.tsx` — **do not regress it,
and NEVER add a layout (or robots/sitemap-driven layout tricks) above `[locale]`** (golden rule #1;
`robots.ts`/`sitemap.ts` are metadata routes, not layouts — they are safe at `src/app/`).
### 3.4 Tier (c): guest search + public nurse profiles (DEFERRED unless explicitly approved in §3.1)
If — and only if — the §3.1 decision approves tier (c): build guest search results and a public nurse
profile as **read-only variants of the phase-4 screens** (no booking CTA past login, no address/PII),
behind new public endpoints. Either way, **file the REQs now as proposals** in
[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (next free numbers —
check the tracker, ≥ REQ-039):
- **Public search read** — anonymous, rate-limited variant of the nurse search (verified-only invariant
preserved; no customer-context fields).
- **Public nurse profile read** — privacy-reviewed field set (display name, photo, verified badge
state, rating aggregate, service/price rows; **never** phone, exact areas, or document data).
Everything else about tier (c) — routes, guest-to-login handoff at the «درخواست رزرو» tap — is
**(DEFERRED → a follow-up phase once the REQs are delivered)**.
### 3.5 Performance sanity
The landing is the one page where first paint is the product. **RSC-first, zero client data fetching
above the fold** (the whole page needs no API at tier a+b), no new client-side libraries, images through
`AppImage`/`next/image` with explicit dimensions, and the per-locale font strategy untouched (Mikhak is
already fa-only, `preload: false` — do not "optimize" it into loading for `/en`). Any interactive island
(e.g. a locale switcher) stays a leaf client component.
## 4. Mocks & seams in this phase
**None.** Tiers (a)+(b) are static content — no service calls, no new `services/{domain}` seams, no mock
flags. The REQ posture: backend gaps become REQ entries appended to
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
(REQ-001…038 pre-date this chain; check the tracker for the next free number). This phase files the two
tier-(c) proposals in §3.4 and builds nothing against them.
## 5. Critical rules you must not get wrong
- **This phase is optional and must stay reversible.** If it is skipped or aborted mid-way, the app must
behave exactly as today (guests → `/login`). Keep the middleware change small and additive.
- **Auth boundaries untouched.** Private routes stay private; `RoleGuard`/`RoleRouter`/
`resolveRoleDestination` semantics unchanged; the middleware remains the auth gate. The only new
public surface is what §3.2/§3.3 name.
- **The `PUBLIC_PATHS` `startsWith` trap:** never add `'/'` — root goes public via the exact-match
rewrite branch, not the list (`client/middleware.ts:23`, `routes.ts:169`).
- **Do not break the login flow:** the phase-3 `returnUrl` capture (if shipped) and next-intl locale
detection/normalization (the 307/308 early-return and the copied `Link` hreflang headers) must survive
the middleware edit.
- **No marketing claims the product can't honor.** Escrow copy is the `EscrowNotice` verbatim string;
"verified" copy describes the real pipeline; **no invented numbers** («۵۰۰۰ پرستار», fake ratings,
fake testimonials). Trust-first means the landing is honest-first.
- **Copy discipline:** every string in **both** `messages/en.json` and `messages/fa.json`; new strings
use the ZWNJ brand spelling «بالین‌یار» (phase 12 canonicalizes the legacy spaced form — don't add
more of it); formal شما register.
- **Design contract:** tokens not hexes (`--bal-*` / palette keys), terracotta stays the single sparing
accent, RTL logical props only, dark mode verified, MUI v9 API only, icons via the `AppIcon` registry,
`App*` wrappers before raw MUI. Any new **shared** component gets a co-located `*.test.tsx`.
- **Never add a layout above `[locale]`** — `lang`/`dir` would freeze on the default locale (the root
layout's comment explains why). `robots.ts`/`sitemap.ts` at `src/app/` are fine; a layout is not.
- **Fetch/cookies rules untouched** — no raw `fetch()`, no cookie reads outside `@/lib/cookies/*`
(at tier a+b you should need neither).
## 6. Definition of Done
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
- [ ] The §3.1 decision is written down (report + `product/notes/open-questions.md`) **before** the
landing was built, and the built scope matches it.
- [ ] `npm run check` green; `npm run test:ci` green for any touched/added shared components;
`en.json`/`fa.json` in sync.
- [ ] An **unauthenticated** visit to `/` renders the landing at the URL `/` (rewrite, not redirect),
in both locales; an **authenticated** customer at `/` still gets the customer home; `/welcome`
while authenticated redirects to `/`.
- [ ] Every other unauthenticated private path still redirects to `/login` (spot-check `/bookings`,
`/nurse`), and login → role routing works exactly as before (including `returnUrl` if phase 3 ran).
- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and on mobile + desktop
widths for the landing (it is the page strangers judge the product by).
- [ ] View-source of the landing shows the localized `<title>`/description, OG tags with an absolute
image URL, and correct `lang`/`dir`; `/robots.txt` and `/sitemap.xml` serve the §3.3 content
(starter `robots.txt` deleted).
- [ ] The landing performs no client-side data fetching (Network tab: no `/api/v1/*` calls while
logged out) and no marketing string contradicts `EscrowNotice`/`TrustBadge` semantics.
- [ ] Tier-(c) REQs filed as proposals in the tracker with the next free numbers.
## 7. How to test (what a human can verify after this phase)
1. In a private/incognito window, open `http://localhost:3000` → normalized to `/fa`, the **landing**
renders (URL stays `/fa`, no `/login` redirect, no `/welcome` in the address bar): hero + tagline,
5 category tiles, 3-step how-it-works with the escrow sentence, trust explainer, nurse CTA, footer.
2. Switch to `/en` → LTR landing, English copy, system font (Network tab: no Mikhak woff2). Toggle dark
mode → all sections stay token-correct.
3. Still logged out, visit `/fa/bookings` → redirected to `/fa/login` exactly as before this phase.
4. Tap the hero CTA → `/login`; complete the seeded customer OTP login → land on `/` and see the
**customer home** (not the landing). Manually revisit `/fa/welcome` while logged in → redirected to `/fa`.
5. Tap «پرستار هستید؟…» while logged out → login screen in its nurse-intent mode (B1 switch preselected).
6. View source on the logged-out landing: localized `<title>` (`… | بالین‌یار` on `/fa`), meta
description, `og:image` absolute URL, `lang="fa" dir="rtl"` (and `lang="en" dir="ltr"` on `/en`).
7. Open `http://localhost:3000/robots.txt` → the new rules (private roots disallowed) and the sitemap
reference; `http://localhost:3000/sitemap.xml` → public routes only, both locales.
8. Confirm in the report/tracker: the §3.1 decision recorded, the two tier-(c) REQs filed, and (if
phase 3 hadn't run) the noted footer-legal-links gap.
## 8. Hand off & document (close the phase)
- Update `client/CLAUDE.md` **Project Structure**: the new `(public-routes)/welcome` route (and the
middleware's rewrite behavior in the routing/middleware description), plus `robots.ts`/`sitemap.ts`.
- Append the §3.1 decision to [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
and regenerate the docs HTML (`cd product && node build-docs.mjs`).
- Write the frontend report at `dev/shared-working-context/reports/ui-phase-13-report.md`: the tier
decision + rationale, the middleware change (with the `PUBLIC_PATHS` trap called out for future
agents), sections shipped, REQ numbers filed, and any graceful omissions (footer legal links,
verification-explainer reuse status).
- List the REQs filed (tier-(c) public search read + public nurse profile read) with their final numbers.
- Save a memory note per operating-rules §8: the public front door exists, `/` now forks by auth via a
middleware **rewrite** (never `'/'` in `PUBLIC_PATHS`), tier (c) is REQ-gated and unbuilt.