21 KiB
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 therobots.txtis 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 · 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):
- There is no public surface.
client/src/app/[locale]/(public-routes)/contains exactly two files:layout.tsxandlogin/page.tsx. Home, search, results, and nurse profiles are all wrapped inRoleGuard(customer)via(customer)/layout.tsx(audit/customer-storefront.md, problem #1). - The middleware sends every guest to
/login.client/middleware.ts:23-30checksPUBLIC_PATHS.some((p) => pathWithoutLocale.startsWith(p))andclient/src/constants/routes.ts:169definesPUBLIC_PATHS: string[] = [ROUTES.LOGIN]— so an unauthenticated hit on/307s to/{locale}/login. Note thestartsWithmatching:'/'can never be added toPUBLIC_PATHS(every path starts with/, which would silently un-gate the whole app). /belongs to the customer app. The(customer)route group has no URL segment, so(customer)/page.tsxis the root route. A secondpage.tsxcannot also resolve to/(Next parallel-page collision), so the landing must be served by a middleware rewrite, not a sibling page.- One static
<title>for ~60 routes. The only metadata export in the app issrc/app/[locale]/layout.tsx:54-58('Balinyaar | بالینیار'+ placeholder description). NogenerateMetadata, no OpenGraph, anywhere (audit/cross-cutting-ux.md). client/public/robots.txtexists but is contradictory starter content — twoUser-agent: *blocks,Disallow: /private/(a path that does not exist in this app) followed byAllow: /. 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) — takeslabel+iconKeyprops 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 («مبلغ بهصورت امانی نزد بالینیار میماند…»), andTrustBadge'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 honestlang/dirper 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 toROUTES.HOME=/),RoleGuardon 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
returnUrlcapture. 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, thePUBLIC_PATHScheck, 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 wrappingPublicLayout— 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.tsxis 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 andpathWithoutLocale === '/',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 inROUTES) toPUBLIC_PATHS. Never add'/'— thestartsWithmatch would make every route public (§1.2). - An authenticated user hitting
/welcomedirectly → redirect to/(a signed-in customer lands on their home, exactly as today). Authenticated/is untouched —(customer)/page.tsxkeeps serving it,RoleGuard/RoleRouterbehavior unchanged. - Preserve the middleware's existing behavior for every other path: the locale-header injection, the
next-intl
Link: alternatehreflang headers it copies through, and the redirect-to-login for private paths (including thereturnUrlcapture if Phase 3 has added it — test that flow after your change).
Landing sections (mobile-first, in order):
- 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. - Category grid — reuse
CategoryTilewith static i18n labels keyed to the fiveKNOWN_CATEGORY_ICONSkeys (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 plainAppLink-wrapped tiles to keep the page RSC. - How it works — 3 steps: «جستجوی پرستار تاییدشده» → «پرداخت امن امانی» → «مراقبت با خیال راحت».
Step 2's supporting text reuses the
EscrowNoticeverbatim copy (same i18n key or the component itself) — never a paraphrase. - 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.
- Nurse recruitment — «پرستار هستید؟ به بالینیار بپیوندید» with a secondary CTA to
/loginwith the nurse intent the login screen already supports (the A1/B1 role switch from f1/phase 3). - 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
generateMetadataon the landing (and login + any phase-3 public pages if present): localized title/description viagetTranslations, a title template%s | بالینیار, andalternates.canonicalpointing at/{locale}for the landing (the rewrite means/welcomeand/serve the same content — canonicalize on/).- OpenGraph:
og:title/og:descriptionper locale + one static OG image (1200×630) atclient/public/og/balinyaar-og.png, built from the phase-0 brand mark on the brand teal/cream. SetmetadataBasefrom an env constant (e.g.NEXT_PUBLIC_SITE_URL) — never a hard-coded origin. - Replace the contradictory
client/public/robots.txtwith an app-routersrc/app/robots.ts(delete the static file), allowing the public routes and disallowing the private roots (/*/nurse,/*/admin,/*/partner,/*/bookings, …); addsrc/app/sitemap.tslisting public routes only (/fa,/en, login, terms/privacy when they exist) with locale alternates. lang/dirper locale is already correct insrc/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.tsare metadata routes, not layouts — they are safe atsrc/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/resolveRoleDestinationsemantics unchanged; the middleware remains the auth gate. The only new public surface is what §3.2/§3.3 name. - The
PUBLIC_PATHSstartsWithtrap: 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
returnUrlcapture (if shipped) and next-intl locale detection/normalization (the 307/308 early-return and the copiedLinkhreflang headers) must survive the middleware edit. - No marketing claims the product can't honor. Escrow copy is the
EscrowNoticeverbatim 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.jsonandmessages/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 theAppIconregistry,App*wrappers before raw MUI. Any new shared component gets a co-located*.test.tsx. - Never add a layout above
[locale]—lang/dirwould freeze on the default locale (the root layout's comment explains why).robots.ts/sitemap.tsatsrc/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 checkgreen;npm run test:cigreen for any touched/added shared components;en.json/fa.jsonin 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;/welcomewhile authenticated redirects to/. - Every other unauthenticated private path still redirects to
/login(spot-check/bookings,/nurse), and login → role routing works exactly as before (includingreturnUrlif 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 correctlang/dir;/robots.txtand/sitemap.xmlserve the §3.3 content (starterrobots.txtdeleted). - The landing performs no client-side data fetching (Network tab: no
/api/v1/*calls while logged out) and no marketing string contradictsEscrowNotice/TrustBadgesemantics. - 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)
- In a private/incognito window, open
http://localhost:3000→ normalized to/fa, the landing renders (URL stays/fa, no/loginredirect, no/welcomein the address bar): hero + tagline, 5 category tiles, 3-step how-it-works with the escrow sentence, trust explainer, nurse CTA, footer. - Switch to
/en→ LTR landing, English copy, system font (Network tab: no Mikhak woff2). Toggle dark mode → all sections stay token-correct. - Still logged out, visit
/fa/bookings→ redirected to/fa/loginexactly as before this phase. - Tap the hero CTA →
/login; complete the seeded customer OTP login → land on/and see the customer home (not the landing). Manually revisit/fa/welcomewhile logged in → redirected to/fa. - Tap «پرستار هستید؟…» while logged out → login screen in its nurse-intent mode (B1 switch preselected).
- View source on the logged-out landing: localized
<title>(… | بالینیارon/fa), meta description,og:imageabsolute URL,lang="fa" dir="rtl"(andlang="en" dir="ltr"on/en). - 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. - 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.mdProject Structure: the new(public-routes)/welcomeroute (and the middleware's rewrite behavior in the routing/middleware description), plusrobots.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 thePUBLIC_PATHStrap 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'/'inPUBLIC_PATHS), tier (c) is REQ-gated and unbuilt.