# Public Landing Routing Refactor **Date:** 2026-05-13 **Status:** Planned — pending execution **Author:** session handoff **Driver:** Stripe activation review — Stripe's compliance crawler cannot view `resolutionflow.com` ## Problem The bare apex URL `https://resolutionflow.com/` serves a Vite SPA shell (`
` and a module script — see [`frontend/index.html`](../../frontend/index.html)) and the React Router config in [`frontend/src/router.tsx`](../../frontend/src/router.tsx) mounts `/` behind ``. The public marketing landing page lives at `/landing`. For unauthenticated visitors, the flow is: 1. Browser fetches `/` → empty HTML shell. 2. JS executes, auth store hydrates as not-authenticated. 3. `ProtectedRoute` client-side ``. Stripe (and many automated compliance crawlers) fetch the apex without executing JS, or don't reliably wait through a client-side redirect chain. They see no business content, no terms link, no pricing — and decline review. ## Goal Make `/` serve the public landing page directly so the apex URL renders marketing content immediately. Move the authenticated dashboard index (currently `QuickStartPage` at `/`) to `/home`. All other authenticated child routes (`/trees`, `/pilot`, `/admin/*`, `/account/*`, etc.) stay where they are — only the index page and the route grouping change. This is the architectural fix. If Stripe's reviewer still cannot see the site after this lands (i.e. their crawler executes zero JS), the documented next escalation is server-side prerendering of public routes via `vite-plugin-ssg` — captured below under Follow-ups. ## Approach ### Router restructure ([`frontend/src/router.tsx`](../../frontend/src/router.tsx)) Use a react-router *layout route* (no `path`, just an `element`) for the authenticated tree so children carry absolute paths and don't all need renaming: ```tsx // Public { path: '/', element: page(PublicLanding), errorElement: }, // Stale-bookmark redirect — keep for one release, delete in a follow-up { path: '/landing', element: }, // Authenticated app — layout route { element: , errorElement: , children: [ { path: '/home', element: page(QuickStartPage) }, { path: '/trees', element: page(TreeLibraryPage) }, { path: '/my-trees', element: page(MyTreesPage) }, // …all other existing children, unchanged (admin/*, account/*, pilot/*, …) ], }, ``` ### `PublicLanding` wrapper (no-flicker authed redirect) Authenticated users hitting `/` should not see marketing. Use a thin router-level wrapper so `LandingPage` stays a pure marketing component and there's no frame-flash before redirect: ```tsx function PublicLanding() { const isAuthed = useAuthStore(s => s.isAuthenticated); return isAuthed ? : ; } ``` ### Auth gate ([`frontend/src/components/layout/ProtectedRoute.tsx:25`](../../frontend/src/components/layout/ProtectedRoute.tsx#L25)) `` → ``. The `state.from` preservation stays. ### Reference updates (21 sites) **Post-login / post-onboarding destinations** | File | Line | Change | |---|---|---| | [`frontend/src/pages/OAuthCallbackPage.tsx`](../../frontend/src/pages/OAuthCallbackPage.tsx#L114) | 114 | `let dest = '/'` → `'/home'`; `'/?welcome=teammate'` → `'/home?welcome=teammate'` | | [`frontend/src/pages/welcome/WelcomeStep1.tsx`](../../frontend/src/pages/welcome/WelcomeStep1.tsx#L88) | 88 | `navigate('/')` → `navigate('/home')` | | [`frontend/src/pages/welcome/WelcomeStep2.tsx`](../../frontend/src/pages/welcome/WelcomeStep2.tsx#L72) | 72 | same | | [`frontend/src/pages/welcome/WelcomeStep3.tsx`](../../frontend/src/pages/welcome/WelcomeStep3.tsx#L194) | 194 | same | | [`frontend/src/pages/AssistantChatPage.tsx`](../../frontend/src/pages/AssistantChatPage.tsx#L2419) | 2419 | same | **Authenticated chrome (logo, mobile nav)** | File | Line | Change | |---|---|---| | [`frontend/src/components/layout/TopBar.tsx`](../../frontend/src/components/layout/TopBar.tsx#L66) | 66 | logo `to="/"` → `to="/home"` | | [`frontend/src/components/layout/AppLayout.tsx`](../../frontend/src/components/layout/AppLayout.tsx#L60) | 60 | mobile nav `path: '/'` → `'/home'` | | [`frontend/src/components/layout/AppLayout.tsx`](../../frontend/src/components/layout/AppLayout.tsx#L107) | 107 | logo `to="/"` → `to="/home"` | **Dashboard onboarding (has in-progress edits — layer carefully)** | File | Line | Change | |---|---|---| | [`frontend/src/components/dashboard/SetupChecklist.tsx`](../../frontend/src/components/dashboard/SetupChecklist.tsx#L54) | 54 | `path: '/'` → `'/home'` | | [`frontend/src/components/dashboard/NextStepCard.tsx`](../../frontend/src/components/dashboard/NextStepCard.tsx#L82) | 82 | `ctaPath: '/'` → `'/home'` | These two files already have uncommitted edits for the "Start a session" pulse/scroll onboarding fix from earlier this session. Layer onto whatever's there — don't overwrite. **Public page back-links** | File | Line | Change | |---|---|---| | [`frontend/src/pages/TermsPage.tsx`](../../frontend/src/pages/TermsPage.tsx#L10) | 10 | `to="/landing"` → `to="/"` | | [`frontend/src/pages/PoliciesPage.tsx`](../../frontend/src/pages/PoliciesPage.tsx#L10) | 10 | same | | [`frontend/src/pages/PrivacyPage.tsx`](../../frontend/src/pages/PrivacyPage.tsx#L10) | 10 | same | | [`frontend/src/pages/ContactPage.tsx`](../../frontend/src/pages/ContactPage.tsx#L10) | 10 | same | | [`frontend/src/pages/PromotionsPage.tsx`](../../frontend/src/pages/PromotionsPage.tsx#L10) | 10 | same | | [`frontend/src/pages/PublicTemplatesPage.tsx`](../../frontend/src/pages/PublicTemplatesPage.tsx#L171) | 171, 409 | same | ### robots.txt + sitemap.xml ([`frontend/public/`](../../frontend/public/)) Neither file exists today. Create both. **`frontend/public/robots.txt`** ``` User-agent: * Allow: / Allow: /terms Allow: /policies Allow: /privacy Allow: /contact Allow: /contact-sales Allow: /pricing Allow: /promotions Allow: /templates Disallow: /home Disallow: /trees/ Disallow: /my-trees Disallow: /pilot/ Disallow: /admin/ Disallow: /account/ Disallow: /script-builder Disallow: /scripts Disallow: /sessions Disallow: /analytics Disallow: /escalations Disallow: /queue Disallow: /review-queue Disallow: /network-diagrams Disallow: /kb-accelerator Disallow: /step-library Disallow: /tickets Disallow: /shares Disallow: /feedback Disallow: /welcome Disallow: /flow-assist Disallow: /dev/ Disallow: /flows/ Disallow: /guides Sitemap: https://resolutionflow.com/sitemap.xml ``` **`frontend/public/sitemap.xml`** — entries for `/`, `/pricing`, `/contact-sales`, `/contact`, `/templates`, `/terms`, `/privacy`, `/policies`, `/promotions`. Standard `` schema with `` and `` of `2026-05-13`. ### Open Graph + Twitter cards [`frontend/src/components/common/PageMeta.tsx`](../../frontend/src/components/common/PageMeta.tsx) already emits `og:title/description/type/site_name` and `twitter:card=summary/title/description`. Gaps: 1. **No `og:url`** in `PageMeta` — add a `url` prop that defaults to `window.location.href` (or take a canonical override). 2. **`twitter:card` is always `summary`** — when an `ogImage` is passed, emit `summary_large_image` instead. 3. **`LandingPage` doesn't pass `ogImage`** — currently no social preview image at all. Need a 1200×630 asset. Acceptable to ship a placeholder (logo on existing landing gradient) and flag for design polish. ### Tests **Update** | File | Change | |---|---| | [`frontend/src/components/layout/__tests__/AppLayout.test.tsx`](../../frontend/src/components/layout/__tests__/AppLayout.test.tsx) | `initialEntries={['/']}` → `['/home']` | | [`frontend/src/pages/__tests__/LandingPage.test.tsx`](../../frontend/src/pages/__tests__/LandingPage.test.tsx) | Keep `['/']` — now correct | **Add** `frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx` (or extend existing) — unauthenticated visit to `/home` should: - Redirect to `/` (not `/landing`). - Preserve original location in `state.from` so post-login flow can return the user to their intended destination. ## Out of scope / non-issues verified - **Service worker / PWA cache invalidation.** `vite.config.ts` has no `vite-plugin-pwa`, no `injectManifest`, no SW registration anywhere in `frontend/src`. The "PWA Icons" comment in `index.html` is iOS apple-touch-icon only. Vite's content-hashed bundles + browser HTTP cache handle invalidation. Flagged during review; no action needed. - **Backend redirects / CORS / OAuth.** Grep of `backend/` shows no hard-coded `/landing` or root-path redirects. OAuth callbacks render client-side via [`OAuthCallbackPage.tsx`](../../frontend/src/pages/OAuthCallbackPage.tsx). No backend changes required. ## Manual follow-ups (not code changes) - **PostHog dashboard audit.** [`frontend/src/main.tsx:20`](../../frontend/src/main.tsx#L20) sets `capture_pageview: true`, so `$pageview` auto-fires for every URL. After this ships, `$current_url ends with /` shifts meaning from "authenticated dashboard visit" to "anonymous marketing visit." Any saved PostHog insight or funnel keyed on `/` will silently mis-interpret. No in-code filters on `'/'` exist (grepped `lib/analytics.ts` and the wider tree). Sweep PostHog dashboards in the PostHog UI before merging this PR and update filters as needed. - **OG image asset.** Placeholder is acceptable to unblock Stripe; design polish can follow. ## Follow-ups (deferred — future PRs) - **Stripe SSR escalation.** If Stripe's reviewer still cannot see the site after this lands (i.e. their crawler executes zero JavaScript), the next step is server-side prerendering of public routes. Cheapest path: `vite-plugin-ssg` for static HTML output of `/`, `/pricing`, `/terms`, `/privacy`, `/policies`, `/contact`, `/contact-sales`, `/promotions`, `/templates`. Keeps the SPA architecture for the authed app. Larger move (only if needed): split marketing to a separate Astro/Next-static project at the apex and move the SPA to `app.resolutionflow.com`. Do not pre-optimize for this. Capture as a decision in [`.ai/DECISIONS.md`](../../.ai/DECISIONS.md) when this PR lands. - **Delete `/landing` redirect alias** after one release cycle. ## Rollout / sequencing 1. Router restructure + `PublicLanding` wrapper. 2. 21 reference updates (post-login, chrome, dashboard onboarding, public page back-links). 3. `ProtectedRoute` redirect target flip. 4. `robots.txt`, `sitemap.xml`. 5. `PageMeta` enhancements (`og:url`, `summary_large_image` toggle). 6. OG image asset, wired into `LandingPage`. 7. Test updates + new `ProtectedRoute` test. 8. Manual: PostHog dashboard sweep. 9. `.ai/DECISIONS.md` entry noting SSR-prerender as next-escalation path. 10. Single PR, single deploy. ## Risk Necessary but not necessarily sufficient for Stripe's crawler. If their bot executes zero JS, even a `/`-routed `LandingPage.tsx` is invisible — Vite still client-renders. The Follow-ups section above captures the escalation path.