Index of every page on this site: llms.txt
TypeScript
Releases

Release: Unhead v3

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.

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

๐Ÿ› ๏ธ 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.

vite.config.ts
import { Unhead } from 'unhead/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, #733, #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.

useHead(unheadInstance, {
  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, #665, #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:

import { ValidatePlugin } from 'unhead/plugins'

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

See PRs #690, #691, #716, #722, #725, #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.

import { CanonicalPlugin } from 'unhead/plugins'

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

See PRs #492, #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.

import { MinifyPlugin } from 'unhead/plugins'

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

See PR #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)
  • Pure, tree-shakeable core with no side effects (#632)
  • Minified internal DOM state properties (#635)
  • Migrated unplugins from estree-walker/acorn-loose to oxc-walker (#663)
  • Walker-based transformHtmlTemplate (#581)
  • TemplateParamsPlugin and AliasSortingPlugin made opt-in for smaller bundles (#493, #494)

๐Ÿ“Š Schema.org

  • 12 new nodes: Dataset, MusicAlbum, MusicGroup, MusicPlaylist, MusicRecording, PodcastEpisode, PodcastSeason, PodcastSeries, Service, TVEpisode, TVSeason, TVSeries (#612)
  • Graph resolution rewrite for correctness and performance (#616)
  • Removed ohash and defu dependencies (#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, #622, #628, #629, #630)
  • @unhead/react/helmet compatibility export for users migrating from react-helmet (#719)
  • useHeadSafe() now whitelists CSS styles (#491)
  • Support for blocking attribute on scripts and stylesheets (#489)
  • useScript() consolidated back into core, legacy support dropped (#498)
  • fediverse:creator meta tag support (#703)
  • Switched from hookable to the lighter HookableCore; rendering hooks now run in the synchronous render pipeline (#631)
  • Deprecation warnings added to aliased packages (@unhead/schema, @unhead/shared) (#678)
  • templateParams extensible via module augmentation (#679)
  • Respect user-provided twitter:card in InferSeoMetaPlugin (#681)
  • Enforce as attribute for preload links (#683)
  • onRendered callback option on useHead() for synchronizing with DOM head updates (#712)
  • tagWeight option on createHead() to override default CAPO tag weight function (#716)

๐Ÿ› Bug Fixes

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

โš ๏ธ Breaking Changes

For the full migration guide, see Migrate to v3.

Summary

OldNew
childreninnerHTML
hid / vmidkey
body: truetagPosition: 'bodyClose'
useServerHead / useServerSeoMetauseHead / useSeoMeta
createHeadCorecreateHead from the client or server entry point
@unhead/dynamic-import/legacy@unhead/dynamic-import/client or @unhead/dynamic-import/server
mode option on entriesUse client/server createHead imports
  • 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)
Did this page help you?