# 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 0–2](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.
