You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cut SSR memory: ssr/client page convention, smaller pool, heap cap
Each Inertia SSR Node worker loads the entire server bundle into its own
V8 heap, including authenticated, interactive pages that don't benefit
from server rendering — wasteful on small instances. Three levers:
- Split pages into pages/ssr/ (server-rendered: public/SEO) and
pages/client/ (client-only: authed/interactive). The strategy dir is
stripped from the Inertia page name, so controllers are unchanged. The
generator now emits _pages.ts (lazy client manifest, code-splitting
preserved) + _ssr_pages.ts (ssr-only, plus a client-only set that
ssr.tsx renders as a server-side no-op).
- SSR_POOL_SIZE env (default 2) sizes the Node worker pool.
- NODE_OPTIONS=--max-old-space-size=160 in the Dockerfile caps each
worker's V8 heap.
Generate the registries before esbuild in assets.build/deploy (the
client bundle imports _pages.ts). Exclude the generated registries from
the biome formatter. Document the split in docs/frontend-pages.md,
CLAUDE.md, and the README.
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,12 +27,12 @@ These are the rules that must not be forgotten or looked up — they're the ones
27
27
28
28
**Inertia (this project's frontend)**
29
29
- Use `render_inertia/2|3`, never `render/3`, for Inertia pages. Don't use LiveView for Inertia pages
30
-
- Page components are **always**`.tsx` (TypeScript), in`assets/js/pages/`
30
+
- Page components are **always**`.tsx` (TypeScript), under`assets/js/pages/` in one of two strategy dirs: **`pages/ssr/`** (server-rendered: public / SEO / first-paint pages) or **`pages/client/`** (client-only: authenticated, interactive, heavy pages — kept out of the Node SSR bundle). The strategy dir is stripped from the Inertia page name, so `render_inertia("Dashboard")` maps to `pages/client/Dashboard.tsx`. The `_pages.ts` / `_ssr_pages.ts` registries are auto-generated from these dirs by `build/generate-ssr-pages.js` — never edit them by hand. New public page → `ssr/`; new authed page → `client/`
31
31
- Layout components live in `assets/js/layouts/` (e.g. `AppLayout`, `AuthLayout`)
32
32
- Reusable UI primitives live flat in `assets/js/components/` (`Button`, `Spinner`, `Select`, `DropdownMenu`, `AlertDialog`, …) — no sub-folders
33
33
- Forms use Inertia's `useForm` hook; errors come from `assign_errors(conn, changeset)` on the server
34
34
-**No raw `try/catch` for async work — wrap every Promise in `go()` from `@api3/promise-utils`** (sync work uses `goSync`). It returns `{ success, data, error }`, which forces every call site to acknowledge the failure path explicitly and prevents the "swallow the error and move on" pattern that hides real bugs. The same applies to dynamic `import()`, `fetch()`, JSON parsing, and any vendor SDK call. The only acceptable exception is at top-level error boundaries that genuinely *do* need to catch everything synchronously
35
-
- Never edit `assets/js/_ssr_pages.ts` — it's auto-generated
35
+
- Never edit `assets/js/_pages.ts` or `assets/js/_ssr_pages.ts` — they're auto-generated
36
36
-**Validation errors must redirect, not re-render.** On `{:error, changeset}` always use `conn |> assign_errors(changeset) |> redirect(to: ~p"/current-page")`. Do **not** use `put_status(:unprocessable_entity) |> render_inertia(...)` — Inertia updates the browser URL to the POST/PUT target when you re-render, which is both wrong UX and a regression risk. The Inertia plug flashes errors through the session across the redirect, so the form still shows them
37
37
- Use the `~p"/..."` sigil (verified routes) for every internal URL — in controllers, tests, and anywhere else in Elixir. Never write raw route strings; compile-time verification catches typos and broken links
38
38
-**JSON serialisation goes through `*_json.ex` view modules**, never ad-hoc serializer modules (no `*Props`, `*Serializer`, or per-controller helpers). One module per resource at `lib/elixir_react_starter_web/controllers/<resource>_json.ex` (e.g. `ElixirReactStarterWeb.PostJSON`) exposes `index/1` and `show/1` (the Phoenix 1.8 equivalents of `render_many`/`render_one`) that both delegate to a single `data/2`. Inertia callers use `MyJSON.data(record, viewer)` directly inside `assign_prop`; JSON API endpoints use `index`/`show` via `render`. This keeps the wire shape for a resource in one place so multiple callers can't drift
0 commit comments