// `/` — `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
}
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 = async ({ query }) => ({
notes: query ? await query>('notes.list', {}) : [],
})
// `meta` as a function of the loader data AND the active locale → the
// 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()
// 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>('app', 'notes.list')
const notes = live ?? initial
const code = (c: ReactNode) => {c}
return (
{"query('notes.list', {})"} }}
/>
{notes.length === 0 ? (
) : (
{notes.map((note) => (
-
{note.title}{' '}
{note.done ? : null}
{note.body ?
{note.body}
: null}
))}
)}
)
}