# 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 `` 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.