From 451639b0c319e7a14f9dc8c3190bbdf8ed4f0dc1 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 17 Mar 2026 15:23:10 +0100 Subject: [PATCH] Add page meta. Update page HTML gen function --- .../server/web-pages/generate-web-html.tsx | 22 +++--- .../server/web-pages/grab-page-component.tsx | 70 +++++++++++++++---- .../server/web-pages/grab-web-meta-html.ts | 63 +++++++++++++++++ .../grab-web-page-hydration-script.tsx | 2 +- .../server/web-pages/handle-web-pages.tsx | 13 +++- src/types/index.ts | 40 ++++++++++- 6 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 src/functions/server/web-pages/grab-web-meta-html.ts diff --git a/src/functions/server/web-pages/generate-web-html.tsx b/src/functions/server/web-pages/generate-web-html.tsx index 8ed0a7c..3a581f5 100644 --- a/src/functions/server/web-pages/generate-web-html.tsx +++ b/src/functions/server/web-pages/generate-web-html.tsx @@ -1,10 +1,10 @@ import path from "path"; import grabContants from "../../../utils/grab-constants"; -import grabDirNames from "../../../utils/grab-dir-names"; import EJSON from "../../../utils/ejson"; import type { LivePageDistGenParams } from "../../../types"; import isDevelopment from "../../../utils/is-development"; import grabWebPageHydrationScript from "./grab-web-page-hydration-script"; +import grabWebMetaHTML from "./grab-web-meta-html"; export default async function genWebHTML({ component, @@ -12,6 +12,7 @@ export default async function genWebHTML({ bundledMap, head, module, + meta, }: LivePageDistGenParams) { const { ClientRootElementIDName, ClientWindowPagePropsName } = await grabContants(); @@ -23,24 +24,27 @@ export default async function genWebHTML({ const componentHTML = renderToString(component); const headHTML = head ? renderToString(head) : ""; - // const SCRIPT_SRC = path.join("/public/pages", bundledMap.path); - // const CSS_SRC = bundledMap.css_path - // ? path.join("/public/pages", bundledMap.css_path) - // : undefined; - // const { HYDRATION_DST_DIR } = grabDirNames(); - let html = `\n`; html += `\n`; html += ` \n`; html += ` \n`; html += ` \n`; - if (bundledMap.css_path) { + + if (meta) { + html += ` ${grabWebMetaHTML({ meta })}\n`; + } + + if (bundledMap?.css_path) { html += ` \n`; } + html += ` \n`; - html += ` \n`; + + if (bundledMap?.path) { + html += ` \n`; + } if (isDevelopment()) { html += `\n`; diff --git a/src/functions/server/web-pages/grab-page-component.tsx b/src/functions/server/web-pages/grab-page-component.tsx index 3a7b028..6bdc0f6 100644 --- a/src/functions/server/web-pages/grab-page-component.tsx +++ b/src/functions/server/web-pages/grab-page-component.tsx @@ -2,7 +2,11 @@ import type { FC } from "react"; import grabDirNames from "../../../utils/grab-dir-names"; import grabRouteParams from "../../../utils/grab-route-params"; import grabRouter from "../../../utils/grab-router"; -import type { BunextPageModule, GrabPageComponentRes } from "../../../types"; +import type { + BundlerCTXMap, + BunextPageModule, + GrabPageComponentRes, +} from "../../../types"; import path from "path"; import AppNames from "../../../utils/grab-app-names"; import { existsSync } from "fs"; @@ -92,6 +96,17 @@ export default async function grabPageComponent({ } })(); + const meta = module.meta + ? typeof module.meta == "function" && routeParams + ? await module.meta({ + ctx: routeParams, + serverRes, + }) + : typeof module.meta == "object" + ? module.meta + : undefined + : undefined; + const Component = module.default as FC; const component = RootComponent ? ( @@ -108,24 +123,51 @@ export default async function grabPageComponent({ routeParams, module, bundledMap, + meta, }; } catch (error: any) { - // console.log(`Grab page component ERROR =>`, error.message); + try { + const match = router.match("/500"); + const filePath = match?.filePath || BUNX_ROOT_500_PRESET_COMPONENT; - const match = router.match("/500"); + const bundledMap = match?.filePath + ? (global.BUNDLER_CTX_MAP?.find( + (m) => m.local_path === match.filePath, + ) ?? ({} as BundlerCTXMap)) + : ({} as BundlerCTXMap); - const filePath = match?.filePath || BUNX_ROOT_500_PRESET_COMPONENT; + const module: BunextPageModule = await import(filePath); + const Component = module.default as FC; + const component = ; - const module: BunextPageModule = await import(filePath); + return { + component, + routeParams, + module, + bundledMap, + }; + } catch { + const DefaultServerError: FC = () => ( +
+ 500 Internal Server Error +
+ ); - const Component = module.default as FC; - const component = ; - - return { - component, - routeParams, - module, - bundledMap: {}, - }; + return { + component: , + routeParams, + module: { + default: DefaultServerError, + }, + }; + } } } diff --git a/src/functions/server/web-pages/grab-web-meta-html.ts b/src/functions/server/web-pages/grab-web-meta-html.ts new file mode 100644 index 0000000..a307388 --- /dev/null +++ b/src/functions/server/web-pages/grab-web-meta-html.ts @@ -0,0 +1,63 @@ +import type { BunextPageModuleMeta } from "../../../types"; + +type Params = { + meta: BunextPageModuleMeta; +}; + +export default async function grabWebMetaHTML({ meta }: Params) { + let html = ``; + + if (meta.title) { + html += ` ${meta.title}\n`; + } + + if (meta.description) { + html += ` \n`; + } + + if (meta.keywords) { + const keywords = Array.isArray(meta.keywords) + ? meta.keywords.join(", ") + : meta.keywords; + html += ` \n`; + } + + if (meta.author) { + html += ` \n`; + } + + if (meta.robots) { + html += ` \n`; + } + + if (meta.canonical) { + html += ` \n`; + } + + if (meta.themeColor) { + html += ` \n`; + } + + if (meta.og) { + const { og } = meta; + if (og.title) html += ` \n`; + if (og.description) html += ` \n`; + if (og.image) html += ` \n`; + if (og.url) html += ` \n`; + if (og.type) html += ` \n`; + if (og.siteName) html += ` \n`; + if (og.locale) html += ` \n`; + } + + if (meta.twitter) { + const { twitter } = meta; + if (twitter.card) html += ` \n`; + if (twitter.title) html += ` \n`; + if (twitter.description) html += ` \n`; + if (twitter.image) html += ` \n`; + if (twitter.site) html += ` \n`; + if (twitter.creator) html += ` \n`; + } + + return html; +} diff --git a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx index e332cb2..d14fd93 100644 --- a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx +++ b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx @@ -5,7 +5,7 @@ import grabConstants from "../../../utils/grab-constants"; const { BUNX_HYDRATION_SRC_DIR } = grabDirNames(); type Params = { - bundledMap: BundlerCTXMap; + bundledMap?: BundlerCTXMap; }; export default async function ({ bundledMap }: Params) { diff --git a/src/functions/server/web-pages/handle-web-pages.tsx b/src/functions/server/web-pages/handle-web-pages.tsx index 706a3d4..d9d657e 100644 --- a/src/functions/server/web-pages/handle-web-pages.tsx +++ b/src/functions/server/web-pages/handle-web-pages.tsx @@ -8,14 +8,23 @@ type Params = { export default async function ({ req }: Params): Promise { try { - const { component, bundledMap, module, serverRes } = - await grabPageComponent({ req }); + const { + component, + bundledMap, + module, + serverRes, + meta, + head, + routeParams, + } = await grabPageComponent({ req }); const html = await genWebHTML({ component, pageProps: serverRes, bundledMap, module, + meta, + head, }); const res_opts: ResponseInit = { diff --git a/src/types/index.ts b/src/types/index.ts index 3d3b85e..dfc46d6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -128,12 +128,46 @@ export type LivePageDistGenParams = { head?: ReactNode; pageProps?: any; module?: BunextPageModule; - bundledMap: BundlerCTXMap; + bundledMap?: BundlerCTXMap; + meta?: BunextPageModuleMeta; }; export type BunextPageModule = { default: FC; server?: BunextPageServerFn; + meta?: BunextPageModuleMeta | BunextPageModuleMetaFn; +}; + +export type BunextPageModuleMetaFn = (params: { + ctx: BunxRouteParams; + serverRes?: BunextPageModuleServerReturn; +}) => Promise; + +export type BunextPageModuleMeta = { + title?: string; + description?: string; + keywords?: string | string[]; + author?: string; + robots?: string; + canonical?: string; + themeColor?: string; + og?: { + title?: string; + description?: string; + image?: string; + url?: string; + type?: string; + siteName?: string; + locale?: string; + }; + twitter?: { + card?: "summary" | "summary_large_image" | "app" | "player"; + title?: string; + description?: string; + image?: string; + site?: string; + creator?: string; + }; }; export type BunextPageServerFn< @@ -155,8 +189,10 @@ export type GrabPageComponentRes = { component: JSX.Element; serverRes?: BunextPageModuleServerReturn; routeParams?: BunxRouteParams; - bundledMap: BundlerCTXMap; + bundledMap?: BundlerCTXMap; module: BunextPageModule; + meta?: BunextPageModuleMeta; + head?: ReactNode; }; export type PageFiles = {