Docs
Next

Rendering pages

SysthemaPage, RootLayout, the catch-all route and convertLexicalNodesToJSX.

On this page

Systhema's frontend rendering converts serialised Lexical content into Systhema React components.

Rendering Lexical contentLink to this section

// Inside a page component or app router route.
import { Lexical } from '@systhemaui/payload/next'

const { content } = page.layout
// `Lexical` (the RichText renderer) maps the serialised Lexical doc → JSX,
// resolving Systhema components + custom blocks.
return <Lexical data={content} />

Each block converter (built-in or custom) receives the serialised node and returns JSX. Custom blocks plug into this pipeline via customBlocks in plugin options.

App routesLink to this section

For full app routing — including /sys/[route] server routes, sitemaps, and the catch-all [...path] route — use the file generator:

pnpm systhema-core payload create-app-files

This creates the Next.js App Router scaffolding Systhema's plugin needs.

PrerenderingLink to this section

The catch-all's generateStaticParams calls generateSysthemaStaticParams({ payloadInstance }), which lists every published page and post for next build to prerender. payloadInstance takes a getter, () => getPayload({ config }), or a getPayload() promise. With SYSTHEMA_PRERENDER=off it returns [] without calling the getter, so the build needs no database. See Building without database access.

Accessibility defaultsLink to this section

Systhema's built-in templates and converters render with accessible defaults:

  • Page shell — <RootLayout> sets <html lang> (from the active locale, overridable via htmlAttributes) and renders a "Skip to content" link as the first focusable element, targeting the <main id="main-content"> that every built-in page template renders.
  • Hero titles render as a real <h1> on every built-in template (page, post, archive) and every hero type — not styled <div>s. The feature hero keeps the text-h2 size because it sits beside its media rather than spanning the page; the element is still the page's h1.
  • Decorative images — a block image with no authored alt renders alt="" (decorative) rather than borrowing the block's internal name.
  • Rich-text upload images take their alt from the media document's authored alt field, falling back to the filename.
  • Links that open a new tab append a visually hidden "(opens in new tab)" hint for screen readers.
  • Icon-only inline blocks keep an accessible name. The button and chip blocks have a Hide title visually checkbox (Advanced Settings) that keeps the title in the DOM for screen readers while hiding it visually (sr-only) — so an icon-only button/chip still has an accessible name. The icon block (which never shows text) has an Accessible label field, applied as aria-label when the icon is a link; if left empty, a name is derived from the link's domain (e.g. a facebook.com link → "Facebook").
  • Google Maps embeds carry a title. The googleMaps block and map heroes render an accessible titled iframe (via GoogleMapEmbed) rather than the title-less @next/third-parties embed.
  • Form block — submission and captcha errors use role="alert", the success confirmation is a focusable role="status" region, and email / country / state fields set the matching autocomplete value.

Frontend localesLink to this section

When frontend locales are enabled, SysthemaPage resolves locale and frontendMessages internally (no locale-aware wrapper is generated — the catch-all page.tsx is the plain, byte-identical baseline whether locales are on or off) and threads them through RootLayout, RootHeader, RootFooter, the built-in page/post/archive templates, custom template props, live-preview client renderers, Lexical converters, forms, post controls, embeds, and the frontend admin bar. The owned site shell only has to pass locale down to those three components — RootLayout derives <html lang> and dir and the resolved message catalog from systhema.config.ts (messages.catalogs + messages.fallback) internally whenever the frontendMessages prop is absent, and it renders SysthemaProvider, which supplies the client-side message context that useSysthemaFrontendTranslator() reads from. Consumer templates can use createSysthemaFrontendTranslator() from @systhemaui/next/messages on the server or useSysthemaFrontendTranslator() from @systhemaui/next/messages/client in a client child. Payload sites keep app/(site)/(systhema)/[[...segments]]; enabling locales never requires a literal [locale] directory — the only file that relocates is the owned site shell, from app/(site)/template.tsx to app/(site)/(systhema)/[[...segments]]/layout.tsx. @systhemaui/payload/next also re-exports getFrontendTranslator(), frontendMessage(), and their Payload-facing types for custom server templates.

The Payload starter also forwards the same canonical systhema.config.ts locales value into SysthemaPayloadPluginOptions. New starters keep this bridge in src/payload.config.ts even when locales are omitted; the value is then undefined, so Payload retains its existing single-language schema and no locale runtime is loaded. Existing starters receive the bridge during the confirmed one-time sync migration — which, along with wrapping next.config.ts in withSysthema(nextConfig) and relocating the site shell, is the entire migration; it generates no other files. A separate locales object in payload.config.ts is rejected because it could make frontend routes and Payload content localization disagree.

RootHeader and RootFooter also resolve their logo link from that locale: the logo points at the locale's own home path (/hu under prefix routing, that locale's domain root under domain routing) instead of a bare /, which would drop the visitor back into the default locale. Pass an explicit logoHref to override it — that always wins, and locale-off projects still get /.

Payload Admin translations remain an independent option. Enabling frontend and content locales does not add Admin languages, while enabling Admin translations does not add public routes or localized fields. For example, locales: { supported: ['en', 'fr', 'nl'], ... } can publish three localized sites while translations: { supported: ['en'] } keeps Payload Admin English-only. Conversely, a Hungarian-only website can offer both English and Hungarian Admin UI. Locale domains are also frontend-only: see the separate-domain, subdomain, and localhost recipes.