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/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/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 11758ba6cad..6ff429a152b 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,
@@ -39,12 +40,14 @@ export type {
RouteData,
RouteLocation,
RouteNavigate,
+ ServerData,
StaticGenerate,
StaticGenerateHandler,
ValidatorErrorKeyDotNotation,
ValidatorErrorType,
ZodConstructor,
} from './types';
+export type { Q_ROUTE } from './constants';
export { ErrorBoundary } from './error-boundary';
export { Link, type LinkProps } from './link-component';
@@ -107,3 +110,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-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/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..2bad927bf71 100644
--- a/starters/apps/base/src/entry.ssr.tsx
+++ b/starters/apps/base/src/entry.ssr.tsx
@@ -1,27 +1,31 @@
/**
* 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,
+ // These are the default values for the document head and are overridden by the `head` exports
+ // documentHead: {
+ // title: "My App",
+ // },
+ },
},
- serverData: {
- ...opts.serverData,
- },
- });
-}
+ };
+});
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,
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,
+ },
},
- });
-}
+ };
+});
|