UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

126 lines (108 loc) 3.72 kB
import { reportError } from '#api/server/report-error' import type { Hono } from '#dep/hono/index' import type { ReactRouter } from '#dep/react-router/index' import { React } from '#dep/react/index' import { ResponseInternalServerError } from '#lib/kit-temp' import type { ReactRouterAid } from '#lib/react-router-aid/index' import * as Theme from '#lib/theme/theme' import { Arr } from '@wollybeard/kit' import * as ReactDomServer from 'react-dom/server' import { createStaticRouter, StaticRouterProvider } from 'react-router' import { templateVariables } from 'virtual:polen/template/variables' import type { PolenGlobalData } from '../constants.js' import { view } from './view.js' interface RenderHooks { transformHtml?: (html: string) => Promise<string> | string } export const createPageHtmlResponse = async ( staticHandlerContext: ReactRouter.StaticHandlerContext, hooks?: RenderHooks, ctx?: Hono.Context, ) => { // Set up Polen global data before React SSR if context is provided if (ctx) { const themeManager = Theme.createThemeManager({ cookieName: `polen-theme-preference`, }) const cookies = ctx.req.header(`cookie`) || `` const cookieTheme = themeManager.readCookie(cookies) const polenData: PolenGlobalData = { serverContext: { theme: cookieTheme || 'system', // No cookie means system preference isDev: !__BUILDING__, }, } globalThis.__POLEN__ = polenData } const router = createStaticRouter(view.dataRoutes, staticHandlerContext) let appHtml = `` try { appHtml = ReactDomServer.renderToString( React.createElement( React.StrictMode, null, React.createElement(StaticRouterProvider, { router, context: staticHandlerContext, }), ), ) } catch (cause) { reportError(new Error(`Failed to server side render the HTML`, { cause })) return ResponseInternalServerError() } // Create the base HTML document let html = ` <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${templateVariables.title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg"> </head> <body> <div id="app">${appHtml}</div> </body> </html>` // Apply HTML transformation hook to the full document if (hooks?.transformHtml) { html = await hooks.transformHtml(html) } const headers = getRouteHeaders(staticHandlerContext) headers.set(`Content-Type`, `text/html; charset=utf-8`) // Check for custom status code in route handle const statusCode = getStatusCode(staticHandlerContext) return new Response(`<!DOCTYPE html>${html}`, { status: statusCode, headers, }) } const getStatusCode = ( context: ReactRouter.StaticHandlerContext, ): number => { // First check if React Router set a status code if (context.statusCode && context.statusCode !== 200) { return context.statusCode } // Then check for custom status in route handle const handle = Arr.getLast(context.matches)?.route.handle as ReactRouterAid.RouteHandle | undefined if (handle?.statusCode) { return handle.statusCode } return 200 } const getRouteHeaders = ( context: ReactRouter.StaticHandlerContext, ): Headers => { const leaf = context.matches[context.matches.length - 1] if (!leaf) return new Headers() const actionHeaders = context.actionHeaders[leaf.route.id] const loaderHeaders = context.loaderHeaders[leaf.route.id] const headers = new Headers(actionHeaders) if (loaderHeaders) { for (const [key, value] of loaderHeaders.entries()) { headers.append(key, value) } } return headers }