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>
This commit is contained in:
266
docs/plans/2026-05-13-public-landing-routing-refactor.md
Normal file
266
docs/plans/2026-05-13-public-landing-routing-refactor.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user