// `/` — `renderMode: 'ssr'`, fed by the sibling api. This is the SSR-from-a- // real-backend pattern: the loader runs server-side per request and pulls // rows from the api BEFORE the HTML is sent, so the first paint + the // reflect real data (crawlable, no client round-trip). The component // then upgrades the same data to live with `useSubscription`. import type { ReactNode } from 'react' import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web' import { useSubscription } from '@voltro/client' import { T } from '@voltro/i18n' import { getCatalog } from '../locales' // The row shape `notes.list` streams. Kept local so the page is // self-contained — you can also `import type { Note }` from the api's // generated types (a type-only cross-app import is browser-safe). interface Note { readonly id: string readonly title: string readonly body: string readonly done: boolean } interface HomeData { readonly notes: ReadonlyArray<Note> } export const renderMode = 'ssr' as const // `query` is present ONLY server-side (ssr/isr). It invokes the api's rpc // directly over POST /rpc, forwarding the request's session cookie so the // SAME Subject + tenant resolve as the WS path. A streaming query is drained // to its FIRST snapshot — here, the current notes for this tenant. Pass the // row type so the result is typed without importing the api. export const loader: LoaderFn<HomeData> = async ({ query }) => ({ notes: query ? await query<ReadonlyArray<Note>>('notes.list', {}) : [], }) // `meta` as a function of the loader data AND the active locale → the <title> // reflects real api data (SEO + social cards) in the visitor's language. It's a // plain catalog lookup (not ICU-formatted), so the row count is substituted // into the `{count}` token by hand. export const meta = ({ locale, loaderData }: { readonly locale: string; readonly loaderData: HomeData }): PageMeta => ({ title: getCatalog(locale)['meta.home.title'].replace('{count}', String(loaderData.notes.length)), description: getCatalog(locale)['meta.home.description'], }) export default function Home(): ReactNode { // The SSR'd snapshot from the loader — correct on first paint. const { notes: initial } = useLoaderData<HomeData>() // Live data once the WebSocket connects. `data` is `undefined` during SSR, // so the server renders `initial` and the client upgrades to `live` on connect. const { data: live } = useSubscription<ReadonlyArray<Note>>('app', 'notes.list') const notes = live ?? initial const code = (c: ReactNode) => <code>{c}</code> return ( <section> <span className="badge"><T id="home.badge" /></span> <h1><T id="home.heading" /></h1> <p className="lead"> <T id="home.lead" values={{ code, queryCall: <code>{"query('notes.list', {})"}</code> }} /> </p> {notes.length === 0 ? ( <p className="empty"> <T id="home.empty" values={{ code }} /> </p> ) : ( <ul className="notes"> {notes.map((note) => ( <li key={note.id}> <span className="title">{note.title}</span>{' '} {note.done ? <span className="done"><T id="home.done" /></span> : null} {note.body ? <p className="body">{note.body}</p> : null} </li> ))} </ul> )} </section> ) }