Index of every page on this site: llms.txt
All skills
unjs avatar

/unhead-vue

@348d075
by unjsunjs/unhead1.3k stars
77
Run it yourself

Manages head tags in Vue 3 SSR and SPA apps with @unhead/vue v3 through createHead, transformHtmlTemplate, useHead, useSeoMeta, useHeadSafe, useScript, and streaming SSR. Use when a task sets titles, meta, Open Graph, or scripts in a Vue app, upgrades from @vueuse/head or Unhead v1 or v2, or hits a useHead provide context error, template params printed literally in the title, or hid and vmid attributes in the HTML.

Use this Skill: https://skilld.dev/gh/unjs/unhead/unhead-vue

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

SKILL.md

≈108 tokens for metadata: the name and description. ≈2.4k when used: this file.

Description uses 5.4% of example budget

Before choosing a Skill, your Agent reads its name and description. All available Skills share that space.

  • A shorter description leaves more room for other Skills. This entry exceeds our 1% size suggestion.

In our Claude Code example, all Skill names and descriptions share 8,000 characters. This Skill uses ≈431 characters, or 5.4%.

The 1% threshold is a size suggestion. Longer descriptions can still fit.

Your model, settings, and other Skills decide how much text your Agent can read.

Example settings and source

The example uses a 200k-token context and default Claude Code settings. The count includes the name, description, separators, and when_to_use when present. Codex also counts local file paths.

Skit's source and limits: Codex 0.160.1, Claude Code 2.1.292.

@unhead/vue

Tested with @unhead/vue 3.4.2, unhead 3.4.2, Vue 3.5, Vite 8, and TypeScript 6.0.3 (vue-tsc 3.3.12) and 7.0.2. Import from @unhead/vue and its subpaths, which re-export unhead. In Nuxt, Nuxt creates and installs the head. Skip Setup and use the composables.

Setup

Create one head in the browser. On the server, create a new head for each request; a shared one leaks tags between requests.

// src/entry-client.ts
import { createHead } from '@unhead/vue/client'
import { createApp } from './main'

const { app } = createApp()
app.use(createHead())
app.mount('#app')
// src/entry-server.ts
import { createHead, transformHtmlTemplate } from '@unhead/vue/server'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render(template: string) {
  const { app } = createApp()
  const head = createHead()
  app.use(head)
  const html = await renderToString(app)
  return transformHtmlTemplate(head, template.replace('<!--app-html-->', html))
}
  • If the template has no <head>, transformHtmlTemplate silently drops every tag.
  • A client head on the server makes transformHtmlTemplate throw Cannot read properties of undefined (reading 'htmlAttrs').
  • The Options API head() option needs app.mixin(VueHeadMixin) in both entries. Without it, Vue ignores head().

Automatic behaviour

  • The server head adds <meta charset="utf-8">, the viewport meta, and <html lang="en">. Override them with useHead or disableDefaults: true. The client head adds none.
  • Tags dedupe on title, name, property, canonical rel, and key. The last entry wins. htmlAttrs and bodyAttrs merge.
  • Matching tags in index.html are replaced, never duplicated. When the replacing entry goes away, they stay gone, except <title>, which reverts to the first document title. For site fallbacks, call useHead(fallback, { tagPriority: 'low' }) in the root component.
  • The client writes the DOM one macrotask after a change, so document.title is still old after await nextTick(). To read the result, use the onRendered entry option.
  • On the server, refs and getters resolve at the first transformHtmlTemplate or head.render() call. The result is then cached.
  • Unhead() from @unhead/vue/vite is optional. In dev it adds ValidatePlugin, which logs [unhead] warnings. In dev and build it rewrites static useSeoMeta input to useHead. Options validate, transformSeoMeta, and treeshake take false.

Common tasks

Reactive and async values

<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { onMounted, ref } from 'vue'

const product = ref<{ name: string, summary: string }>()
useHead({
  title: () => product.value?.name ?? 'Loading',
  meta: [{ name: 'description', content: () => product.value?.summary ?? 'Loading product details.' }],
})

onMounted(() => {
  fetch('/api/product').then(r => r.json()).then((data) => {
    product.value = data
  })
})
</script>
  • Pass the ref or a getter.
  • Call useHead synchronously in setup. After an await in a callback, or in an async setup() outside <script setup>, it throws useHead() was called without provide context. Update a ref instead, or call patch() on an entry made in setup. Top-level await in <script setup> keeps the context.

Titles

<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({ title: 'Pricing', titleTemplate: title => (title ? `${title} | Acme` : 'Acme') })
</script>
  • A string template '%s | Acme' needs no plugin. If no entry sets a title, it renders | Acme.
  • titleTemplate: computed(title => ...) gets the previous computed value and renders undefined | Acme, with no type error.
  • titleTemplate: null on a page removes an inherited template.
  • %separator, %siteName, and other templateParams need TemplateParamsPlugin. Without it they print as text. Register it in the client entry and in each server createHead:
import { createHead } from '@unhead/vue/client'
import { TemplateParamsPlugin } from '@unhead/vue/plugins'

export const head = createHead({ plugins: [TemplateParamsPlugin] })

SEO meta

<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'Pricing',
  description: 'Compare plans.',
  ogTitle: 'Pricing',
  ogImage: [{ url: 'https://example.com/og.png', width: 1200, height: 630, alt: 'Acme' }],
  robots: { noindex: true, nofollow: true },
})
</script>
  • robots: { index: false, follow: false } renders no robots tag. Use noindex and nofollow, or a string.
  • content: null removes an inherited meta tag.
  • useHead does not sanitize. For values from users or APIs, use useHeadSafe. It keeps only JSON scripts. It drops on*, http-equiv, javascript: and data: URLs, and canonical, preload, prefetch, preconnect, and manifest links; set a canonical with useHead.

Third-party scripts

<script setup lang="ts">
import { useScript } from '@unhead/vue'

interface Analytics { track: (event: string) => void }

const analytics = useScript<Analytics>('https://example.com/analytics.js', {
  use: () => (window as unknown as { analytics: Analytics }).analytics,
})
analytics.proxy.track('pageview')
analytics.onLoaded(api => api.track('loaded'))
</script>
  • In a component, the script loads on mount with defer and fetchpriority="low". An absolute URL also gets crossorigin="anonymous" and referrerpolicy="no-referrer". SSR renders nothing for it unless you pass trigger: 'server'.
  • If the script host sends no CORS headers, the browser blocks the load. Pass useScript({ src, crossorigin: false }).
  • trigger: 'manual' waits for load(). A Ref<boolean> or getter trigger loads when it becomes true.
  • Unmount does not remove the script. proxy exists only with use or resolve.

Streaming SSR

On the server, createStreamableHead() from @unhead/vue/stream/server returns { head, wrapStream }; return wrapStream(renderToWebStream(app), template). On the client, use createStreamableHead() from @unhead/vue/stream/client, and add Unhead({ streaming: true }) to the Vite config.

  • Without the stream bootstrap, the client call returns undefined. Throw then; otherwise each useHead throws the provide-context error.
  • A useHead call reaches the shell <head> only if no await ran before it, in its component or any ancestor. Later calls become inline patch scripts, which most bots ignore. Set canonical, robots, description, and Open Graph in the root or layout.
  • wrapStream keeps the <title>, <meta>, and charset tags of the template, so the shell has two of each. Remove them from index.html.
  • On the client, streamed patch tags stay after their component unmounts. Set title and description on every page.

Traps

  • v2 names are not converted: hid, vmid, children, and body: true render as literal attributes, such as <meta hid="description"> or an empty <script children="...">. The table below has the new names.
  • Promise values are not awaited. A promise title is dropped; a promise content renders [object Promise]. PromisesPlugin resolves them on the client, but on the server it drops every entry that holds a promise. Await the data first.
  • tagPriority: 'before:...' and 'after:...' need AliasSortingPlugin, or Unhead ignores them.
  • useServerHead, useServerHeadSafe, and useServerSeoMeta are deprecated aliases that also run in the browser. With Unhead(), only a production client build removes them. For server-only tags, use if (import.meta.env.SSR). The upgrade guide's import.meta.server exists only in Nuxt.
  • Type errors name the wrong property. Type '"description"' is not assignable to type 'ResolvableValue<undefined>' means content is missing or a v2 key is present. A font preload without crossorigin fails on rel or as, not on crossorigin. Wrap a non-standard rel or script type in defineLink() or defineScript().
  • The install guide says @unhead/vue@next, which is 3.0.0-beta.9. Install @unhead/vue.

Version limits

In 3.4.2, a templateParams value of 0 renders empty. A numeric templateParams value used in a JSON script with processTemplateParams: true throws in a hook: the HTML comes back unprocessed, then Node exits on an unhandled rejection that try/catch misses. Pass strings. unhead#1002 fixes both after 3.4.2.

Upgrading from v1, v2, or @vueuse/head

Old 3.x
createHead from @unhead/vue or @vueuse/head createHead from @unhead/vue/client or @unhead/vue/server
createServerHead() createHead() from @unhead/vue/server
Head from @vueuse/head Head from @unhead/vue/components
hid, vmid / children / body: true key / innerHTML or textContent / tagPosition: 'bodyClose'
await renderSSRHead(head) head.render(), synchronous
useHead(input, { mode: 'server' }) the server entry, or import.meta.env.SSR
disableCapoSorting: true tagWeight(tag) on the server createHead; lower sorts first
import unhead from '@unhead/vue/vite' import { Unhead } from '@unhead/vue/vite'
script.then(), script.track() script.onLoaded(), script.proxy.track()

Debug

Without the Vite plugin, add ValidatePlugin() from @unhead/vue/plugins when import.meta.env.DEV is true; it also logs in production. It flags a missing TemplateParamsPlugin only if templateParams is set.

Source: SKILL.md on GitHub

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

No alerts4d3 checks · Risk SAFE
  • Gen Agent Trust Hub4d

    The skill provides safe documentation and examples for using the @unhead/vue library to manage head tags in Vue applications, including security best practices for sanitizing user-provided data.

  • Socket4d

    No alerts

  • Snyk4d

    Risk: LOW · No issues

Signed by skilld at 348d075. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 13 hours ago.

Activeupdated 2 weeks ago

README badge

README badge for unjs/unhead