Skip to content
File

Blob: src/client/server/render.tsx

typescript58 lines
1import { renderToReadableStream } from "react-dom/server";
2 
3import { AppLayout } from "@/client/components/AppLayout";
4import { resolveDocumentAssets } from "./assets";
5import { Document } from "./document";
6import { getViewDefinition } from "./registry";
7import type { Viewer } from "./viewer";
8 
9function needsHighlightTheme(name: string, data: Record<string, unknown>): boolean {
10 return (
11 name === "blob" ||
12 name === "commit" ||
13 (name === "overview" && typeof data.readmeMd === "string" && data.readmeMd.length > 0)
14 );
15}
16 
17export type RenderUiViewOptions = {
18 // The signed-in viewer (if any) is computed by the Worker route handler
19 // and threaded through the SSR shell. The renderer stays presentational
20 // and never touches cookies or D1 itself.
21 viewer?: Viewer | null;
22};
23 
24export async function renderUiView(
25 env: Env,
26 name: string,
27 data: Record<string, unknown>,
28 options: RenderUiViewOptions = {}
29): Promise<BodyInit | null> {
30 const definition = getViewDefinition(name);
31 if (!definition) {
32 return null;
33 }
34 
35 const page = definition.render(data);
36 if (definition.kind === "fragment") {
37 // Fragments don't need a full document wrapper — React won't emit a doctype
38 return renderToReadableStream(page);
39 }
40 
41 const assets = await resolveDocumentAssets(env, definition.clientEntrypoints || []);
42 const element = (
43 <Document
44 title={(data.title as string | undefined) || definition.title}
45 assets={assets}
46 needsHighlight={needsHighlightTheme(name, data)}
47 >
48 <AppLayout currentView={name} viewer={options.viewer ?? null}>
49 {page}
50 </AppLayout>
51 </Document>
52 );
53 
54 // React's renderToReadableStream automatically emits <!DOCTYPE html>
55 // when the root element is <html>
56 return renderToReadableStream(element);
57}