Files
resolutionflow/docs/plans/2026-05-13-public-landing-routing-refactor.md
Michael Chihlas e5b26245ca
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m45s
CI / e2e (pull_request) Successful in 10m13s
CI / backend (pull_request) Successful in 11m27s
docs: add architecture reports, public-landing routing plan, build-a-page tutorial, self-serve signup phase-2 design
- docs/architecture/: god-node map + report (2026-05-06), workflows.json/html + analysis snapshot
- docs/plans/2026-05-13-public-landing-routing-refactor.md
- docs/tutorials/build-a-page.md
- abc-feat-self-serve-signup-phase-2-design-20260507-112020.md (root)

Core dumps (core.144926, core.145678, docs/architecture/core.1392564) and
agent .remember/ state are intentionally left untracked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:59:29 -04:00

267 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
(`<div id="root"></div>` 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 `<ProtectedRoute>`. 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 `<Navigate to="/landing">`.
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: <RouteError /> },
// Stale-bookmark redirect — keep for one release, delete in a follow-up
{ path: '/landing', element: <Navigate to="/" replace /> },
// Authenticated app — layout route
{
element: <ProtectedRoute><AppLayout /></ProtectedRoute>,
errorElement: <RouteError />,
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 ? <Navigate to="/home" replace /> : <LandingPage />;
}
```
### Auth gate ([`frontend/src/components/layout/ProtectedRoute.tsx:25`](../../frontend/src/components/layout/ProtectedRoute.tsx#L25))
`<Navigate to="/landing" state={{ from: location }} replace />`
`<Navigate to="/" state={{ from: location }} replace />`.
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 `<urlset>` schema with `<loc>` and
`<lastmod>` 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.