Skip to content

Commit 61ff48a

Browse files
committed
feat(ssr): pass additional documentHead via serverData
1 parent b98bd37 commit 61ff48a

File tree

13 files changed

+88
-18
lines changed

13 files changed

+88
-18
lines changed

.changeset/quiet-friends-taste.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@qwik.dev/router': minor
3+
'@qwik.dev/core': minor
4+
---
5+
6+
feat: You can now pass `documentHead` into the `renderToStream()` and `renderToString()` 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.

packages/docs/src/routes/api/qwik-server/api.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
}
167167
],
168168
"kind": "Interface",
169-
"content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[base?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| ((options: [RenderOptions](#renderoptions)<!-- -->) =&gt; string)\n\n\n</td><td>\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</td></tr>\n<tr><td>\n\n[containerAttributes?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nRecord&lt;string, string&gt;\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[containerTagName?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\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</td></tr>\n<tr><td>\n\n[locale?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| ((options: [RenderOptions](#renderoptions)<!-- -->) =&gt; string)\n\n\n</td><td>\n\n_(Optional)_ Language to use when rendering the document.\n\n\n</td></tr>\n<tr><td>\n\n[prefetchStrategy?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[preloader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PreloaderOptions](#preloaderoptions) \\| false\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[qwikLoader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n</td><td>\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[serverData?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nRecord&lt;string, any&gt;\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[snapshot?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Defaults to `true`\n\n\n</td></tr>\n</tbody></table>",
169+
"content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[base?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| ((options: [RenderOptions](#renderoptions)<!-- -->) =&gt; string)\n\n\n</td><td>\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</td></tr>\n<tr><td>\n\n[containerAttributes?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nRecord&lt;string, string&gt;\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[containerTagName?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\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</td></tr>\n<tr><td>\n\n[locale?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| ((options: [RenderOptions](#renderoptions)<!-- -->) =&gt; string)\n\n\n</td><td>\n\n_(Optional)_ Language to use when rendering the document.\n\n\n</td></tr>\n<tr><td>\n\n[prefetchStrategy?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[preloader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PreloaderOptions](#preloaderoptions) \\| false\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[qwikLoader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n</td><td>\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[serverData?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nRecord&lt;string, any&gt; &amp; { documentHead?: import('@qwik.dev/router').DocumentHeadValue; }\n\n\n</td><td>\n\n_(Optional)_ Metadata that can be retrieved during SSR with `useServerData()`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[snapshot?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Defaults to `true`\n\n\n</td></tr>\n</tbody></table>",
170170
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts",
171171
"mdFile": "core.renderoptions.md"
172172
},
@@ -381,4 +381,4 @@
381381
"mdFile": "core.versions.md"
382382
}
383383
]
384-
}
384+
}

packages/docs/src/routes/api/qwik-server/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,11 +749,11 @@ Defaults to `{ include: true }`.
749749
750750
</td><td>
751751
752-
Record&lt;string, any&gt;
752+
Record&lt;string, any&gt; &amp; \{ documentHead?: import('@qwik.dev/router').DocumentHeadValue; }
753753
754754
</td><td>
755755
756-
_(Optional)_
756+
_(Optional)_ Metadata that can be retrieved during SSR with `useServerData()`.
757757
758758
</td></tr>
759759
<tr><td>

packages/docs/src/routes/docs/(qwikrouter)/pages/index.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ export const head: DocumentHead = ({resolveValue, params}) => {
121121
};
122122
```
123123

124+
### Server-injected Head
125+
126+
You can also pass `documentHead` into the `renderToStream()` and `renderToString()` functions as part of the `serverData` option.
127+
128+
The values passed will be used as the default values for the document head and are overridden by the `head` exports.
129+
130+
```tsx title="src/entry.ssr.tsx" {10}
131+
import {
132+
renderToStream,
133+
type RenderToStreamOptions,
134+
} from "@qwik.dev/core/server";
135+
import Root from "./root";
136+
137+
export default function (opts: RenderToStreamOptions) {
138+
return renderToStream(<Root />, {
139+
...opts,
140+
serverData: {
141+
...opts.serverData,
142+
documentHead: {
143+
title: "My App",
144+
},
145+
},
146+
});
147+
}
148+
```
149+
124150
### Nested Layouts and Head
125151

126152
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.

packages/qwik-router/src/runtime/src/head.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ export const resolveHead = (
1818
endpoint: EndpointResponse | ClientPageData,
1919
routeLocation: RouteLocation,
2020
contentModules: ContentModule[],
21-
locale: string
21+
locale: string,
22+
fromServer?: DocumentHeadValue
2223
) => {
23-
const head = createDocumentHead();
24+
const head = createDocumentHead(fromServer);
2425
const getData = ((loaderOrAction: LoaderInternal | ActionInternal) => {
2526
const id = loaderOrAction.__id;
2627
if (loaderOrAction.__brand === 'server_loader') {
@@ -92,11 +93,12 @@ const mergeArray = (
9293
}
9394
};
9495

95-
export const createDocumentHead = (): ResolvedDocumentHead => ({
96+
export const createDocumentHead = (fromServer?: DocumentHeadValue): ResolvedDocumentHead => ({
9697
title: '',
9798
meta: [],
9899
links: [],
99100
styles: [],
100101
scripts: [],
101102
frontmatter: {},
103+
...fromServer,
102104
});

packages/qwik-router/src/runtime/src/qwik-router-component.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
ContentModule,
5656
ContentState,
5757
ContentStateInternal,
58+
DocumentHeadValue,
5859
Editable,
5960
EndpointResponse,
6061
LoadedRoute,
@@ -150,6 +151,7 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
150151
if (!urlEnv) {
151152
throw new Error(`Missing Qwik URL Env Data`);
152153
}
154+
const serverHead = useServerData<DocumentHeadValue>('documentHead');
153155

154156
if (isServer) {
155157
if (
@@ -217,7 +219,9 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
217219
replaceState: false,
218220
scroll: true,
219221
});
220-
const documentHead = useStore<Editable<ResolvedDocumentHead>>(createDocumentHead);
222+
const documentHead = useStore<Editable<ResolvedDocumentHead>>(() =>
223+
createDocumentHead(serverHead)
224+
);
221225
const content = useStore<Editable<ContentState>>({
222226
headings: undefined,
223227
menu: undefined,
@@ -487,7 +491,13 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
487491
(routeInternal as any).untrackedValue = { type: navType, dest: trackUrl };
488492

489493
// Needs to be done after routeLocation is updated
490-
const resolvedHead = resolveHead(clientPageData!, routeLocation, contentModules, locale);
494+
const resolvedHead = resolveHead(
495+
clientPageData!,
496+
routeLocation,
497+
contentModules,
498+
locale,
499+
serverHead
500+
);
491501

492502
// Update content
493503
content.headings = pageModule.headings;

packages/qwik/src/server/qwik.server.api.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import { DocumentHeadValue } from '@qwik.dev/router';
78
import type { QwikManifest } from '@qwik.dev/core/optimizer';
89
import type { ResolvedManifest } from '@qwik.dev/core/optimizer';
910
import type { ServerQwikManifest } from '@qwik.dev/core/optimizer';
@@ -103,8 +104,9 @@ export interface RenderOptions extends SerializeDocumentOptions {
103104
// (undocumented)
104105
preloader?: PreloaderOptions | false;
105106
qwikLoader?: QwikLoaderOptions;
106-
// (undocumented)
107-
serverData?: Record<string, any>;
107+
serverData?: Record<string, any> & {
108+
documentHead?: DocumentHeadValue;
109+
};
108110
snapshot?: boolean;
109111
}
110112

packages/qwik/src/server/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ export interface RenderOptions extends SerializeDocumentOptions {
168168
*/
169169
containerTagName?: string;
170170
containerAttributes?: Record<string, string>;
171-
serverData?: Record<string, any>;
171+
/** Metadata that can be retrieved during SSR with `useServerData()`. */
172+
serverData?: Record<string, any> & {
173+
// note that this is leaking Qwik Router into Qwik but it's just types
174+
documentHead?: import('@qwik.dev/router').DocumentHeadValue;
175+
};
172176
}
173177

174178
/** @public */

starters/apps/base/src/entry.ssr.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export default function (opts: RenderToStreamOptions) {
2222
},
2323
serverData: {
2424
...opts.serverData,
25+
// These are the default values for the document head and are overridden by the `head` exports
26+
// documentHead: {
27+
// title: "My App",
28+
// },
2529
},
2630
});
2731
}

starters/apps/qwikrouter-test/src/entry.ssr.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,14 @@ export default function (opts: RenderToStreamOptions) {
88
return renderToStream(<Root />, {
99
base: "/qwikrouter-test/build/",
1010
...opts,
11+
serverData: {
12+
...opts.serverData,
13+
// ensure that documentHead injection works
14+
documentHead: {
15+
title: "Qwik Router Test",
16+
meta: [{ name: "hello", content: "world" }],
17+
scripts: [{ key: "hello", script: 'window.hello = "world";' }],
18+
},
19+
},
1120
});
1221
}

0 commit comments

Comments
 (0)