---
title: "CLI"
description: "Audit and migrate unhead usage with the unhead CLI: source lint, prerendered HTML validation, and live URL inspection."
canonical_url: "https://unhead.unjs.io/docs/typescript/head/guides/tooling/cli"
last_updated: "2026-05-14T21:49:17.545Z"
---

`@unhead/cli` ships an `unhead` binary that wraps both source-level lint and runtime-rule validation behind a small set of subcommands. Use it locally for one-shot audits, or wire it into CI to catch regressions before they ship.

## Install

```bash
pnpm add -D @unhead/cli
```

The binary is registered as `unhead` (run `unhead --help` to discover commands).

## Commands

### `unhead audit [globs...]`

Lints your source for unhead misuse. Vue and Svelte SFCs and `nuxt.config.ts`'s `app.head` block are all audited as first-class inputs; the parser is `oxc-parser`, no ESLint install required.

```bash
unhead audit                   # default: all .{js,ts,vue,svelte,...}
unhead audit src/**/*.ts       # narrowed glob
```

Exits with code 1 when any rule fires at `error` severity. Warnings and info findings don't fail CI by themselves so you can adopt the rules incrementally.

The output also includes a green `✓` line for every file with `useHead` / `useSeoMeta` usage and zero diagnostics (so you can confirm what was scanned), plus a `parse-error` warning for any script block oxc couldn't read — typically a real TS/JS syntax error in your source.

#### Project insights

Beyond per-file lints, `audit` runs cross-file checks:

- **page-missing-head** (`info`) — flags any file under `**/pages/**/*.vue` that doesn't call `useHead` / `useSeoMeta` directly *or* via a project composable that transitively does. The transitive part comes from a fixpoint over the project's call graph, so wrappers like `useDefaultMeta()` → `useHead()` count as coverage.
- **prefer-use-seo-meta** (`warning`, autofixable) — fires on `useHead` calls that only set `title` / `description` / `meta:[…]`. The flat `useSeoMeta` shape is fully typed against `MetaFlat`, so a typo like `name: 'descriptipon'` becomes a TypeScript error at write time instead of silently shipping a broken meta tag. `migrate` rewrites the call in place.
- **Title consistency** — collects every static `title:` / `titleTemplate:` literal across `useHead`, `useSeoMeta`, `defineNuxtConfig`, and project composables identified as head-providing (e.g. `useToolSeo({ title: '…' })`). Surfaces mixed separators, common trailing suffixes that should live in `titleTemplate` + `templateParams.siteName`, and titles that duplicate a suffix already set by `titleTemplate`. Each finding pairs the observation with a concrete migration hint.

### `unhead migrate [globs...]`

Applies every autofixable rule. Run once on a v2 codebase and you've migrated the deprecated props, wrapped tag literals in `defineLink` / `defineScript` for type narrowing, converted meta-only `useHead` calls to `useSeoMeta` for typed key autocompletion, and patched the obvious SEO/perf foot-guns.

```bash
unhead migrate
unhead migrate --dry-run       # report fixable count without writing
```

### `unhead validate-html [globs...]`

Runs the runtime [`ValidatePlugin`](/docs/typescript/head/guides/tooling/validate-plugin) over already-rendered HTML files. Catches the cross-tag and rendered-output rules that source lint can't see: canonical vs `og:url` mismatch, missing `og:image` dimensions, charset position, meta tags beyond the 1MB crawler limit, etc.

```bash
unhead validate-html '.output/public/**/*.html'
unhead validate-html dist/index.html --json
```

Exits with code 1 on any rule with `warn` severity (the runtime plugin only emits `warn` / `info`, not `error`).

### `unhead validate-url <url>`

Fetches a live URL and runs the same validation against the response. Useful for spot-checking production or staging.

```bash
unhead validate-url https://example.com
unhead validate-url https://example.com --json
unhead validate-url https://example.com --user-agent 'Twitterbot/1.0'
```

The default user agent is `facebookexternalhit/1.1` so social-crawler-aware rules (e.g. `meta-beyond-1mb`) engage on the response.

## What runs where

unhead validation lives in three layers; each catches a different class of issue. The CLI is one entry point to all three.

<table>
<thead>
  <tr>
    <th>
      Rule class
    </th>
    
    <th>
      <code>
        audit
      </code>
      
       / <code>
        migrate
      </code>
      
       (source)
    </th>
    
    <th>
      <code>
        validate-html
      </code>
      
       / <code>
        validate-url
      </code>
      
       (rendered HTML)
    </th>
    
    <th>
      Runtime <code>
        ValidatePlugin
      </code>
      
       (live app)
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Typos in meta <code>
        name
      </code>
      
       / <code>
        property
      </code>
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Deprecated v2 props
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Numeric <code>
        tagPriority
      </code>
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Preload missing <code>
        as
      </code>
      
       / font <code>
        crossorigin
      </code>
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Twitter handle missing <code>
        @
      </code>
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Robots <code>
        index/noindex
      </code>
      
       conflict
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Non-absolute canonical / OG URLs
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Empty meta content / HTML in title
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Canonical vs <code>
        og:url
      </code>
      
       mismatch
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        og:image
      </code>
      
       missing dimensions
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Missing description / title
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Missing OG title / description
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Charset not within first N tags (SSR)
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Meta tag past 1MB crawler limit
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Too many preloads / preconnects / <code>
        fetchpriority="high"
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      Inline script / style size budget
    </td>
    
    <td>
      —
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        prefer-use-seo-meta
      </code>
      
       (meta-only <code>
        useHead
      </code>
      
       → <code>
        useSeoMeta
      </code>
      
      )
    </td>
    
    <td>
      ✓ (autofix)
    </td>
    
    <td>
      —
    </td>
    
    <td>
      —
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        page-missing-head
      </code>
      
       (no <code>
        useHead
      </code>
      
      /composable in <code>
        pages/**
      </code>
      
      )
    </td>
    
    <td>
      ✓ (info, cross-file fixpoint)
    </td>
    
    <td>
      —
    </td>
    
    <td>
      —
    </td>
  </tr>
  
  <tr>
    <td>
      Title separator / suffix consistency across pages
    </td>
    
    <td>
      ✓ (cross-file)
    </td>
    
    <td>
      —
    </td>
    
    <td>
      —
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        parse-error
      </code>
      
       (script block oxc couldn't parse)
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      —
    </td>
    
    <td>
      —
    </td>
  </tr>
</tbody>
</table>

Source lint is the cheapest feedback loop and runs in your editor. The HTML pass catches everything that depends on the resolved tag list. The runtime plugin gives you live warnings during dev. Use them together.

## Devtools integration

When your dev server uses the unhead Vite plugin, the audit command also runs from inside the devtools panel — a Run audit / Apply migrate button on the new `/audit` tab calls the CLI programmatically and renders results inline (with click-to-source on every message). No extra setup; the panel just lazy-imports `@unhead/cli` if it's installed.
