Files
baya-monorepo/archive/post-phase/ui/ui-phase-13-public-front-door.md
T
2026-08-02 18:48:32 +03:30

21 KiB
Raw Blame History

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 · 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 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, 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).
  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 and Phase 2; the primitives kit + per-route metadata groundwork from Phase 1.
  • 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 has already run: the terms/privacy pages and the login returnUrl capture. If Phase 4 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 — 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 — 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 — the design contract (invoke the skill; §7 non-negotiables all apply here).
  • Product: ../../../product/overview/platform-summary.md (the four ground truths — everything the landing claims must trace to them) and ../../../product/business/index.md (verification + escrow rules, so marketing copy is honest). ../../../product/notes/open-questions.md is where §3.1's decision gets recorded.
  • The REQ tracker: ../../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 (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'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.tsxdo 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 (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 (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:

  • 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 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.