Views
Register ViewServiceProvider and add config/views.ts. Views live in resources/views/ as .tsx modules. The default extension is .tsx. .tyr templates are not supported in 5.0.
import { View, setViewApplication } from '@pondoknusa/core';
import { Response } from '@pondoknusa/http';
setViewApplication(app);
Route.get('/', async () =>
Response.html(await View.render('welcome', { name: 'Ada' })),
);View module shape
Each view exports a default render function. PascalCase tags resolve to other views (Alert → components/alert.tsx, AppLayout → layouts/app.tsx).
interface Props {
name?: string;
showDetails?: boolean;
users?: string[];
greeting?: string;
}
export default function render({ name, showDetails, users = [], greeting }: Props) {
return (
<AppLayout title="Welcome">
<h1>Hello {name}</h1>
{showDetails && <p>Users: {users.length}</p>}
<ul>
{users.map((user) => (
<li>{user}</li>
))}
</ul>
<Alert message={greeting} />
</AppLayout>
);
}{name}— escaped output- Nested JSX under a component becomes the
childrenslot (already-rendered HTML) <Slot name="footer">…</Slot>— named slot{items.map((item) => <li>{item}</li>)}— loops{cond && <p>…</p>}and ternaries — conditionals<Island id="counter" count={0}>…</Island>— hydrate on the client<ViewFragment name="rows">…</ViewFragment>— named fragment forView.renderFragment()
Reusable UI under a namespace uses member tags: <Ui.Button label="Save" /> resolves to ui::components.button.
Generate views with pondoknusa make:view pages.about.
The compiler compiles the default-export return only. Keep the render function as a single return of JSX. Put extra logic in imported helpers, not in statements before return.
Trust boundary
View expressions run as developer-trusted code (Function for non-trivial paths). Treat .tsx views like application source. Do not compile or render markup supplied by end users. {name} is escaped. Slot children is HTML the compiler already rendered.
Server-side rendering
Pondoknusa supports progressive enhancement: render HTML on the server, then hydrate interactive regions on the client.
Document shell
Use Response.ssr() to wrap a rendered view in a complete HTML document and inject the hydration manifest:
import { Route, View } from '@pondoknusa/core';
import { Response } from '@pondoknusa/http';
Route.get('/', async () => {
const html = await View.render('welcome', { name: 'Ada' });
return Response.ssr(html, {
hydrationManifest: View.getHydrationManifest(),
});
});buildSsrDocument() from @pondoknusa/http performs the same wrapping when you need the HTML string without building a Response.
The manifest is serialized into <script type="application/json" id="tyr-hydration"> before </body>.
Islands
Mark interactive regions with <Island>. The server renders fallback HTML. The client mounts a registered handler for the same id.
<Island id="counter" count={0}>
<button type="button" class="counter">0</button>
</Island>Register the client mount function in resources/client/:
import { registerIsland } from '@pondoknusa/ssr';
registerIsland('counter', ({ element, props }) => {
const button = element.querySelector('button');
let count = Number(props.count ?? 0);
button?.addEventListener('click', () => {
count += 1;
if (button) button.textContent = String(count);
});
});Bootstrap hydration after the page loads:
import { hydrate } from '@pondoknusa/ssr';
hydrate();hydrate() reads data-tyr-island markers (and the optional #tyr-hydration manifest) and calls the matching registerIsland() handler.
Scaffold a new island with pondoknusa make:island counter. That creates resources/views/islands/counter.tsx, resources/client/islands/counter.ts, and registers the client mount in your bundle entry.
Streaming layouts
Return a chunked SSR response in one call — no manual async iteration in the controller:
Route.get('/dashboard', () =>
View.streamSsr('dashboard', context, {
sidebar: async () => '<aside>Fresh sidebar</aside>',
}),
);View.streamSsr() pipes View.renderStream() through Response.ssrStream(), which flushes the document shell (<head> + CSS links) before the first view chunk, then streams body content, and injects the hydration manifest after the view stream completes. The Node HTTP adapter flushes each chunk as it is produced.
Lower-level control is still available when you need it:
return Response.ssrStream(View.renderStream('dashboard', context, handlers), {
title: 'Dashboard',
hydrationManifest: () => View.getHydrationManifest(),
});View.renderStream() yields HTML in document order: shell markup, then each stream section as it resolves.
await View.catalog() returns { components, islands } so tooling can see which views declare each island id and whether a client mount exists. For design-system JSON export, use pondoknusa view:catalog --json.