Skip to content

Commit 95905b8

Browse files
authored
feat(vue): replace unihead with unhead (#230)
* feat(vue): replace unihead with unhead * chore: add bodyattr to streaming page * refactor: pass head to configure To allow registering hooks in unhead * chore: update lockfile * fix: sync virtual ts with virtual
1 parent 817d02a commit 95905b8

File tree

18 files changed

+766
-627
lines changed

18 files changed

+766
-627
lines changed

docs/vue/route-modules.md

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,80 @@ export default {
140140

141141
**`@fastify/vue`** renders `<head>` elements **independently** from SSR. This allows you to fetch data for populating `<meta>` tags first, stream them right away to the client, and only then perform SSR.
142142

143-
> Under the hood, it uses the [`unihead`](https://github.com/galvez/unihead) library, which has a SSR function and a browser library that allows for dynamic changes during client-side navigation. This is a very small library built specifically for `@fastify/vite` core renderers, and used in the current implementation of `createHtmlFunction()` for `@fastify/vue`. This may change in the futuree as other libraries are considered, but for most use cases it should be enough.
144-
145-
To populate `<title>`, `<meta>` and `<link>` elements, export a `getMeta()` function that returns an object matching the interface expected by [unihead](https://github.com/galvez/unihead):
143+
To populate `<head>` elements, export a `getMeta()` function that returns an object matching the interface expected by [unhead](https://github.com/unjs/unhead):
146144

147145
```ts
148-
interface RouteMeta {
149-
title?: string | null,
150-
html?: Record<string, string> | null
151-
body?: Record<string, string> | null
152-
meta?: Record<string, string>[] | null,
153-
link?: Record<string, string>[] | null,
146+
interface ReactiveHead {
147+
/**
148+
* The `<title>` HTML element defines the document's title that is shown in a browser's title bar or a page's tab.
149+
* It only contains text; tags within the element are ignored.
150+
*
151+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title
152+
*/
153+
title?: ResolvableTitle;
154+
/**
155+
* Generate the title from a template.
156+
*/
157+
titleTemplate?: ResolvableTitleTemplate;
158+
/**
159+
* Variables used to substitute in the title and meta content.
160+
*/
161+
templateParams?: ResolvableProperties<{
162+
separator?: '|' | '-' | '·' | string;
163+
} & Record<string, Stringable | ResolvableProperties<Record<string, Stringable>>>>;
164+
/**
165+
* The `<base>` HTML element specifies the base URL to use for all relative URLs in a document.
166+
* There can be only one <base> element in a document.
167+
*
168+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
169+
*/
170+
base?: ResolvableBase;
171+
/**
172+
* The `<link>` HTML element specifies relationships between the current document and an external resource.
173+
* This element is most commonly used to link to stylesheets, but is also used to establish site icons
174+
* (both "favicon" style icons and icons for the home screen and apps on mobile devices) among other things.
175+
*
176+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as
177+
*/
178+
link?: ResolvableArray<ResolvableLink>;
179+
/**
180+
* The `<meta>` element represents metadata that cannot be expressed in other HTML elements, like `<link>` or `<script>`.
181+
*
182+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
183+
*/
184+
meta?: ResolvableArray<ResolvableMeta>;
185+
/**
186+
* The `<style>` HTML element contains style information for a document, or part of a document.
187+
* It contains CSS, which is applied to the contents of the document containing the `<style>` element.
188+
*
189+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
190+
*/
191+
style?: ResolvableArray<(ResolvableStyle | string)>;
192+
/**
193+
* The `<script>` HTML element is used to embed executable code or data; this is typically used to embed or refer to JavaScript code.
194+
*
195+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
196+
*/
197+
script?: ResolvableArray<(ResolvableScript | string)>;
198+
/**
199+
* The `<noscript>` HTML element defines a section of HTML to be inserted if a script type on the page is unsupported
200+
* or if scripting is currently turned off in the browser.
201+
*
202+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript
203+
*/
204+
noscript?: ResolvableArray<(ResolvableNoscript | string)>;
205+
/**
206+
* Attributes for the `<html>` HTML element.
207+
*
208+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html
209+
*/
210+
htmlAttrs?: ResolvableHtmlAttributes;
211+
/**
212+
* Attributes for the `<body>` HTML element.
213+
*
214+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body
215+
*/
216+
bodyAttrs?: ResolvableBodyAttributes;
154217
}
155218
```
156219

@@ -176,7 +239,7 @@ export function getMeta (ctx) {
176239
return {
177240
title: ctx.data.page.title,
178241
meta: [
179-
{ name: 'twitter:title', value: ctx.data.page.title },
242+
{ name: 'twitter:title', content: ctx.data.page.title },
180243
]
181244
}
182245
}

packages/fastify-vue/client.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function useRouteContext () {
1313
return useRoute().meta[serverRouteContext]
1414
}
1515

16-
export function createBeforeEachHandler ({ routeMap, ctxHydration, head }, layout) {
16+
export function createBeforeEachHandler ({ routeMap, ctxHydration }, layout) {
1717
return async function beforeCreate (to) {
1818
// The client-side route context
1919
const ctx = routeMap[to.matched[0].path]
@@ -48,7 +48,8 @@ export function createBeforeEachHandler ({ routeMap, ctxHydration, head }, layou
4848
// memoized module, so there's barely any overhead
4949
const { getMeta, onEnter } = await ctx.loader()
5050
if (ctx.getMeta) {
51-
head.update(await getMeta(ctx))
51+
ctx.head = await getMeta(ctx)
52+
ctxHydration.useHead.push(ctx.head)
5253
}
5354
if (ctx.onEnter) {
5455
const updatedData = await onEnter(ctx)

packages/fastify-vue/context.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default class RouteContext {
5050
return {
5151
state: this.state,
5252
data: this.data,
53+
head: this.head,
5354
layout: this.layout,
5455
getMeta: this.getMeta,
5556
getData: this.getData,

packages/fastify-vue/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737
},
3838
"dependencies": {
3939
"@fastify/vite": "workspace:^",
40+
"@unhead/vue": "^2.0.5",
4041
"acorn": "^8.12.1",
4142
"acorn-walk": "^8.3.4",
4243
"devalue": "latest",
4344
"html-rewriter-wasm": "^0.4.1",
4445
"mlly": "^1.5.0",
45-
"unihead": "^0.8.0",
4646
"vue": "^3.5.8",
4747
"vue-router": "^4.4.5",
4848
"youch": "^3.3.4"

packages/fastify-vue/rendering.js

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Readable } from 'node:stream'
22
import { renderToString, renderToNodeStream } from 'vue/server-renderer'
33
import * as devalue from 'devalue'
4-
import Head from 'unihead'
4+
import { createHead, transformHtmlTemplate } from '@unhead/vue/server'
55
import { createHtmlTemplates } from './templating.js'
66

77
export async function createRenderFunction ({ routes, create }) {
@@ -45,6 +45,12 @@ export async function createHtmlFunction (source, _, config) {
4545
return async function () {
4646
const { routes, context, body } = await this.render()
4747

48+
// Apply head attributes
49+
if (!context.useHead) {
50+
context.useHead = createHead()
51+
}
52+
53+
context.useHead.push(context.head)
4854
this.type('text/html')
4955

5056
// Use template with client module import removed
@@ -94,35 +100,47 @@ export async function createHtmlFunction (source, _, config) {
94100
}
95101
}
96102

97-
export function sendClientOnlyShell (templates, context) {
98-
context.head = new Head(context.head).render()
99-
return `${
100-
templates.beforeElement(context)
101-
}${
102-
templates.afterElement(context)
103-
}`
103+
export async function sendClientOnlyShell (templates, context) {
104+
return await transformHtmlTemplate(
105+
context.useHead,
106+
`${
107+
templates.beforeElement(context)
108+
}${
109+
templates.afterElement(context)
110+
}`
111+
)
104112
}
105113

106-
export function sendShell (templates, context, body) {
107-
context.head = new Head(context.head).render()
108-
return `${
114+
export async function sendShell (templates, context, body) {
115+
return await transformHtmlTemplate(
116+
context.useHead,
117+
`${
109118
templates.beforeElement(context)
110119
}${
111120
body
112121
}${
113122
templates.afterElement(context)
114-
}`
123+
}`)
115124
}
116125

117-
export function streamShell (templates, context, body) {
118-
context.head = new Head(context.head).render()
126+
export async function streamShell (templates, context, body) {
119127
return Readable.from(createShellStream(templates, context, body))
120128
}
121129

122130
async function * createShellStream (templates, context, body) {
123-
yield templates.beforeElement(context)
131+
yield await transformHtmlTemplate(
132+
context.useHead,
133+
templates.beforeElement(context)
134+
)
135+
124136
for await (const chunk of body) {
125-
yield chunk
137+
yield await transformHtmlTemplate(
138+
context.useHead,
139+
chunk
140+
)
126141
}
127-
yield templates.afterElement(context)
142+
yield await transformHtmlTemplate(
143+
context.useHead,
144+
templates.afterElement(context)
145+
)
128146
}

packages/fastify-vue/virtual-ts/create.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
routeLayout,
77
createBeforeEachHandler,
88
} from '@fastify/vue/client'
9+
import { createHead as createClientHead } from '@unhead/vue/client'
10+
import { createHead as createServerHead } from '@unhead/vue/server'
911

1012
import * as root from '$app/root.vue'
1113

@@ -28,6 +30,10 @@ export default async function create (ctx) {
2830
const isServer = import.meta.env.SSR
2931
instance.config.globalProperties.$isServer = isServer
3032

33+
const head = isServer ? createServerHead() : createClientHead()
34+
instance.use(head)
35+
ctxHydration.useHead = head
36+
3137
instance.provide(routeLayout, layoutRef)
3238
if (!isServer && ctxHydration.state) {
3339
ctxHydration.state = reactive(ctxHydration.state)
@@ -42,7 +48,7 @@ export default async function create (ctx) {
4248
instance.use(router)
4349

4450
if (typeof root.configure === 'function') {
45-
await root.configure({ app: instance, router })
51+
await root.configure({ app: instance, router, head })
4652
}
4753

4854
return { instance, ctx, state: ctxHydration.state, router }

packages/fastify-vue/virtual-ts/mount.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Head from 'unihead/client'
21
import { hydrateRoutes } from '@fastify/vue/client'
32
import routes from '$app/routes.ts'
43
import create from '$app/create.ts'
@@ -7,13 +6,11 @@ import * as root from '$app/root.vue'
76

87
async function mountApp (...targets) {
98
const ctxHydration = await extendContext(window.route, context)
10-
const head = new Head(window.route.head, window.document)
119
const resolvedRoutes = await hydrateRoutes(routes)
1210
const routeMap = Object.fromEntries(
1311
resolvedRoutes.map((route) => [route.path, route]),
1412
)
1513
const { instance, router } = await create({
16-
head,
1714
ctxHydration,
1815
routes: window.routes,
1916
routeMap,

packages/fastify-vue/virtual/create.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
routeLayout,
77
createBeforeEachHandler,
88
} from '@fastify/vue/client'
9+
import { createHead as createClientHead } from '@unhead/vue/client'
10+
import { createHead as createServerHead } from '@unhead/vue/server'
911

1012
import * as root from '$app/root.vue'
1113

@@ -28,6 +30,10 @@ export default async function create (ctx) {
2830
const isServer = import.meta.env.SSR
2931
instance.config.globalProperties.$isServer = isServer
3032

33+
const head = isServer ? createServerHead() : createClientHead()
34+
instance.use(head)
35+
ctxHydration.useHead = head
36+
3137
instance.provide(routeLayout, layoutRef)
3238
if (!isServer && ctxHydration.state) {
3339
ctxHydration.state = reactive(ctxHydration.state)
@@ -42,7 +48,7 @@ export default async function create (ctx) {
4248
instance.use(router)
4349

4450
if (typeof root.configure === 'function') {
45-
await root.configure({ app: instance, router })
51+
await root.configure({ app: instance, router, head })
4652
}
4753

4854
return { instance, ctx, state: ctxHydration.state, router }

packages/fastify-vue/virtual/mount.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Head from 'unihead/client'
1+
22
import { hydrateRoutes } from '@fastify/vue/client'
33
import routes from '$app/routes.js'
44
import create from '$app/create.js'
@@ -7,17 +7,18 @@ import * as root from '$app/root.vue'
77

88
async function mountApp (...targets) {
99
const ctxHydration = await extendContext(window.route, context)
10-
const head = new Head(window.route.head, window.document)
1110
const resolvedRoutes = await hydrateRoutes(routes)
1211
const routeMap = Object.fromEntries(
1312
resolvedRoutes.map((route) => [route.path, route]),
1413
)
1514
const { instance, router } = await create({
16-
head,
1715
ctxHydration,
1816
routes: window.routes,
1917
routeMap,
2018
})
19+
20+
ctxHydration.useHead.push(window.route.head)
21+
2122
await router.isReady()
2223
let mountTargetFound = false
2324
for (const target of targets) {

0 commit comments

Comments
 (0)