- 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>
267 lines
11 KiB
Markdown
267 lines
11 KiB
Markdown
# 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.
|