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

11 KiB
Raw Permalink Blame History

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) and the React Router config in 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)

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:

// 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:

function PublicLanding() {
  const isAuthed = useAuthStore(s => s.isAuthenticated);
  return isAuthed ? <Navigate to="/home" replace /> : <LandingPage />;
}

Auth gate (frontend/src/components/layout/ProtectedRoute.tsx:25)

<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 114 let dest = '/''/home'; '/?welcome=teammate''/home?welcome=teammate'
frontend/src/pages/welcome/WelcomeStep1.tsx 88 navigate('/')navigate('/home')
frontend/src/pages/welcome/WelcomeStep2.tsx 72 same
frontend/src/pages/welcome/WelcomeStep3.tsx 194 same
frontend/src/pages/AssistantChatPage.tsx 2419 same

Authenticated chrome (logo, mobile nav)

File Line Change
frontend/src/components/layout/TopBar.tsx 66 logo to="/"to="/home"
frontend/src/components/layout/AppLayout.tsx 60 mobile nav path: '/''/home'
frontend/src/components/layout/AppLayout.tsx 107 logo to="/"to="/home"

Dashboard onboarding (has in-progress edits — layer carefully)

File Line Change
frontend/src/components/dashboard/SetupChecklist.tsx 54 path: '/''/home'
frontend/src/components/dashboard/NextStepCard.tsx 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 10 to="/landing"to="/"
frontend/src/pages/PoliciesPage.tsx 10 same
frontend/src/pages/PrivacyPage.tsx 10 same
frontend/src/pages/ContactPage.tsx 10 same
frontend/src/pages/PromotionsPage.tsx 10 same
frontend/src/pages/PublicTemplatesPage.tsx 171, 409 same

robots.txt + sitemap.xml (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 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 initialEntries={['/']}['/home']
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. No backend changes required.

Manual follow-ups (not code changes)

  • PostHog dashboard audit. frontend/src/main.tsx:20 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 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.