---
title: "Release: Unhead v3"
description: "Unhead v3 release notes: streaming SSR, a unified Vite plugin with DevTools, and rendering and bundle optimizations."
canonical_url: "https://unhead.unjs.io/docs/releases/v3"
last_updated: "2026-07-21T04:58:32.974Z"
---

Streaming SSR drove the Unhead v3 rendering rewrite. Frameworks such as Nuxt, SolidStart, and SvelteKit can stream HTML as data resolves, while earlier Unhead versions resolved head tags once per response. The v3 renderer is synchronous and supports head updates during a stream.

## 📣 Highlights

### 🌊 Streaming SSR

Head tags can update as suspense boundaries resolve. When a boundary registers another entry, Unhead serializes it to a client-side queue instead of waiting for the full response.

```ts [Vue server]
// entry-server.ts
import { createStreamableHead } from '@unhead/vue/stream/server'

const { head, wrapStream } = createStreamableHead()
app.use(head)

// wraps the Vue stream, injecting head updates as chunks resolve
return wrapStream(renderToWebStream(app), template)
```

```ts [Vue client]
// entry-client.ts
import { createStreamableHead } from '@unhead/vue/stream/client'

const head = createStreamableHead()
if (!head)
  throw new Error('Unhead streaming bootstrap state is missing')
app.use(head)
```

Before the main JavaScript bundle loads, a queue stub (`window.__unhead__`) collects incoming head entries. The client head instance drains that queue during initialization, then handles subsequent updates.

Streaming is supported for Vue, React, Solid.js, Svelte, and vanilla TypeScript. See [PR #537](https://github.com/unjs/unhead/pull/537).

### 🛠️ Unified Vite Plugin + DevTools

A single `@unhead/{framework}/vite` plugin replaces the old manual composition of `@unhead/addons`, a streaming plugin, and framework glue. By default, it enables tree-shaking, the `useSeoMeta` → `useHead` transform, development validation, and Vite DevTools integration. The panel appears when Vite DevTools is enabled. Inline minification and streaming SSR are available through options.

```ts [vite.config.ts]
import { Unhead } from '@unhead/dynamic-import/vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue(), Unhead({ streaming: true })],
})
```

The DevTools panel surfaces live head state during development: every `useHead()` / `useSeoMeta()` call with its source file and line number, resolved tags, SEO overview (title, description, canonical, Open Graph), `useScript()` load status, active plugins, template params, and warnings from the Validate plugin. Source tracing lets you click through from any tag back to the exact line that created it.

Available for Vue, React, Svelte, Solid, and vanilla via `@unhead/bundler/vite` (the renamed `@unhead/addons` package; the old name still works with a deprecation warning).

See PRs [#726](https://github.com/unjs/unhead/pull/726), [#733](https://github.com/unjs/unhead/pull/733), [#731](https://github.com/unjs/unhead/pull/731).

### 🎯 `useHead()` Type Narrowing

`useHead()` now narrows types based on input. Link, script, and meta tags resolve to specific subtypes instead of a generic union, so you get precise autocomplete and type errors when something is wrong.

```ts
useHead({
  link: [
    // Narrows to StylesheetLink: requires href, offers media, integrity, etc.
    { rel: 'stylesheet', href: '/styles.css' },
    // Narrows to PreloadLink: requires as attribute
    { rel: 'preload', as: 'font', href: '/font.woff2', crossorigin: 'anonymous' },
  ],
  script: [
    // Narrows to ModuleScript
    { src: '/app.mjs', type: 'module' },
    // Narrows to JsonLdScript
    { type: 'application/ld+json', innerHTML: '{}' },
  ],
})
```

See [PR #627](https://github.com/unjs/unhead/pull/627), [#665](https://github.com/unjs/unhead/pull/665), [#744](https://github.com/unjs/unhead/pull/744).

### ✅ ValidatePlugin

The optional `ValidatePlugin` inspects resolved head output. Its rules cover missing titles, duplicate resource hints, conflicting preload priorities, render-blocking scripts, late `<meta charset>` tags, excessive `fetchpriority="high"` hints, and preconnects without `crossorigin`. Migration rules detect deprecated properties (`children`, `hid`/`vmid`, and `body: true`) and missing `TemplateParamsPlugin` or `AliasSortingPlugin` registrations. The unified Vite plugin registers validation during development, and rules use ESLint-style flat config:

```ts
import { ValidatePlugin } from '@unhead/dynamic-import/plugins'

createHead({
  plugins: [
    ValidatePlugin({
      rules: {
        'missing-description': 'off',
      }
    })
  ]
})
```

See PRs [#690](https://github.com/unjs/unhead/pull/690), [#691](https://github.com/unjs/unhead/pull/691), [#716](https://github.com/unjs/unhead/pull/716), [#722](https://github.com/unjs/unhead/pull/722), [#725](https://github.com/unjs/unhead/pull/725), [#732](https://github.com/unjs/unhead/pull/732).

### 🔗 Canonical Plugin

The new built-in `CanonicalPlugin` resolves relative URLs in existing canonical links and supported URL meta tags, including `og:image`, `twitter:image`, and `og:url`. For canonical links and `og:url`, it can filter query parameters, normalize trailing slashes, and strip hash fragments. It does not create a canonical link when one is missing.

```ts
import { CanonicalPlugin } from '@unhead/dynamic-import/plugins'

createHead({
  plugins: [
    CanonicalPlugin({
      canonicalHost: 'https://mysite.com',
      trailingSlash: true,
      queryWhitelist: ['page', 'sort'],
    })
  ]
})
```

See PRs [#492](https://github.com/unjs/unhead/pull/492), [#713](https://github.com/unjs/unhead/pull/713).

### 🗜️ MinifyPlugin

The optional `MinifyPlugin` minifies inline `<script>` and `<style>` content during SSR using JavaScript-only minifiers with no native dependencies. `MinifyTransform` in `@unhead/bundler` handles static `innerHTML` literals at build time. The underlying `minifyJS`, `minifyCSS`, and `minifyJSON` utilities are exported from `unhead/minify`.

```ts
import { MinifyPlugin } from '@unhead/dynamic-import/plugins'

createHead({
  plugins: [MinifyPlugin()]
})
```

See [PR #705](https://github.com/unjs/unhead/pull/705).

## 📦 Rendering and Bundle Optimizations

The release preparation recorded smaller bundles and faster render benchmarks, but the original comparison environment and baselines were not retained. The implementation changes are preserved in the linked pull requests:

- Full CAPO weighting moved to SSR, while the client uses lightweight priority aliases ([#626](https://github.com/unjs/unhead/pull/626))
- Pure, tree-shakeable core with no side effects ([#632](https://github.com/unjs/unhead/pull/632))
- Minified internal DOM state properties ([#635](https://github.com/unjs/unhead/pull/635))
- Migrated unplugins from `estree-walker`/`acorn-loose` to `oxc-walker` ([#663](https://github.com/unjs/unhead/pull/663))
- Walker-based `transformHtmlTemplate` ([#581](https://github.com/unjs/unhead/pull/581))
- `TemplateParamsPlugin` and `AliasSortingPlugin` made opt-in for smaller bundles ([#493](https://github.com/unjs/unhead/pull/493), [#494](https://github.com/unjs/unhead/pull/494))

## 📊 Schema.org

- **12 new nodes**: `Dataset`, `MusicAlbum`, `MusicGroup`, `MusicPlaylist`, `MusicRecording`, `PodcastEpisode`, `PodcastSeason`, `PodcastSeries`, `Service`, `TVEpisode`, `TVSeason`, `TVSeries` ([#612](https://github.com/unjs/unhead/pull/612))
- **Graph resolution rewrite** for correctness and performance ([#616](https://github.com/unjs/unhead/pull/616))
- **Removed ohash and defu dependencies** ([#605](https://github.com/unjs/unhead/pull/605))

## 🔄 Other Changes

- `renderDOMHead()` / `renderSSRHead()` are now fully synchronous, single-pass via a composable `resolveTags()` pipeline; the head instance exposes a pluggable `render()` function for framework integrations ([#619](https://github.com/unjs/unhead/pull/619), [#622](https://github.com/unjs/unhead/pull/622), [#628](https://github.com/unjs/unhead/pull/628), [#629](https://github.com/unjs/unhead/pull/629), [#630](https://github.com/unjs/unhead/pull/630))
- `@unhead/react/helmet` compatibility export for users migrating from `react-helmet` ([#719](https://github.com/unjs/unhead/pull/719))
- `useHeadSafe()` now whitelists CSS styles ([#491](https://github.com/unjs/unhead/pull/491))
- Support for `blocking` attribute on scripts and stylesheets ([#489](https://github.com/unjs/unhead/pull/489))
- `useScript()` consolidated back into core, legacy support dropped ([#498](https://github.com/unjs/unhead/pull/498))
- `fediverse:creator` meta tag support ([#703](https://github.com/unjs/unhead/pull/703))
- Switched from `hookable` to the lighter `HookableCore`; rendering hooks now run in the synchronous render pipeline ([#631](https://github.com/unjs/unhead/pull/631))
- Deprecation warnings added to aliased packages (`@unhead/schema`, `@unhead/shared`) ([#678](https://github.com/unjs/unhead/pull/678))
- `templateParams` extensible via module augmentation ([#679](https://github.com/unjs/unhead/pull/679))
- Respect user-provided `twitter:card` in `InferSeoMetaPlugin` ([#681](https://github.com/unjs/unhead/pull/681))
- Enforce `as` attribute for preload links ([#683](https://github.com/unjs/unhead/pull/683))
- `onRendered` callback option on `useHead()` for synchronizing with DOM head updates ([#712](https://github.com/unjs/unhead/pull/712))
- `tagWeight` option on `createHead()` to override default CAPO tag weight function ([#716](https://github.com/unjs/unhead/pull/716))

## 🐛 Bug Fixes

- Hydration race condition with deferred patches ([#634](https://github.com/unjs/unhead/pull/634))
- Process pending patches even when dirty is false ([#636](https://github.com/unjs/unhead/pull/636))
- Deduplicate matching tags inside same render cycle ([#668](https://github.com/unjs/unhead/pull/668))
- Dedupe `<link rel="alternate">` correctly ([#655](https://github.com/unjs/unhead/pull/655), [#656](https://github.com/unjs/unhead/pull/656), [#658](https://github.com/unjs/unhead/pull/658))
- React: dispose head entries on unmount in StrictMode ([#664](https://github.com/unjs/unhead/pull/664))
- React: force invalidation on entry disposal ([#559](https://github.com/unjs/unhead/pull/559))
- Vue: support computed getter trigger ([#638](https://github.com/unjs/unhead/pull/638))
- Vue: expose `@unhead/dynamic-import/stream/iife` with correct types ([#707](https://github.com/unjs/unhead/pull/707))
- Scripts: prevent scope disposal from aborting unrelated trigger ([#660](https://github.com/unjs/unhead/pull/660))
- Schema.org: allow `null` to opt out of default values ([#680](https://github.com/unjs/unhead/pull/680))
- Schema.org: normalize `target` to array before merging `potentialAction` ([#709](https://github.com/unjs/unhead/pull/709))
- Avoid mutating cached `titleTemplate` tag in `resolveTitleTemplate` ([#715](https://github.com/unjs/unhead/pull/715))

## ⚠️ Breaking Changes

For the full migration guide, see [Migrate to v3](/docs/migration-guide/v3).

### Summary

<table>
<thead>
  <tr>
    <th>
      Old
    </th>
    
    <th>
      New
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        children
      </code>
    </td>
    
    <td>
      <code>
        innerHTML
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hid
      </code>
      
       / <code>
        vmid
      </code>
    </td>
    
    <td>
      <code>
        key
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        body: true
      </code>
    </td>
    
    <td>
      <code>
        tagPosition: 'bodyClose'
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useServerHead
      </code>
      
       / <code>
        useServerSeoMeta
      </code>
    </td>
    
    <td>
      <code>
        useHead
      </code>
      
       / <code>
        useSeoMeta
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createHeadCore
      </code>
    </td>
    
    <td>
      <code>
        createHead
      </code>
      
       from the client or server entry point
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @unhead/dynamic-import/legacy
      </code>
    </td>
    
    <td>
      <code>
        @unhead/dynamic-import/client
      </code>
      
       or <code>
        @unhead/dynamic-import/server
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        mode
      </code>
      
       option on entries
    </td>
    
    <td>
      Use client/server <code>
        createHead
      </code>
      
       imports
    </td>
  </tr>
</tbody>
</table>

- `renderDOMHead()` and `renderSSRHead()` are now synchronous (remove `await`)
- CJS removed, all packages are ESM-only
- `TemplateParamsPlugin` and `AliasSortingPlugin` are no longer included by default
- The `init` hook was removed; `dom:renderTag` is deprecated and no longer called; `dom:rendered` is deprecated but still emitted; `dom:beforeRender` is now synchronous
- `@unhead/addons` renamed to `@unhead/bundler`; framework Vite plugins now use a named `Unhead` export instead of a default export
- `Link` / `Script` unions are strict: use the new `defineLink` / `defineScript` helpers for custom `rel` / `type` values, and meta `content` is now required (use `content: null` to remove)
