Skip to content

Commit 85c19c8

Browse files
authoredFeb 14, 2025··
perf(core)!: make TemplateParamsPlugin opt-in (#493)
* perf(core): make `TemplateParamsPlugin` opt-in * chore: fix tests
1 parent b1fd5ce commit 85c19c8

12 files changed

+335
-267
lines changed
 

Diff for: ‎bench/ssr-harlanzw-com-e2e.test.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Head } from '@unhead/schema'
22
import { InferSeoMetaPlugin } from '@unhead/addons'
33
import { definePerson, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org/vue'
4+
import { TemplateParamsPlugin } from 'unhead/plugins'
45
import { describe, it } from 'vitest'
56
import { useHead, useSeoMeta, useServerHead } from '../packages/vue/src'
67
import { createHead as createServerHead, renderSSRHead } from '../packages/vue/src/server'
@@ -10,7 +11,11 @@ describe('ssr e2e bench', () => {
1011
// we're going to replicate the logic needed to render the tags for a harlanzw.com page
1112

1213
// 1. Add nuxt.config meta tags
13-
const head = createServerHead()
14+
const head = createServerHead({
15+
plugins: [
16+
TemplateParamsPlugin,
17+
],
18+
})
1419
// nuxt.config app.head
1520
head.push({
1621
title: 'Harlan Wilton',

Diff for: ‎docs/1.guides/0.titles.md

+12-47
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Creating your own title like this is simple using `useHead()`{lang="ts"} with a
9090
```ts twoslash [input.vue]
9191
useHead({
9292
title: 'Home',
93-
titleTemplate: '%s %separator MySite'
93+
titleTemplate: '%s | MySite'
9494
})
9595
```
9696

@@ -106,61 +106,26 @@ useHead({
106106

107107
### Template Params
108108

109-
You may ask why we don't just use a function for the title template, and while this is supported, it can create issues with SSR and hydration.
109+
Template params are an opt-in plugin make your tags more dynamic. You get `%s` and `%separator` built-in, and can add your own:
110110

111-
Instead, it's recommended to use the params. Out-of-the-box, Unhead provides:
112-
113-
| Token | Description |
114-
|--------------|-------------------------------------------------|
115-
| `%s` | The current page title. |
116-
| `%separator` | The separator, defaults to a pipe character \|. |
117-
118-
The `%separator` token is smart - it only appears between content and automatically removes itself when the title is empty or when multiple separators would appear.
119-
120-
Define custom template params to maintain consistent formatting:
121-
122-
::code-group
123-
124-
```ts twoslash [input.vue]
111+
::code-block
112+
```ts [Input]
125113
useHead({
126114
title: 'Home',
127115
titleTemplate: '%s %separator %siteName',
128116
templateParams: {
129-
seperator: '',
130-
siteName: 'MySite'
117+
separator: '·',
118+
siteName: 'My Site'
131119
}
132120
})
133121
```
134122

135-
```html [output.html]
136-
<head>
137-
<title>Home — MySite</title>
138-
</head>
123+
```html [Output]
124+
<title>Home · My Site</title>
139125
```
140-
141126
::
142127

143-
I'd suggest choosing your own separator as the `'|'` is a bit ugly in my opinion, you can try:
144-
145-
```ts
146-
type Seperator = '-' | '' | '' | '·' | '❤️'
147-
```
148-
149-
You can use template params in other head tags too, such as meta descriptions and open graph tags.
150-
151-
```ts
152-
useHead({
153-
templateParams: {
154-
siteName: 'MyApp'
155-
},
156-
title: 'Home',
157-
meta: [
158-
{ name: 'description', content: 'Welcome to %siteName - where we make awesome happen' },
159-
{ property: 'og:title', content: 'Home | %siteName' },
160-
{ property: 'og:description', content: 'Check out %siteName today!' }
161-
]
162-
})
163-
```
128+
Check out the [Template Params](/usage/guides/template-params) guide to get started.
164129

165130
### Resetting the Title Template
166131

@@ -201,12 +166,12 @@ Remembering how to use the meta tags can be annoying, so we can use the [`useSeo
201166

202167
```ts [input.vue]
203168
useSeoMeta({
204-
titleTemplate: '%s %separator Health Tips',
169+
titleTemplate: '%s | Health Tips',
205170
title: 'Why you should eat more broccoli',
206171
// og title is not effected by titleTemplate, we can use template params here if we need
207-
ogTitle: 'Hey! Health Tips %separator 10 reasons to eat more broccoli.',
172+
ogTitle: 'Hey! Health Tips - 10 reasons to eat more broccoli.',
208173
// explicit twitter title is only needed when we want to display something just for X
209-
twitterTitle: 'Hey X! Health Tips %separator 10 reasons to eat more broccoli.',
174+
twitterTitle: 'Hey X! Health Tips - 10 reasons to eat more broccoli.',
210175
})
211176
```
212177

Diff for: ‎docs/1.guides/6.template-params.md

-119
This file was deleted.
File renamed without changes.
File renamed without changes.

Diff for: ‎docs/plugins/6.template-params.md

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
title: "Template Params"
3+
description: "Use template params to simplify your meta tags"
4+
---
5+
6+
# Template Params
7+
8+
Template params let you use variables in your meta tags. While you could use functions, template params work better with [SSR](/setup/ssr/how-it-works) and avoid hydration issues.
9+
10+
## Setup
11+
12+
First, add the plugin to your Unhead configuration:
13+
14+
::code-block
15+
```ts [Input]
16+
import { TemplateParamsPlugin } from '@unhead/plugins'
17+
import { createHead } from 'unhead'
18+
19+
const head = createHead({
20+
plugins: [
21+
TemplateParamsPlugin()
22+
]
23+
})
24+
```
25+
::
26+
27+
## Built-in Params
28+
29+
Unhead includes two built-in template params:
30+
31+
| Token | Description |
32+
|--------------|-------------------------------------------------|
33+
| `%s` | The current page title |
34+
| `%separator` | Smart separator (defaults to \|) |
35+
36+
The `%separator` is clever - it only appears between content and removes itself when:
37+
- The title is empty
38+
- Multiple separators would appear next to each other
39+
40+
::code-block
41+
```ts [Input]
42+
useHead({
43+
title: 'Home',
44+
titleTemplate: '%s %separator %siteName',
45+
templateParams: {
46+
separator: '', // Use an em dash instead of |
47+
siteName: 'MySite'
48+
}
49+
})
50+
```
51+
52+
```html [Output]
53+
<title>Home — MySite</title>
54+
```
55+
::
56+
57+
## Choosing a Separator
58+
59+
The default `|` separator isn't great for readability. Try these instead:
60+
61+
```ts
62+
type Separator = '-' | '' | '' | '·' | '❤️'
63+
```
64+
65+
## Meta Tags and Social Sharing
66+
67+
Template params work great with [SEO meta tags](/usage/composables/use-seo-meta) and social sharing:
68+
69+
::code-block
70+
```ts [Input]
71+
useHead({
72+
templateParams: {
73+
siteName: 'MyApp',
74+
separator: '·'
75+
},
76+
title: 'Home',
77+
meta: [
78+
{ name: 'description', content: 'Welcome to %siteName - where we make awesome happen' },
79+
{ property: 'og:title', content: 'Home %separator %siteName' },
80+
{ property: 'og:description', content: 'Check out %siteName today!' }
81+
]
82+
})
83+
```
84+
85+
```html [Output]
86+
<head>
87+
<title>Home · MyApp</title>
88+
<meta name="description" content="Welcome to MyApp - where we make awesome happen">
89+
<meta property="og:title" content="Home · MyApp">
90+
<meta property="og:description" content="Check out MyApp today!">
91+
</head>
92+
```
93+
::
94+
95+
## Enable for Other Tags
96+
97+
For tags using `innerHTML` or `textContent`, add `processTemplateParams: true`:
98+
99+
::code-block
100+
```ts [Input]
101+
useHead({
102+
templateParams: { name: 'My App' },
103+
script: [
104+
{
105+
innerHTML: { name: '%name' },
106+
type: 'application/json',
107+
processTemplateParams: true
108+
}
109+
]
110+
})
111+
```
112+
113+
```html [Output]
114+
<script type="application/json">{ "name": "My App" }</script>
115+
```
116+
::
117+
118+
## Disable for Specific Tags
119+
120+
Add `processTemplateParams: false` to skip template processing:
121+
122+
::code-block
123+
```ts [Input]
124+
useHead({
125+
title: 'Hello %name',
126+
templateParams: { name: 'World' },
127+
}, {
128+
processTemplateParams: false,
129+
})
130+
```
131+
132+
```html [Output]
133+
<title>Hello %name</title>
134+
```
135+
::
136+
137+
## Complex Example
138+
139+
Here's how you might use template params with nested objects and multiple tags:
140+
141+
::code-block
142+
```ts [Input]
143+
useHead({
144+
templateParams: {
145+
site: {
146+
name: 'My Site',
147+
url: 'https://example.com',
148+
},
149+
separator: '·',
150+
subPage: null
151+
},
152+
title: 'My Page',
153+
titleTemplate: '%s %separator %subPage %separator %site.name',
154+
meta: [
155+
{
156+
name: 'description',
157+
content: 'Welcome to %site.name.',
158+
},
159+
{
160+
property: 'og:site_name',
161+
content: '%site.name',
162+
},
163+
{
164+
property: 'og:url',
165+
content: '%site.url/my-page',
166+
},
167+
],
168+
})
169+
```
170+
171+
```html [Output]
172+
<head>
173+
<title>My Page · My Site</title>
174+
<meta name="description" content="Welcome to My Site.">
175+
<meta property="og:site_name" content="My Site">
176+
<meta property="og:url" content="https://example.com/my-page">
177+
</head>
178+
```
179+
::
File renamed without changes.

Diff for: ‎packages/schema-org/src/plugin.ts

+101-96
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SchemaOrgGraph } from './core/graph'
22
import type { MetaInput, ResolvedMeta } from './types'
33
import { defu } from 'defu'
4+
import { TemplateParamsPlugin } from 'unhead/plugins'
45
import { defineHeadPlugin, processTemplateParams } from 'unhead/utils'
56
import {
67
createSchemaOrgGraph,
@@ -31,112 +32,116 @@ export function SchemaOrgUnheadPlugin(config: MetaInput, meta: () => Partial<Met
3132
config = resolveMeta({ ...config })
3233
let graph: SchemaOrgGraph
3334
let resolvedMeta = {} as ResolvedMeta
34-
return defineHeadPlugin(head => ({
35-
key: 'schema-org',
36-
hooks: {
37-
'entries:resolve': () => {
38-
graph = createSchemaOrgGraph()
39-
},
40-
'entries:normalize': async ({ tags }) => {
41-
for (const tag of tags) {
42-
if (tag.tag === 'script' && tag.props.type === 'application/ld+json' && tag.props.nodes) {
43-
// this is a bit expensive, load in seperate chunk
44-
const { loadResolver } = await import('./resolver')
45-
const nodes = await tag.props.nodes
46-
for (const node of Array.isArray(nodes) ? nodes : [nodes]) {
47-
// malformed input
48-
if (typeof node !== 'object' || Object.keys(node).length === 0) {
49-
continue
50-
}
51-
const newNode = {
52-
...node,
53-
_dedupeStrategy: tag.tagDuplicateStrategy,
54-
_resolver: loadResolver(await node._resolver),
35+
return defineHeadPlugin((head) => {
36+
head.use(TemplateParamsPlugin)
37+
return {
38+
key: 'schema-org',
39+
hooks: {
40+
'entries:resolve': () => {
41+
graph = createSchemaOrgGraph()
42+
},
43+
'entries:normalize': async ({ tags }) => {
44+
for (const tag of tags) {
45+
if (tag.tag === 'script' && tag.props.type === 'application/ld+json' && tag.props.nodes) {
46+
// this is a bit expensive, load in seperate chunk
47+
const { loadResolver } = await import('./resolver')
48+
const nodes = await tag.props.nodes
49+
for (const node of Array.isArray(nodes) ? nodes : [nodes]) {
50+
// malformed input
51+
if (typeof node !== 'object' || Object.keys(node).length === 0) {
52+
continue
53+
}
54+
const newNode = {
55+
...node,
56+
_dedupeStrategy: tag.tagDuplicateStrategy,
57+
_resolver: loadResolver(await node._resolver),
58+
}
59+
graph.push(newNode)
5560
}
56-
graph.push(newNode)
61+
tag.tagPosition = tag.tagPosition || config.tagPosition === 'head' ? 'head' : 'bodyClose'
5762
}
58-
tag.tagPosition = tag.tagPosition || config.tagPosition === 'head' ? 'head' : 'bodyClose'
59-
}
60-
if (tag.tag === 'htmlAttrs' && tag.props.lang) {
61-
resolvedMeta.inLanguage = tag.props.lang
62-
}
63-
else if (tag.tag === 'title') {
64-
resolvedMeta.title = tag.textContent
65-
}
66-
else if (tag.tag === 'meta' && tag.props.name === 'description') {
67-
resolvedMeta.description = tag.props.content
68-
}
69-
else if (tag.tag === 'link' && tag.props.rel === 'canonical') {
70-
resolvedMeta.url = tag.props.href
71-
// may be using template params that aren't resolved
72-
if (resolvedMeta.url && !resolvedMeta.host) {
73-
try {
74-
resolvedMeta.host = new URL(resolvedMeta.url).origin
63+
if (tag.tag === 'htmlAttrs' && tag.props.lang) {
64+
resolvedMeta.inLanguage = tag.props.lang
65+
}
66+
else if (tag.tag === 'title') {
67+
resolvedMeta.title = tag.textContent
68+
}
69+
else if (tag.tag === 'meta' && tag.props.name === 'description') {
70+
resolvedMeta.description = tag.props.content
71+
}
72+
else if (tag.tag === 'link' && tag.props.rel === 'canonical') {
73+
resolvedMeta.url = tag.props.href
74+
// may be using template params that aren't resolved
75+
if (resolvedMeta.url && !resolvedMeta.host) {
76+
try {
77+
resolvedMeta.host = new URL(resolvedMeta.url).origin
78+
}
79+
catch {
80+
}
7581
}
76-
catch {}
7782
}
78-
}
79-
else if (tag.tag === 'meta' && tag.props.property === 'og:image') {
80-
resolvedMeta.image = tag.props.content
81-
}
82-
// use template params
83-
else if (tag.tag === 'templateParams' && tag.props.schemaOrg) {
84-
resolvedMeta = {
85-
...resolvedMeta,
86-
// @ts-expect-error untyped
87-
...tag.props.schemaOrg,
83+
else if (tag.tag === 'meta' && tag.props.property === 'og:image') {
84+
resolvedMeta.image = tag.props.content
85+
}
86+
// use template params
87+
else if (tag.tag === 'templateParams' && tag.props.schemaOrg) {
88+
resolvedMeta = {
89+
...resolvedMeta,
90+
// @ts-expect-error untyped
91+
...tag.props.schemaOrg,
92+
}
93+
delete tag.props.schemaOrg
8894
}
89-
delete tag.props.schemaOrg
9095
}
91-
}
92-
},
93-
'tags:resolve': async (ctx) => {
94-
// find the schema.org node, should be a single instance
95-
for (const k in ctx.tags) {
96-
const tag = ctx.tags[k]
97-
if (tag.tag === 'script' && tag.props.type === 'application/ld+json' && tag.props.nodes) {
98-
delete tag.props.nodes
99-
const resolvedGraph = graph.resolveGraph({ ...(await meta?.() || {}), ...config, ...resolvedMeta })
100-
if (!resolvedGraph.length) {
101-
// removes the tag
102-
tag.props = {}
96+
},
97+
'tags:resolve': async (ctx) => {
98+
// find the schema.org node, should be a single instance
99+
for (const k in ctx.tags) {
100+
const tag = ctx.tags[k]
101+
if (tag.tag === 'script' && tag.props.type === 'application/ld+json' && tag.props.nodes) {
102+
delete tag.props.nodes
103+
const resolvedGraph = graph.resolveGraph({ ...(await meta?.() || {}), ...config, ...resolvedMeta })
104+
if (!resolvedGraph.length) {
105+
// removes the tag
106+
tag.props = {}
107+
return
108+
}
109+
// eslint-disable-next-line node/prefer-global/process
110+
const minify = options?.minify || process.env.NODE_ENV === 'production'
111+
tag.innerHTML = JSON.stringify({
112+
'@context': 'https://schema.org',
113+
'@graph': resolvedGraph,
114+
}, (_, value) => {
115+
// process template params here
116+
if (typeof value !== 'object')
117+
return processTemplateParams(value, head._templateParams!, head._separator!)
118+
return value
119+
}, minify ? 0 : 2)
103120
return
104121
}
105-
// eslint-disable-next-line node/prefer-global/process
106-
const minify = options?.minify || process.env.NODE_ENV === 'production'
107-
tag.innerHTML = JSON.stringify({
108-
'@context': 'https://schema.org',
109-
'@graph': resolvedGraph,
110-
}, (_, value) => {
111-
// process template params here
112-
if (typeof value !== 'object')
113-
return processTemplateParams(value, head._templateParams!, head._separator!)
114-
return value
115-
}, minify ? 0 : 2)
116-
return
117122
}
118-
}
119-
},
120-
'tags:afterResolve': (ctx) => {
121-
let firstNodeKey: number | undefined
122-
for (const k in ctx.tags) {
123-
const tag = ctx.tags[k]
124-
if ((tag.props.type === 'application/ld+json' && tag.props.nodes) || tag.key === 'schema-org-graph') {
125-
delete tag.props.nodes
126-
if (typeof firstNodeKey === 'undefined') {
127-
firstNodeKey = k as any
128-
continue
123+
},
124+
'tags:afterResolve': (ctx) => {
125+
let firstNodeKey: number | undefined
126+
for (const k in ctx.tags) {
127+
const tag = ctx.tags[k]
128+
if ((tag.props.type === 'application/ld+json' && tag.props.nodes) || tag.key === 'schema-org-graph') {
129+
delete tag.props.nodes
130+
if (typeof firstNodeKey === 'undefined') {
131+
firstNodeKey = k as any
132+
continue
133+
}
134+
// merge props on to first node and delete
135+
ctx.tags[firstNodeKey].props = defu(ctx.tags[firstNodeKey].props, tag.props)
136+
delete ctx.tags[firstNodeKey].props.nodes
137+
// @ts-expect-error untyped
138+
ctx.tags[k] = false
129139
}
130-
// merge props on to first node and delete
131-
ctx.tags[firstNodeKey].props = defu(ctx.tags[firstNodeKey].props, tag.props)
132-
delete ctx.tags[firstNodeKey].props.nodes
133-
// @ts-expect-error untyped
134-
ctx.tags[k] = false
135140
}
136-
}
137-
// there many be multiple script nodes within the same entry
138-
ctx.tags = ctx.tags.filter(Boolean)
141+
// there many be multiple script nodes within the same entry
142+
ctx.tags = ctx.tags.filter(Boolean)
143+
},
139144
},
140-
},
141-
}))
145+
}
146+
})
142147
}

Diff for: ‎packages/unhead/src/createHead.ts

-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
} from './types'
1212
import { createHooks } from 'hookable'
1313
import { AliasSortingPlugin } from './plugins/aliasSorting'
14-
import { TemplateParamsPlugin } from './plugins/templateParams'
1514
import { isMetaArrayDupeKey, sortTags, tagWeight, UsesMergeStrategy, ValidHeadTags } from './utils'
1615
import { dedupeKey } from './utils/dedupe'
1716
import { normalizeEntryToTags } from './utils/normalize'
@@ -190,7 +189,6 @@ export function createHeadCore<T extends Record<string, any> = Head>(resolvedOpt
190189
},
191190
}
192191
;[
193-
TemplateParamsPlugin,
194192
AliasSortingPlugin,
195193
...resolvedOptions?.plugins || [],
196194
].forEach(p => registerPlugin(head, p))

Diff for: ‎packages/vue/test/unit/dom/templateParams.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { renderDOMHead } from '@unhead/dom'
44
import { useHead } from '@unhead/vue'
5+
import { TemplateParamsPlugin } from 'unhead/plugins'
56
import { describe, it } from 'vitest'
67
import { ref } from 'vue'
78
import { useDom } from '../../../../unhead/test/fixtures'
@@ -27,6 +28,10 @@ describe('vue templateParams', () => {
2728
siteName: () => 'My Awesome Site',
2829
},
2930
})
31+
}, {
32+
plugins: [
33+
TemplateParamsPlugin,
34+
],
3035
})
3136

3237
await renderDOMHead(head, { document: dom.window.document })
@@ -58,6 +63,10 @@ describe('vue templateParams', () => {
5863
siteName: () => 'My Awesome Site',
5964
},
6065
})
66+
}, {
67+
plugins: [
68+
TemplateParamsPlugin,
69+
],
6170
})
6271

6372
await renderDOMHead(head, { document: dom.window.document })

Diff for: ‎packages/vue/test/unit/ssr/templateParams.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { renderSSRHead } from '@unhead/ssr'
2+
import { TemplateParamsPlugin } from 'unhead/plugins'
23
import { createHead } from 'unhead/server'
34
import { describe, it } from 'vitest'
45
import { ref } from 'vue'
@@ -21,6 +22,10 @@ describe('ssr vue templateParams', () => {
2122
separator,
2223
siteName: () => 'My Awesome Site',
2324
},
25+
}, {
26+
plugins: [
27+
TemplateParamsPlugin,
28+
],
2429
})
2530

2631
expect(headResult).toMatchInlineSnapshot(`
@@ -69,6 +74,10 @@ describe('ssr vue templateParams', () => {
6974
description: 'A Nuxt 3 playground',
7075
},
7176
},
77+
}, {
78+
plugins: [
79+
TemplateParamsPlugin,
80+
],
7281
})
7382

7483
expect(headResult).toMatchInlineSnapshot(`
@@ -91,6 +100,10 @@ describe('ssr vue templateParams', () => {
91100
titleTemplate: '%s %separator %siteName',
92101
templateParams: {
93102
},
103+
}, {
104+
plugins: [
105+
TemplateParamsPlugin,
106+
],
94107
})
95108

96109
expect(headResult).toMatchInlineSnapshot(`
@@ -111,6 +124,10 @@ describe('ssr vue templateParams', () => {
111124
templateParams: {
112125
separator: '/',
113126
},
127+
}, {
128+
plugins: [
129+
TemplateParamsPlugin,
130+
],
114131
})
115132

116133
expect(headResult).toMatchInlineSnapshot(`
@@ -132,6 +149,10 @@ describe('ssr vue templateParams', () => {
132149
separator: '/',
133150
siteName: 'My Awesome Site',
134151
},
152+
}, {
153+
plugins: [
154+
TemplateParamsPlugin,
155+
],
135156
})
136157

137158
expect(headResult).toMatchInlineSnapshot(`
@@ -148,6 +169,9 @@ describe('ssr vue templateParams', () => {
148169
it('edge case', async () => {
149170
const head = createHead({
150171
disableDefaults: true,
172+
plugins: [
173+
TemplateParamsPlugin,
174+
],
151175
})
152176
head.push({
153177
title: '%site.tagline',

Diff for: ‎packages/vue/test/util.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { createHead as createServerHead } from '@unhead/vue/server'
1010
import { renderToString } from '@vue/server-renderer'
1111
import { createApp, createSSRApp, h } from 'vue'
1212

13-
export function csrVueAppWithUnhead(dom: JSDOM, fn: () => void | Promise<void>) {
13+
export function csrVueAppWithUnhead(dom: JSDOM, fn: () => void | Promise<void>, options?: CreateHeadOptions) {
1414
const head = createClientHead({
1515
document: dom.window.document,
16+
...options,
1617
})
1718
const app = createApp({
1819
setup() {
@@ -63,9 +64,10 @@ export async function ssrRenderHeadToString(fn: () => void) {
6364
return renderSSRHead(head)
6465
}
6566

66-
export async function ssrRenderOptionsHead(input: any) {
67+
export async function ssrRenderOptionsHead(input: any, options?: CreateHeadOptions) {
6768
const head = createServerHead({
6869
disableDefaults: true,
70+
...options,
6971
})
7072
const app = createSSRApp({
7173
head() {

0 commit comments

Comments
 (0)
Please sign in to comment.