From 7d751348d1a27b2417c23e4282d0666e81eacc4c Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 3 Aug 2025 13:09:07 +0200 Subject: [PATCH 1/2] feat(router): createRenderer for better DX/types --- .changeset/quiet-friends-taste.md | 5 + e2e/adapters-e2e/src/entry.preview.tsx | 1 - e2e/adapters-e2e/src/entry.ssr.tsx | 20 +- packages/docs/src/entry.ssr.tsx | 26 ++- .../docs/src/routes/api/qwik-router/api.json | 84 +++++++ .../docs/src/routes/api/qwik-router/index.mdx | 211 ++++++++++++++++++ .../docs/src/routes/api/qwik-server/api.json | 2 +- .../docs/src/routes/api/qwik-server/index.mdx | 2 +- packages/insights/src/entry.ssr.tsx | 32 +-- .../qwik-router/src/runtime/src/constants.ts | 2 +- .../src/runtime/src/create-renderer.ts | 51 +++++ packages/qwik-router/src/runtime/src/index.ts | 8 + .../runtime/src/qwik-router.runtime.api.md | 53 +++++ packages/qwik-router/src/runtime/src/types.ts | 12 + packages/qwik/src/server/qwik.server.api.md | 1 - packages/qwik/src/server/types.ts | 1 + starters/apps/base/src/entry.ssr.tsx | 33 ++- starters/features/localize/src/entry.ssr.tsx | 38 ++-- 18 files changed, 496 insertions(+), 86 deletions(-) create mode 100644 .changeset/quiet-friends-taste.md create mode 100644 packages/qwik-router/src/runtime/src/create-renderer.ts diff --git a/.changeset/quiet-friends-taste.md b/.changeset/quiet-friends-taste.md new file mode 100644 index 00000000000..c785274ed68 --- /dev/null +++ b/.changeset/quiet-friends-taste.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': minor +--- + +feat: `createRenderer()` wraps the `renderToStream()` function with Qwik Router types, for nicer `entry.ssr` files. diff --git a/e2e/adapters-e2e/src/entry.preview.tsx b/e2e/adapters-e2e/src/entry.preview.tsx index 35a2b15c5da..d491a3f5b02 100644 --- a/e2e/adapters-e2e/src/entry.preview.tsx +++ b/e2e/adapters-e2e/src/entry.preview.tsx @@ -11,7 +11,6 @@ * */ import { createQwikRouter } from '@qwik.dev/router/middleware/node'; -// make sure qwikCityPlan is imported before entry import render from './entry.ssr'; /** The default export is the QwikCity adapter used by Vite preview. */ diff --git a/e2e/adapters-e2e/src/entry.ssr.tsx b/e2e/adapters-e2e/src/entry.ssr.tsx index 2b563785e7b..a0829f38b76 100644 --- a/e2e/adapters-e2e/src/entry.ssr.tsx +++ b/e2e/adapters-e2e/src/entry.ssr.tsx @@ -9,25 +9,17 @@ * - Npm run preview * - Npm run build */ -import { renderToStream, type RenderToStreamOptions } from '@qwik.dev/core/server'; +import { createRenderer } from '@qwik.dev/router'; import Root from './root'; -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { +export default createRenderer((opts) => ({ + jsx: , + options: { ...opts, // Use container attributes to set attributes on the html tag. containerAttributes: { lang: 'en-us', ...opts.containerAttributes, }, - // prefetchStrategy: { - // implementation: { - // linkInsert: "html-append", - // linkRel: "modulepreload", - // }, - // }, - serverData: { - ...opts.serverData, - }, - }); -} + }, +})); diff --git a/packages/docs/src/entry.ssr.tsx b/packages/docs/src/entry.ssr.tsx index 7cc6b404eb0..01ffac13602 100644 --- a/packages/docs/src/entry.ssr.tsx +++ b/packages/docs/src/entry.ssr.tsx @@ -1,5 +1,4 @@ -import type { PreloaderOptions, RenderToStreamOptions } from '@qwik.dev/core/server'; -import { renderToStream } from '@qwik.dev/core/server'; +import { createRenderer } from '@qwik.dev/router'; import Root from './root'; // You can pass these as query parameters, as well as `preloadDebug` @@ -10,9 +9,9 @@ const preloaderSettings = [ 'preloadProbability', ] as const; -export default function (opts: RenderToStreamOptions) { +export default createRenderer((opts) => { const { serverData } = opts; - const urlStr = serverData?.url; + const urlStr = serverData.url; if (urlStr) { const { searchParams } = new URL(urlStr); if (searchParams.size) { @@ -21,7 +20,7 @@ export default function (opts: RenderToStreamOptions) { preloader: { ...(typeof opts.preloader === 'object' ? opts.preloader : undefined), }, - } as Omit & { preloader: PreloaderOptions }; + }; if (searchParams.has('preloaderDebug')) { newOpts.preloader!.debug = true; } @@ -33,11 +32,14 @@ export default function (opts: RenderToStreamOptions) { opts = newOpts; } } - return renderToStream(, { - ...opts, - containerAttributes: { - lang: 'en', - ...opts.containerAttributes, + return { + jsx: , + options: { + ...opts, + containerAttributes: { + lang: 'en', + ...opts.containerAttributes, + }, }, - }); -} + }; +}); diff --git a/packages/docs/src/routes/api/qwik-router/api.json b/packages/docs/src/routes/api/qwik-router/api.json index 8bb483e095b..9b88024948c 100644 --- a/packages/docs/src/routes/api/qwik-router/api.json +++ b/packages/docs/src/routes/api/qwik-router/api.json @@ -86,6 +86,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.contentmenu.md" }, + { + "name": "createRenderer", + "id": "createrenderer", + "hierarchy": [ + { + "name": "createRenderer", + "id": "createrenderer" + } + ], + "kind": "Function", + "content": "Creates the `render()` function that is required by `createQwikRouter()`. It requires a function that returns the `jsx` and `options` for the renderer.\n\n\n```typescript\ncreateRenderer: (getOptions: (options: RendererOptions) => {\n jsx: JSXOutput;\n options: RendererOutputOptions;\n}) => Render\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ngetOptions\n\n\n\n\n(options: [RendererOptions](#rendereroptions)) => { jsx: JSXOutput; options: [RendererOutputOptions](#rendereroutputoptions); }\n\n\n\n\n\n
\n**Returns:**\n\nRender\n\n\n\n```tsx\nconst renderer = createRenderer((opts) => {\n if (opts.requestHeaders['x-hello'] === 'world') {\n return { jsx: , options: opts };\n }\n return { jsx: , options: {\n ...opts,\n serverData: {\n ...opts.serverData,\n documentHead: {\n meta: [\n { name: 'renderedAt', content: new Date().toISOString() },\n ],\n },\n },\n } };\n});\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts", + "mdFile": "router.createrenderer.md" + }, { "name": "DataValidator", "id": "datavalidator", @@ -506,6 +520,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.preventnavigatecallback.md" }, + { + "name": "Q_ROUTE", + "id": "q_route", + "hierarchy": [ + { + "name": "Q_ROUTE", + "id": "q_route" + } + ], + "kind": "Variable", + "content": "```typescript\nQ_ROUTE = \"q:route\"\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/constants.ts", + "mdFile": "router.q_route.md" + }, { "name": "QWIK_CITY_SCROLLER", "id": "qwik_city_scroller", @@ -618,6 +646,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.qwikrouterconfig.md" }, + { + "name": "QwikRouterEnvData", + "id": "qwikrouterenvdata", + "hierarchy": [ + { + "name": "QwikRouterEnvData", + "id": "qwikrouterenvdata" + } + ], + "kind": "Interface", + "content": "```typescript\nexport interface QwikRouterEnvData \n```\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[ev](./router.qwikrouterenvdata.ev.md)\n\n\n\n\n\n\n\nRequestEvent\n\n\n\n\n\n
\n\n[loadedRoute](./router.qwikrouterenvdata.loadedroute.md)\n\n\n\n\n\n\n\nLoadedRoute \\| null\n\n\n\n\n\n
\n\n[params](./router.qwikrouterenvdata.params.md)\n\n\n\n\n\n\n\n[PathParams](#pathparams)\n\n\n\n\n\n
\n\n[response](./router.qwikrouterenvdata.response.md)\n\n\n\n\n\n\n\nEndpointResponse\n\n\n\n\n\n
\n\n[routeName](./router.qwikrouterenvdata.routename.md)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", + "mdFile": "router.qwikrouterenvdata.md" + }, { "name": "QwikRouterMockProps", "id": "qwikroutermockprops", @@ -674,6 +716,34 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/qwik-router-component.tsx", "mdFile": "router.qwikrouterprovider.md" }, + { + "name": "RendererOptions", + "id": "rendereroptions", + "hierarchy": [ + { + "name": "RendererOptions", + "id": "rendereroptions" + } + ], + "kind": "TypeAlias", + "content": "```typescript\nexport type RendererOptions = Omit & {\n serverData: ServerData;\n};\n```\n**References:** [ServerData](#serverdata)", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts", + "mdFile": "router.rendereroptions.md" + }, + { + "name": "RendererOutputOptions", + "id": "rendereroutputoptions", + "hierarchy": [ + { + "name": "RendererOutputOptions", + "id": "rendereroutputoptions" + } + ], + "kind": "TypeAlias", + "content": "```typescript\nexport type RendererOutputOptions = Omit & {\n serverData: ServerData & {\n documentHead?: DocumentHeadValue;\n } & Record;\n};\n```\n**References:** [ServerData](#serverdata), [DocumentHeadValue](#documentheadvalue)", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts", + "mdFile": "router.rendereroutputoptions.md" + }, { "name": "ResolvedDocumentHead", "id": "resolveddocumenthead", @@ -786,6 +856,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts", "mdFile": "router.server_.md" }, + { + "name": "ServerData", + "id": "serverdata", + "hierarchy": [ + { + "name": "ServerData", + "id": "serverdata" + } + ], + "kind": "TypeAlias", + "content": "The server data that is provided by Qwik Router during SSR rendering. It can be retrieved with `useServerData(key)` in the server, but it is not available in the client.\n\n\n```typescript\nexport type ServerData = {\n url: string;\n requestHeaders: Record;\n locale: string | undefined;\n nonce: string | undefined;\n containerAttributes: Record & {\n [Q_ROUTE]: string;\n };\n qwikrouter: QwikRouterEnvData;\n};\n```\n**References:** [Q\\_ROUTE](#q_route), [QwikRouterEnvData](#qwikrouterenvdata)", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", + "mdFile": "router.serverdata.md" + }, { "name": "ServerFunction", "id": "serverfunction", diff --git a/packages/docs/src/routes/api/qwik-router/index.mdx b/packages/docs/src/routes/api/qwik-router/index.mdx index e003a81f71c..867f4f4d1c8 100644 --- a/packages/docs/src/routes/api/qwik-router/index.mdx +++ b/packages/docs/src/routes/api/qwik-router/index.mdx @@ -321,6 +321,70 @@ string [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) +## createRenderer + +Creates the `render()` function that is required by `createQwikRouter()`. It requires a function that returns the `jsx` and `options` for the renderer. + +```typescript +createRenderer: ( + getOptions: (options: RendererOptions) => { + jsx: JSXOutput; + options: RendererOutputOptions; + }, +) => Render; +``` + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +getOptions + + + +(options: [RendererOptions](#rendereroptions)) => \{ jsx: JSXOutput; options: [RendererOutputOptions](#rendereroutputoptions); } + + + +
+**Returns:** + +Render + +```tsx +const renderer = createRenderer((opts) => { + if (opts.requestHeaders["x-hello"] === "world") { + return { jsx: , options: opts }; + } + return { + jsx: , + options: { + ...opts, + serverData: { + ...opts.serverData, + documentHead: { + meta: [{ name: "renderedAt", content: new Date().toISOString() }], + }, + }, + }, + }; +}); +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts) + ## DataValidator ```typescript @@ -1777,6 +1841,14 @@ export type PreventNavigateCallback = ( [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) +## Q_ROUTE + +```typescript +Q_ROUTE = "q:route"; +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/constants.ts) + ## QWIK_CITY_SCROLLER > Warning: This API is now obsolete. @@ -1990,6 +2062,98 @@ _(Optional)_ [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) +## QwikRouterEnvData + +```typescript +export interface QwikRouterEnvData +``` + + + + + + + +
+ +Property + + + +Modifiers + + + +Type + + + +Description + +
+ +[ev](./router.qwikrouterenvdata.ev.md) + + + + + +RequestEvent + + + +
+ +[loadedRoute](./router.qwikrouterenvdata.loadedroute.md) + + + + + +LoadedRoute \| null + + + +
+ +[params](./router.qwikrouterenvdata.params.md) + + + + + +[PathParams](#pathparams) + + + +
+ +[response](./router.qwikrouterenvdata.response.md) + + + + + +EndpointResponse + + + +
+ +[routeName](./router.qwikrouterenvdata.routename.md) + + + + + +string + + + +
+ +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) + ## QwikRouterMockProps ```typescript @@ -2122,6 +2286,32 @@ QwikRouterProvider: import("@qwik.dev/core").Component; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/qwik-router-component.tsx) +## RendererOptions + +```typescript +export type RendererOptions = Omit & { + serverData: ServerData; +}; +``` + +**References:** [ServerData](#serverdata) + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts) + +## RendererOutputOptions + +```typescript +export type RendererOutputOptions = Omit & { + serverData: ServerData & { + documentHead?: DocumentHeadValue; + } & Record; +}; +``` + +**References:** [ServerData](#serverdata), [DocumentHeadValue](#documentheadvalue) + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/create-renderer.ts) + ## ResolvedDocumentHead ```typescript @@ -2335,6 +2525,27 @@ _(Optional)_ [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts) +## ServerData + +The server data that is provided by Qwik Router during SSR rendering. It can be retrieved with `useServerData(key)` in the server, but it is not available in the client. + +```typescript +export type ServerData = { + url: string; + requestHeaders: Record; + locale: string | undefined; + nonce: string | undefined; + containerAttributes: Record & { + [Q_ROUTE]: string; + }; + qwikrouter: QwikRouterEnvData; +}; +``` + +**References:** [Q_ROUTE](#q_route), [QwikRouterEnvData](#qwikrouterenvdata) + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) + ## ServerFunction ```typescript diff --git a/packages/docs/src/routes/api/qwik-server/api.json b/packages/docs/src/routes/api/qwik-server/api.json index ac484a7b749..0ddb7aca42b 100644 --- a/packages/docs/src/routes/api/qwik-server/api.json +++ b/packages/docs/src/routes/api/qwik-server/api.json @@ -166,7 +166,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[base?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Specifies the root of the JS files of the client build. Setting a base, will cause the render of the `q:base` attribute in the `q:container` element.\n\n\n
\n\n[containerAttributes?](#)\n\n\n\n\n\n\n\nRecord<string, string>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[containerTagName?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ When set, the app is serialized into a fragment. And the returned html is not a complete document. Defaults to `html`\n\n\n
\n\n[locale?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Language to use when rendering the document.\n\n\n
\n\n[prefetchStrategy?](#)\n\n\n\n\n\n\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[preloader?](#)\n\n\n\n\n\n\n\n[PreloaderOptions](#preloaderoptions) \\| false\n\n\n\n\n_(Optional)_\n\n\n
\n\n[qwikLoader?](#)\n\n\n\n\n\n\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`.\n\n\n
\n\n[serverData?](#)\n\n\n\n\n\n\n\nRecord<string, any>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[snapshot?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Defaults to `true`\n\n\n
", + "content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[base?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Specifies the root of the JS files of the client build. Setting a base, will cause the render of the `q:base` attribute in the `q:container` element.\n\n\n
\n\n[containerAttributes?](#)\n\n\n\n\n\n\n\nRecord<string, string>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[containerTagName?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ When set, the app is serialized into a fragment. And the returned html is not a complete document. Defaults to `html`\n\n\n
\n\n[locale?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Language to use when rendering the document.\n\n\n
\n\n[prefetchStrategy?](#)\n\n\n\n\n\n\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[preloader?](#)\n\n\n\n\n\n\n\n[PreloaderOptions](#preloaderoptions) \\| false\n\n\n\n\n_(Optional)_\n\n\n
\n\n[qwikLoader?](#)\n\n\n\n\n\n\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`.\n\n\n
\n\n[serverData?](#)\n\n\n\n\n\n\n\nRecord<string, any>\n\n\n\n\n_(Optional)_ Metadata that can be retrieved during SSR with `useServerData()`.\n\n\n
\n\n[snapshot?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Defaults to `true`\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts", "mdFile": "core.renderoptions.md" }, diff --git a/packages/docs/src/routes/api/qwik-server/index.mdx b/packages/docs/src/routes/api/qwik-server/index.mdx index 01f00eab3dc..dec0fd1587e 100644 --- a/packages/docs/src/routes/api/qwik-server/index.mdx +++ b/packages/docs/src/routes/api/qwik-server/index.mdx @@ -753,7 +753,7 @@ Record<string, any> -_(Optional)_ +_(Optional)_ Metadata that can be retrieved during SSR with `useServerData()`. diff --git a/packages/insights/src/entry.ssr.tsx b/packages/insights/src/entry.ssr.tsx index 50920e7b0cf..2d2a9e5deee 100644 --- a/packages/insights/src/entry.ssr.tsx +++ b/packages/insights/src/entry.ssr.tsx @@ -1,24 +1,24 @@ /** * WHAT IS THIS FILE? * - * SSR entry point, in all cases the application is rendered outside the browser, this entry point - * will be the common one. + * SSR renderer function, used by Qwik Router. * - * - Server (express, cloudflare...) - * - `npm run start` - * - `npm run preview` - * - `npm run build` + * Note that this is the only place the Qwik renderer is called. On the client, containers resume + * and do not call render. */ -import { renderToStream, type RenderToStreamOptions } from '@qwik.dev/core/server'; +import { createRenderer } from '@qwik.dev/router'; import Root from './root'; -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - ...opts, - // Use container attributes to set attributes on the html tag. - containerAttributes: { - lang: 'en-us', - ...opts.containerAttributes, +export default createRenderer((opts) => { + return { + jsx: , + options: { + ...opts, + // Use container attributes to set attributes on the html tag. + containerAttributes: { + lang: 'en-us', + ...opts.containerAttributes, + }, }, - }); -} + }; +}); diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index a4c847bec7a..3032f6e5149 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -12,7 +12,7 @@ export const QLOADER_KEY = 'qloaders'; export const QFN_KEY = 'qfunc'; export const QDATA_KEY = 'qdata'; - +/** @public */ export const Q_ROUTE = 'q:route'; export const DEFAULT_LOADERS_SERIALIZATION_STRATEGY: SerializationStrategy = diff --git a/packages/qwik-router/src/runtime/src/create-renderer.ts b/packages/qwik-router/src/runtime/src/create-renderer.ts new file mode 100644 index 00000000000..c5836ea5d51 --- /dev/null +++ b/packages/qwik-router/src/runtime/src/create-renderer.ts @@ -0,0 +1,51 @@ +import type { JSXOutput } from '@qwik.dev/core'; +import { renderToStream, type Render, type RenderOptions } from '@qwik.dev/core/server'; +import type { DocumentHeadValue, ServerData } from './types'; + +/** @public */ +export type RendererOptions = Omit & { + serverData: ServerData; +}; +/** @public */ +export type RendererOutputOptions = Omit & { + serverData: ServerData & { + documentHead?: DocumentHeadValue; + } & Record; +}; + +/** + * Creates the `render()` function that is required by `createQwikRouter()`. It requires a function + * that returns the `jsx` and `options` for the renderer. + * + * @example + * + * ```tsx + * const renderer = createRenderer((opts) => { + * if (opts.requestHeaders['x-hello'] === 'world') { + * return { jsx: , options: opts }; + * } + * return { jsx: , options: { + * ...opts, + * serverData: { + * ...opts.serverData, + * documentHead: { + * meta: [ + * { name: 'renderedAt', content: new Date().toISOString() }, + * ], + * }, + * }, + * } }; + * }); + * ``` + * + * @public + */ +export const createRenderer = ( + getOptions: (options: RendererOptions) => { jsx: JSXOutput; options: RendererOutputOptions } +) => { + return ((opts: RendererOptions) => { + const { jsx, options } = getOptions(opts); + return renderToStream(jsx, options as any); + // We force the type to be Render because that's what createQwikRouter accepts + }) as unknown as Render; +}; diff --git a/packages/qwik-router/src/runtime/src/index.ts b/packages/qwik-router/src/runtime/src/index.ts index 11758ba6cad..49aaf7b5b59 100644 --- a/packages/qwik-router/src/runtime/src/index.ts +++ b/packages/qwik-router/src/runtime/src/index.ts @@ -29,6 +29,7 @@ export type { PreventNavigateCallback, QwikCityPlan, QwikRouterConfig, + QwikRouterEnvData, RequestEvent, RequestEventAction, RequestEventBase, @@ -45,6 +46,7 @@ export type { ValidatorErrorType, ZodConstructor, } from './types'; +export type { Q_ROUTE } from './constants'; export { ErrorBoundary } from './error-boundary'; export { Link, type LinkProps } from './link-component'; @@ -107,3 +109,9 @@ export type { TypedDataValidator, ValidatorReturn, } from './types'; + +export { + createRenderer, + type RendererOptions, + type RendererOutputOptions, +} from './create-renderer'; diff --git a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md index ba9e8ef0089..1629f7710a9 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md +++ b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md @@ -16,6 +16,8 @@ import { QRLEventHandlerMulti } from '@qwik.dev/core'; import { QwikIntrinsicElements } from '@qwik.dev/core'; import { QwikJSX } from '@qwik.dev/core'; import type { ReadonlySignal } from '@qwik.dev/core'; +import { Render } from '@qwik.dev/core/server'; +import { RenderOptions } from '@qwik.dev/core/server'; import { RequestEvent } from '@qwik.dev/router/middleware/request-handler'; import { RequestEventAction } from '@qwik.dev/router/middleware/request-handler'; import { RequestEventBase } from '@qwik.dev/router/middleware/request-handler'; @@ -99,6 +101,12 @@ export { CookieOptions } export { CookieValue } +// @public +export const createRenderer: (getOptions: (options: RendererOptions) => { + jsx: JSXOutput_2; + options: RendererOutputOptions; +}) => Render; + // @public (undocumented) export type DataValidator = {}> = { validate(ev: RequestEvent, data: unknown): Promise>; @@ -331,6 +339,9 @@ export type PathParams = Record; // @public (undocumented) export type PreventNavigateCallback = (url?: number | URL) => ValueOrPromise; +// @public (undocumented) +export const Q_ROUTE = "q:route"; + // @public @deprecated (undocumented) export const QWIK_CITY_SCROLLER = "_qCityScroller"; @@ -368,6 +379,24 @@ export interface QwikRouterConfig { readonly trailingSlash?: boolean; } +// @public (undocumented) +export interface QwikRouterEnvData { + // (undocumented) + ev: RequestEvent; + // Warning: (ae-forgotten-export) The symbol "LoadedRoute" needs to be exported by the entry point index.d.ts + // + // (undocumented) + loadedRoute: LoadedRoute | null; + // (undocumented) + params: PathParams; + // Warning: (ae-forgotten-export) The symbol "EndpointResponse" needs to be exported by the entry point index.d.ts + // + // (undocumented) + response: EndpointResponse; + // (undocumented) + routeName: string; +} + // @public (undocumented) export interface QwikRouterMockProps { // (undocumented) @@ -389,6 +418,18 @@ export interface QwikRouterProps { // @public (undocumented) export const QwikRouterProvider: Component; +// @public (undocumented) +export type RendererOptions = Omit & { + serverData: ServerData; +}; + +// @public (undocumented) +export type RendererOutputOptions = Omit & { + serverData: ServerData & { + documentHead?: DocumentHeadValue; + } & Record; +}; + export { RequestEvent } export { RequestEventAction } @@ -461,6 +502,18 @@ export const RouterOutlet: Component; // @public (undocumented) export const server$: (qrl: T, options?: ServerConfig | undefined) => ServerQRL; +// @public +export type ServerData = { + url: string; + requestHeaders: Record; + locale: string | undefined; + nonce: string | undefined; + containerAttributes: Record & { + [Q_ROUTE]: string; + }; + qwikrouter: QwikRouterEnvData; +}; + // @public (undocumented) export type ServerFunction = { (this: RequestEventBase, ...args: any[]): any; diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 4f15cbe84c6..0420e615c0c 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -18,6 +18,7 @@ import type { } from '@qwik.dev/router/middleware/request-handler'; import type * as v from 'valibot'; import type * as z from 'zod'; +import type { Q_ROUTE } from './constants'; export type { Cookie, @@ -349,6 +350,7 @@ export interface StaticGenerate { /** @deprecated Use `QwikRouterEnvData` instead. Will be removed in V3. */ export type QwikCityEnvData = QwikRouterEnvData; +/** @public */ export interface QwikRouterEnvData { routeName: string; ev: RequestEvent; @@ -357,6 +359,16 @@ export interface QwikRouterEnvData { loadedRoute: LoadedRoute | null; } +/** @public The server data that is provided by Qwik Router during SSR rendering. It can be retrieved with `useServerData(key)` in the server, but it is not available in the client. */ +export type ServerData = { + url: string; + requestHeaders: Record; + locale: string | undefined; + nonce: string | undefined; + containerAttributes: Record & { [Q_ROUTE]: string }; + qwikrouter: QwikRouterEnvData; +}; + export interface SimpleURL { origin: string; href: string; diff --git a/packages/qwik/src/server/qwik.server.api.md b/packages/qwik/src/server/qwik.server.api.md index 89744905a84..866ce994137 100644 --- a/packages/qwik/src/server/qwik.server.api.md +++ b/packages/qwik/src/server/qwik.server.api.md @@ -103,7 +103,6 @@ export interface RenderOptions extends SerializeDocumentOptions { // (undocumented) preloader?: PreloaderOptions | false; qwikLoader?: QwikLoaderOptions; - // (undocumented) serverData?: Record; snapshot?: boolean; } diff --git a/packages/qwik/src/server/types.ts b/packages/qwik/src/server/types.ts index a807989e126..5e7d60a6fe4 100644 --- a/packages/qwik/src/server/types.ts +++ b/packages/qwik/src/server/types.ts @@ -168,6 +168,7 @@ export interface RenderOptions extends SerializeDocumentOptions { */ containerTagName?: string; containerAttributes?: Record; + /** Metadata that can be retrieved during SSR with `useServerData()`. */ serverData?: Record; } diff --git a/starters/apps/base/src/entry.ssr.tsx b/starters/apps/base/src/entry.ssr.tsx index 62fefc67a42..e510db518c4 100644 --- a/starters/apps/base/src/entry.ssr.tsx +++ b/starters/apps/base/src/entry.ssr.tsx @@ -1,27 +1,24 @@ /** * WHAT IS THIS FILE? * - * SSR renderer function, used for all build/dev targets except client-only. + * SSR renderer function, used by Qwik Router. * - * Note that except for client-only, this is the only place the Qwik renderer is called. + * Note that this is the only place the Qwik renderer is called. * On the client, containers resume and do not call render. */ -import { - renderToStream, - type RenderToStreamOptions, -} from "@qwik.dev/core/server"; +import { createRenderer } from "@qwik.dev/router"; import Root from "./root"; -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - ...opts, - // Use container attributes to set attributes on the html tag. - containerAttributes: { - lang: "en-us", - ...opts.containerAttributes, +export default createRenderer((opts) => { + return { + jsx: , + options: { + ...opts, + // Use container attributes to set attributes on the html tag. + containerAttributes: { + lang: "en-us", + ...opts.containerAttributes, + }, }, - serverData: { - ...opts.serverData, - }, - }); -} + }; +}); diff --git a/starters/features/localize/src/entry.ssr.tsx b/starters/features/localize/src/entry.ssr.tsx index 158f962d791..0d9b34dae69 100644 --- a/starters/features/localize/src/entry.ssr.tsx +++ b/starters/features/localize/src/entry.ssr.tsx @@ -1,30 +1,26 @@ /** * WHAT IS THIS FILE? * - * SSR entry point, in all cases the application is rendered outside the browser, this - * entry point will be the common one. - * - * - Server (express, cloudflare...) - * - npm run start - * - npm run preview - * - npm run build + * SSR renderer function, used by Qwik Router. * + * Note that this is the only place the Qwik renderer is called. + * On the client, containers resume and do not call render. */ -import { - renderToStream, - type RenderToStreamOptions, -} from "@qwik.dev/core/server"; +import { createRenderer } from "@qwik.dev/router"; import Root from "./root"; import { extractBase } from "./routes/[locale]/i18n-utils"; -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - ...opts, - base: extractBase, // determine the base URL for the client code - // Use container attributes to set attributes on the html tag. - containerAttributes: { - lang: opts.serverData?.locale ?? "en-us", - ...opts.containerAttributes, +export default createRenderer((opts) => { + return { + jsx: , + options: { + ...opts, + base: extractBase, // determine the base URL for the client code + // Use container attributes to set attributes on the html tag. + containerAttributes: { + lang: opts.serverData?.locale ?? "en-us", + ...opts.containerAttributes, + }, }, - }); -} + }; +}); From bbfc9e48014c1a9571460f6708acbba728319dc1 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 3 Aug 2025 13:09:52 +0200 Subject: [PATCH 2/2] feat(router): `documentHead` via `serverData` the createRenderer type was already updated in the previous commit --- .changeset/stale-corners-flow.md | 5 ++++ .../routes/docs/(qwikrouter)/pages/index.mdx | 25 +++++++++++++++++++ packages/qwik-router/src/runtime/src/head.ts | 19 +++++++------- packages/qwik-router/src/runtime/src/index.ts | 1 + .../src/runtime/src/qwik-router-component.tsx | 14 +++++++++-- starters/apps/base/src/entry.ssr.tsx | 7 ++++++ .../apps/qwikrouter-test/src/entry.ssr.tsx | 9 +++++++ .../src/routes/(common)/index.tsx | 5 ---- starters/e2e/qwikrouter/head.e2e.ts | 12 +++++++++ starters/e2e/qwikrouter/page.e2e.ts | 2 +- 10 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 .changeset/stale-corners-flow.md create mode 100644 starters/e2e/qwikrouter/head.e2e.ts diff --git a/.changeset/stale-corners-flow.md b/.changeset/stale-corners-flow.md new file mode 100644 index 00000000000..f3611a71421 --- /dev/null +++ b/.changeset/stale-corners-flow.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': minor +--- + +feat: You can now put `documentHead` into the rendering functions as part of the `serverData` option. This is useful for passing title, meta tags, scripts, etc. to the `useDocumentHead()` hook from within the server. diff --git a/packages/docs/src/routes/docs/(qwikrouter)/pages/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/pages/index.mdx index fe57c84a55e..2fc5a7deee0 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/pages/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/pages/index.mdx @@ -121,6 +121,31 @@ export const head: DocumentHead = ({resolveValue, params}) => { }; ``` +### Server-injected Head + +You can also pass `documentHead` to `createRenderer()` as part of the `serverData` option. + +The values passed will be used as the default values for `useDocumentHead()`, before the `head` exports are resolved. + +```tsx title="src/entry.ssr.tsx" {10} +import { createRenderer } from "@qwik.dev/router"; +import Root from "./root"; + +export default createRenderer((opts) => { + return { + app: , + options: { + ...opts, + serverData: { + ...opts.serverData, + documentHead: { + title: "My App", + }, + }, + } +}); +``` + ### Nested Layouts and Head In an advanced case, a [layout](/docs/(qwikrouter)/layout/index.mdx) may want to modify the document title of an already resolved document head. In the example below, the page component returns the title of `Foo`. The containing layout component can read the value of the page's document head and modify it. In this example, the layout component is adding `MyCompany - ` to the title, so that when rendered, the title will be `MyCompany - Foo`. Every layout in the stack has the opportunity to return a new value. diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index 4a853b128d2..cab7b757ee2 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -18,9 +18,10 @@ export const resolveHead = ( endpoint: EndpointResponse | ClientPageData, routeLocation: RouteLocation, contentModules: ContentModule[], - locale: string + locale: string, + defaults?: DocumentHeadValue ) => { - const head = createDocumentHead(); + const head = createDocumentHead(defaults); const getData = ((loaderOrAction: LoaderInternal | ActionInternal) => { const id = loaderOrAction.__id; if (loaderOrAction.__brand === 'server_loader') { @@ -92,11 +93,11 @@ const mergeArray = ( } }; -export const createDocumentHead = (): ResolvedDocumentHead => ({ - title: '', - meta: [], - links: [], - styles: [], - scripts: [], - frontmatter: {}, +export const createDocumentHead = (defaults?: DocumentHeadValue): ResolvedDocumentHead => ({ + title: defaults?.title || '', + meta: [...(defaults?.meta || [])], + links: [...(defaults?.links || [])], + styles: [...(defaults?.styles || [])], + scripts: [...(defaults?.scripts || [])], + frontmatter: { ...defaults?.frontmatter }, }); diff --git a/packages/qwik-router/src/runtime/src/index.ts b/packages/qwik-router/src/runtime/src/index.ts index 49aaf7b5b59..6ff429a152b 100644 --- a/packages/qwik-router/src/runtime/src/index.ts +++ b/packages/qwik-router/src/runtime/src/index.ts @@ -40,6 +40,7 @@ export type { RouteData, RouteLocation, RouteNavigate, + ServerData, StaticGenerate, StaticGenerateHandler, ValidatorErrorKeyDotNotation, diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index a42f13d50fb..f123e56dc87 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -55,6 +55,7 @@ import type { ContentModule, ContentState, ContentStateInternal, + DocumentHeadValue, Editable, EndpointResponse, LoadedRoute, @@ -150,6 +151,7 @@ export const QwikRouterProvider = component$((props) => { if (!urlEnv) { throw new Error(`Missing Qwik URL Env Data`); } + const serverHead = useServerData('documentHead'); if (isServer) { if ( @@ -217,7 +219,9 @@ export const QwikRouterProvider = component$((props) => { replaceState: false, scroll: true, }); - const documentHead = useStore>(createDocumentHead); + const documentHead = useStore>(() => + createDocumentHead(serverHead) + ); const content = useStore>({ headings: undefined, menu: undefined, @@ -487,7 +491,13 @@ export const QwikRouterProvider = component$((props) => { (routeInternal as any).untrackedValue = { type: navType, dest: trackUrl }; // Needs to be done after routeLocation is updated - const resolvedHead = resolveHead(clientPageData!, routeLocation, contentModules, locale); + const resolvedHead = resolveHead( + clientPageData!, + routeLocation, + contentModules, + locale, + serverHead + ); // Update content content.headings = pageModule.headings; diff --git a/starters/apps/base/src/entry.ssr.tsx b/starters/apps/base/src/entry.ssr.tsx index e510db518c4..2bad927bf71 100644 --- a/starters/apps/base/src/entry.ssr.tsx +++ b/starters/apps/base/src/entry.ssr.tsx @@ -19,6 +19,13 @@ export default createRenderer((opts) => { lang: "en-us", ...opts.containerAttributes, }, + serverData: { + ...opts.serverData, + // These are the default values for the document head and are overridden by the `head` exports + // documentHead: { + // title: "My App", + // }, + }, }, }; }); diff --git a/starters/apps/qwikrouter-test/src/entry.ssr.tsx b/starters/apps/qwikrouter-test/src/entry.ssr.tsx index 6da6d55d53d..a3f859b1965 100644 --- a/starters/apps/qwikrouter-test/src/entry.ssr.tsx +++ b/starters/apps/qwikrouter-test/src/entry.ssr.tsx @@ -8,5 +8,14 @@ export default function (opts: RenderToStreamOptions) { return renderToStream(, { base: "/qwikrouter-test/build/", ...opts, + serverData: { + ...opts.serverData, + // ensure that documentHead injection works + documentHead: { + title: "Qwik Router Test", + meta: [{ name: "hello", content: "world" }], + scripts: [{ key: "hello", script: 'window.hello = "world";' }], + }, + }, }); } diff --git a/starters/apps/qwikrouter-test/src/routes/(common)/index.tsx b/starters/apps/qwikrouter-test/src/routes/(common)/index.tsx index 4f53a580178..55148df8fee 100644 --- a/starters/apps/qwikrouter-test/src/routes/(common)/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/(common)/index.tsx @@ -1,5 +1,4 @@ import { component$ } from "@qwik.dev/core"; -import type { DocumentHead } from "@qwik.dev/router"; // @ts-ignore import ImageJpeg from "../../media/MyTest.jpeg?jsx"; // @ts-ignore @@ -18,7 +17,3 @@ export default component$(() => { ); }); - -export const head: DocumentHead = { - title: "Welcome to Qwik Router", -}; diff --git a/starters/e2e/qwikrouter/head.e2e.ts b/starters/e2e/qwikrouter/head.e2e.ts new file mode 100644 index 00000000000..d4230ca9a51 --- /dev/null +++ b/starters/e2e/qwikrouter/head.e2e.ts @@ -0,0 +1,12 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Qwik Router documentHead", () => { + test("pass documentHead to Qwik", async ({ page }) => { + await page.goto("/qwikrouter-test/"); + // injected title via renderToStream serverData + await expect(page).toHaveTitle("Qwik Router Test - Qwik"); + const meta = page.locator("meta[name='hello']"); + await expect(meta).toHaveAttribute("content", "world"); + expect(await page.evaluate(() => (window as any).hello)).toBe("world"); + }); +}); diff --git a/starters/e2e/qwikrouter/page.e2e.ts b/starters/e2e/qwikrouter/page.e2e.ts index bbb6bedb9bb..f7841998365 100644 --- a/starters/e2e/qwikrouter/page.e2e.ts +++ b/starters/e2e/qwikrouter/page.e2e.ts @@ -27,7 +27,7 @@ function tests() { /*********** Home Page ***********/ await assertPage(ctx, { pathname: "/qwikrouter-test/", - title: "Welcome to Qwik Router - Qwik", + title: "Qwik Router Test - Qwik", layoutHierarchy: ["root"], h1: "Welcome to Qwik Router", activeHeaderLink: false,