Repository navigation
Prerender the static pages so they read without JavaScript - #20
Merged
Merged
Conversation
The Security page imported `MIN_PASSWORD_LENGTH` from `services/signIn`, and `Layout` reads `useAuth` from a module that also holds the provider, so rendering either pulled in the Firebase SDK and started Auth. That is harmless in the browser, but it rules out rendering those pages in Node at build time: started there, Firebase Auth keeps the process from exiting. `useAuth` and its context stay in `contexts/AuthContext.tsx`, now type-only on Firebase, and `AuthProvider` moves to `contexts/AuthProvider.tsx`. The password rule moves to `config/password.ts`. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every path on the site was answered with `index.html`, whose `#root` is empty until the bundle runs, so with JavaScript off the terms, privacy, support and security pages were blank. Crawlers and link previews that run nothing also saw the front page's title on all of them. The build now renders those pages to HTML. `npm run prerender` (the end of `npm run build`) builds `src/prerender.tsx` for Node and writes `dist/<page>.html` for each entry in `src/staticPages.tsx`: the built `index.html` with the page in `#root` and its own title and `og:url`. `App` routes the same list. Each page has a rewrite of its own in `firebase.json` ahead of the catch-all, and the script fails the build when one is missing, or when the renderer imports Firebase (which would hang it). `cleanUrls` would avoid the list but redirects `/index.html`, the site's image page, to `/`. With JavaScript on, `main.tsx` waits for Firebase to report who is signed in before mounting, and `AuthProvider` starts from that answer. Mounting at once rendered the provider's empty first frame over the page, blanking it briefly. The front page is left out: filling `index.html` would show it on every route until the bundle loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment said `Layout` still imports the Firebase config and that this was harmless. It has not since `AuthProvider` moved out of `AuthContext`, and the prerender script fails the build when the renderer imports Firebase, so the sentence read as permission for the very thing the guard forbids. It now says what is true and why the guard exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prerender set `<title>` and `og:url` but left `og:title` as the front page's headline. Slack, iMessage and X read `og:title` before `<title>`, so a shared `/terms` link still previewed as the front page, now under the Terms URL, and the docs claimed otherwise. `og:title` now carries the page's title. The descriptions stay the product's on purpose: they say what the page belongs to, which is what a card for the terms needs. `fill` takes a pattern so the script does not repeat the front page's headline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`AuthProvider` set `authKnown` in its own `authStateReady()` callback and `main.tsx` mounted in another. The provider started from the known answer only because its module registered first. Lazy-load `App` or reorder the imports and `mount` would run with the flag still false, bringing back the blank frame over the prerendered page with nothing failing to say so. `AuthProvider` now exports the promise that sets the flag, and `main.tsx` waits on that one, so the order holds by construction. `main.tsx` no longer imports the Firebase config itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment said `hydrateRoot` could not work because the first render is `AuthProvider`'s empty one. That stopped being true when the provider began starting from the known answer, and a comment recording a rejected alternative on a false premise invites someone to rule it out, or back in, for the wrong reason. The reason that holds is the signed-in visitor, whose nav and menu never match the signed-out markup, so hydration would fail for them and render from scratch with an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A prerendered page can be read and scrolled before the bundle loads, and `/terms#refunds` lands on its section at once. The page scrolls inside `#layout-border`, which `createRoot` replaced with a new element at the top, and then `ScrollToTop`'s mount effect scrolled to the top again, so the reader was thrown back to the start. `ScrollToTop` now scrolls only when the location changes from the one it mounted on, kept in a ref because StrictMode runs the effect twice. `mount` reads the offset, commits the first render with `flushSync`, and puts the offset back. Keyboard focus is still lost for someone who tabbed in before the bundle loaded: carrying it over would mean finding the same element in the new tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Privacy, Terms, Support and Security were blank with JavaScript off: Hosting answers every path with
index.html, whose#rootstays empty until the bundle runs. Crawlers and link previews that run nothing also saw the front page's title on all four.What changed
npm run buildnow ends withnpm run prerender. It buildssrc/prerender.tsxfor Node and writesdist/<page>.htmlfor each page insrc/staticPages.tsx: the builtindex.htmlwith the page in#root, plus the page's own<title>,og:titleandog:url.Approutes from the same list.firebase.json, ahead of the**catch-all. The script fails the build if one is missing. I didn't usecleanUrls: it would redirect/index.htmlto/, and the site's image page is/index.html?view=….useAuthis now split fromAuthProvider, andMIN_PASSWORD_LENGTHmoved toconfig/password.ts. The script fails with a message if the renderer ever imports Firebase again.main.tsxwaits for Firebase to say who is signed in, then mounts withcreateRoot.AuthProviderstarts from that answer, so the page never blanks. The reader's scroll position is carried over,#fragmentlandings included, andScrollToTopnow scrolls only when the app moves to another page.The front page is not prerendered. Every unknown path is served
index.html, so filling that file would show the landing page on/linksand the other routes until the bundle loaded.For the reviewer
hydrateRoot: a signed-in visitor's first render never matches the prerendered markup, so React would report an error and render from scratch anyway.dist/is under a dot-folder like.claude/worktrees/.docs/web.mdsays how to preview around it.apps/weband thefirebase.jsonrewrites to production.Testing
npm run lint,npm run test:keys(67),npm run build,npm run build:chromeandnpm run test:rules(172) pass. The rules tests ran before the review fixes, which don't touch rules./,/links, unknown paths and/index.html?view=still get the empty shell./terms#refundskeeps its place, and in-app links still start at the top.🤖 Generated with Claude Code