- 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>
11 KiB
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:
- Browser fetches
/→ empty HTML shell. - JS executes, auth store hydrates as not-authenticated.
ProtectedRouteclient-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:
- No
og:urlinPageMeta— add aurlprop that defaults towindow.location.href(or take a canonical override). twitter:cardis alwayssummary— when anogImageis passed, emitsummary_large_imageinstead.LandingPagedoesn't passogImage— 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.fromso post-login flow can return the user to their intended destination.
Out of scope / non-issues verified
- Service worker / PWA cache invalidation.
vite.config.tshas novite-plugin-pwa, noinjectManifest, no SW registration anywhere infrontend/src. The "PWA Icons" comment inindex.htmlis 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/landingor root-path redirects. OAuth callbacks render client-side viaOAuthCallbackPage.tsx. No backend changes required.
Manual follow-ups (not code changes)
- PostHog dashboard audit.
frontend/src/main.tsx:20setscapture_pageview: true, so$pageviewauto-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 (greppedlib/analytics.tsand 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-ssgfor 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 toapp.resolutionflow.com. Do not pre-optimize for this. Capture as a decision in.ai/DECISIONS.mdwhen this PR lands. - Delete
/landingredirect alias after one release cycle.
Rollout / sequencing
- Router restructure +
PublicLandingwrapper. - 21 reference updates (post-login, chrome, dashboard onboarding, public page back-links).
ProtectedRouteredirect target flip.robots.txt,sitemap.xml.PageMetaenhancements (og:url,summary_large_imagetoggle).- OG image asset, wired into
LandingPage. - Test updates + new
ProtectedRoutetest. - Manual: PostHog dashboard sweep.
.ai/DECISIONS.mdentry noting SSR-prerender as next-escalation path.- 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.