Introduction

While Unhead has always been framework agnostic, the majority of adoption was by the Vue ecosystem.

With the release of Unhead v2, we now have first-class support for other frameworks. However, this guide will focus on the changes that affect Vue and TypeScript users.

The high-level of Unhead v2 was to remove deprecations and remove the implicit context implementation.

Legacy Support

Unhead v2 is mostly fully backwards compatible with Unhead v1.

While not recommended, if upgrading is not possible for you, you can change your imports to the following:

-import { createServerHead, useHead } from 'unhead'
+import { createServerHead, useHead } from 'unhead/legacy'

This will be removed in a future minor version, so you should lock your dependencies to the version that works for you.

Client / Server Subpath Exports

๐Ÿšฆ Impact Level: Critical

Nuxt should not be effected by this change.

โš ๏ธ Breaking Changes:

  • createServerHead() and createHead() exports from unhead are removed

The path where you import createHead from has been updated to be a subpath export.

Please follow the updated installation instructions or simply update the import to use the subpath.

Client bundle:

-import { createServerHead } from 'unhead'
+import { createHead } from 'unhead/client'

// avoids bundling server plugins
createHead()

Server bundle:

-import { createServerHead } from 'unhead'
+import { createHead } from 'unhead/server'

// avoids bundling server plugins
-createServerHead()
+createHead()

Removed Implicit Context

๐Ÿšฆ Impact Level: Critical

Nuxt should not be effected by this change.

โš ๏ธ Breaking Changes:

  • getActiveHead(), activeHead exports are removed
  • Vue Only: setHeadInjectionHandler() is removed
  • Vue Only: Error may be thrown when using useHead() after async operations

The implicit context implementation kept a global instance of Unhead available so that you could use the useHead() composables anywhere in your application.

useHead({
  title: 'This just worked!'
})

While safe client side, this was a leaky abstraction server side and led to memory leaks in some cases.

In v2, the core composables no longer have access to the Unhead instance. Instead, you must pass the Unhead instance to the composables.

Passing the instance is only relevant if you're importing from unhead. In JavaScript frameworks we tie the context to the framework itself so you don't need to worry about this.
import { useHead } from 'unhead'

// example of getting the instance
const unheadInstance = useMyApp().unhead
useHead(unheadInstance, {
  title: 'Looks good'
})

For frameworks users, you may run into issues with the context being lost.

<script setup lang="ts">
// In Vue this happens in lifecycle hooks where we have async operations.
onMounted(async () => {
  await fetchSomeData()
  useHead({
    title: 'This will not work'
  })
})
</script>

If you're getting errors on your useHead() about context, check the new documentation:

Removed vmid, hid, children, body

๐Ÿšฆ Impact Level: High

For legacy support with Vue Meta we allowed end users to provide deprecated properties: vmid, hid, children and body.

You must either update these properties to the appropriate replacement, remove them, or you can use the DeprecationsPlugin.

Meta tags with vmid, hid

These are already deduped magically so you can safely remove them there.

useHead({
  meta: [{
    name: 'description',
-   vmid: 'description'
-   hid: 'description'
  }]
})

Other Tags with vmid, hid

Use key if you need the deduplication feature. This is useful for tags that may change from server to client rendering.

useHead({
  script: [{
-   vmid: 'my-key'
-   hid: 'my-key'
+   key: 'my-key',
  }]
})

Using children

The children key is a direct replacement of innerHTML which you should use instead.

When migrating your code ensure that you're not dynamically setting innerHTML as this can lead to XSS vulnerabilities.
useHead({
  script: [
      {
-        children: '..'
+        innerHTML: '..'
      }
   ]
})

Using body

The body key should be updated to use the Tag Position feature.

useHead({
  script: [
      {
-        body: true
+        tagPosition: 'bodyClose'
      }
   ]
})

Use Deprecations Plugin

import { createHead } from 'unhead'
import { DeprecationsPlugin } from 'unhead/optionalPlugins'

const unhead = createHead({
  plugins: [DeprecationsPlugin]
})

Opt-in Template Params & Tag Alias Sorting

๐Ÿšฆ Impact Level: High

To reduce the bundle size and improve performance, we've moved the template params and tag alias sorting to optional plugins.

If you'd like to continue using these, please opt-in to the plugins.

import { AliasSortingPlugin, TemplateParamsPlugin } from 'unhead/plugins'

createHead({
  plugins: [TemplateParamsPlugin, AliasSortingPlugin]
})

Vue 2 Support

๐Ÿšฆ Impact Level: Critical

Unhead v2 no longer supports Vue v2. If you're using Vue v2, you will need to lock your dependencies to the latest v1 version of Unhead.

Promise Input Support

๐Ÿšฆ Impact Level: Medium

If you have promises as input they will no longer be resolved, either await the promise before passing it along or register the optional promises plugin.

Option 1: Await Promise

useHead({
  link: [
    {
-     href: import('~/assets/MyFont.css?url'),
+     href: await import('~/assets/MyFont.css?url'),
      rel: 'stylesheet',
      type: 'text/css'
    }
  ]
})

Option 2: Promise Plugin

import { PromisePlugin } from 'unhead/optionalPlugins'

const unhead = createHead({
  plugins: [PromisePlugin]
})

Updated useScript()

๐Ÿšฆ Impact Level: High

โš ๏ธ Breaking Changes:

  • Script instance is no longer augmented as a proxy and promise
  • script.proxy is rewritten for simpler, more stable behavior
  • stub() and runtime hook script:instance-fn are removed

Replacing promise usage

If you're using the script as a promise you should instead opt to use the onLoaded() functions.

const script = useScript()

-script.then(() => console.log('loaded')
+script.onLoaded(() => console.log('loaded'))

Replacing proxy usage

If you're accessing the underlying API directly from the script instance, you will now need to only access it from the .proxy.

const script = useScript('..', {
  use() { return { foo: [] } }
})

-script.foo.push('bar')
+script.proxy.foo.push('bar')

Replacing stub()

If you were using stub for anything you should replace this with either custom use() behavior.

const script = useScript('...', {
-  stub() { return { foo: import.meta.server ? [] : undefined } }
})

+script.proxy = {} // your own implementation

Tag Sorting Updated

๐Ÿšฆ Impact Level: Low

An optional Capo.js plugin was added to Unhead, in v2 we make this the default sorting behavior.

As all head tags may be re-ordered this will break any snapshot tests that you have in place and in some rare cases may lead to performance regressions.

You can opt-out of Capo.js sorting by providing the option.

createHead({
  disableCapoSorting: true,
})

Default SSR Tags

๐Ÿšฆ Impact Level: Low

When SSR Unhead will now insert important default tags for you:

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1">
  • <html lang="en">

If you were previously relying on these being left empty, you may need to either disable them by using disableDefaultTags or insert tags to override them.

import { createHead } from '@unhead/vue/server'

// disable when creating the head instance
createHead({
  disableDefaults: true,
})
import { useHead } from 'unhead'

// override the defaults
useHead({
  htmlAttrs: {
    lang: 'fr'
  }
})

CJS Exports Removed

๐Ÿšฆ Impact Level: Low

CommonJS exports have been removed in favor of ESM only.

-const { createHead } = require('unhead/client')
+import { createHead } from 'unhead/client'

Deprecated @unhead/schema

๐Ÿšฆ Impact Level: Low

The @unhead/schema package is now deprecated and will be removed in a future version. You should instead import the schema from unhead or @unhead/vue.

-import { HeadTag } from '@unhead/schema'
+import { HeadTag } from 'unhead/types'
Did this page help you?