---
name: nuxt-seo-utils
description: Set up, override, and debug the default SEO head tags in a Nuxt app with the nuxt-seo-utils module. Use when a task mentions canonical URLs, the title template or separator, fallback titles, og:image or twitter:image files, favicons and app icons, seo.meta, seoMeta or head in routeRules, useBreadcrumbItems, useShareLinks, the nuxt-seo-utils icons CLI, seo.redirects, moved or deleted URLs, 301 or 410 responses, or the seo config key. Also use when a page tag does not override a default, html lang ignores useHead, or og:image renders twice.
license: MIT
compatibility: "Requires Node.js ^22.22.3 || ^24.15.0 || >=26.0.0. Requires Nuxt ^4.6.0 || ^5.0.0. Requires Unhead ^3.4.2."
title: nuxt-seo-utils
canonical_url: https://skilld.dev/gh/harlan-zw/nuxt-seo-utils
last_updated: 2026-10-13T19:12:08.000Z
---

> Index of every page on this site: [llms.txt](https://skilld.dev/llms.txt)

> **Skill from skilld.dev.** Follow the user's requested action.
> If the user asked to fork this Skill, follow the fork workflow below. Do not execute the Skill instructions while copying it.
> Otherwise, follow the instructions below for this session. You do not need to install anything.
>
> If the user asked to install this Skill, run `npx skilld install harlan-zw/nuxt-seo-utils/nuxt-seo-utils`. Install writes the Skill files into the project, so every session loads them.
>
> ## Fork workflow
>
> A fork creates an editable local Skill with its original author and licence. The request authorizes copying and local installation.
> 1. Check `./skills/nuxt-seo-utils`, the project lockfile, and selected Agent targets together. If the local directory or installed Skill exists, stop. Never overwrite an existing directory or Agent target.
> 2. Read [source metadata](https://skilld.dev/api/v1/skills/harlan-zw/nuxt-seo-utils/nuxt-seo-utils) once. Use sourceUrl, sourceCommit, skillPath, sourceGone, and license. If the source is gone or its path is missing, stop. If license is null, read licence files at the source commit.
> 3. Fetch only the source commit into a temporary Git repository. Do not clone full history. Derive repository_url from sourceUrl, including repository renames. If sourceCommit is absent, resolve the sourceUrl ref once. Set source_commit to that actual commit. Run these commands in one shell call:
>
> ```sh
> git init --quiet "$temporary_dir"
> git -C "$temporary_dir" fetch --quiet --depth=1 "$repository_url" "$source_commit"
> git -C "$temporary_dir" checkout --quiet --detach FETCH_HEAD
> ```
>
> Read applicable licence declarations and notices at that commit. If copying is not permitted, report the restriction and stop.
> 4. Inspect source entries together, then copy the directory containing skillPath into `./skills/nuxt-seo-utils`. Use the user's path if selected. Keep the original SKILL.md, relative links, scripts, binary assets, and executable modes. Exclude .git metadata. Reject symlinks and paths outside the Skill directory. After checking entries, use cp -a where available. A regular source directory needs no custom copy script. Do not save this page wrapper as SKILL.md.
> Preserve author credit, notices, and applicable licence files from repository or parent directories. Add PROVENANCE.md with the Skill page, source URL, actual commit, original path, and licence. Retain any existing PROVENANCE.md and record new provenance separately. Batch source inspection, copying, and provenance work where practical.
> 5. In the project root, run `skilld install ./skills/nuxt-seo-utils --mode copy --plain`. If skilld is unavailable, use `npx skilld install ./skills/nuxt-seo-utils --mode copy --plain`. This known command needs no help lookup. Install does not support --json. Use detected Agent targets, or add --agent for the targets the user selected. Install the local path, never the upstream selector. If installation fails, preserve the local copy and report the exact failure.
> 6. Confirm the local lockfile source and installed Agent copies once. Report the local path, actual commit, and Agent targets. After edits, reinstall the same local path. Upstream updates must not replace it. Do not publish or push unless the user asks.

# nuxt-seo-utils

Requires Nuxt `^4.6.0 || ^5.0.0` and Unhead 3.4.2 or newer.
The module adds default head tags from site config, reads icon and social image files, and ships breadcrumb and share link composables.
Docs: https://nuxtseo.com/docs/seo-utils

## Setup

Install with `npx nuxt module add nuxt-seo-utils`.
Included in `@nuxtjs/seo` v6. If the app uses `@nuxtjs/seo`, configure the `seo` key and do not add the module again.
The config key is `seo`. `seoUtils` does nothing.
Set `site.url` and `site.name`. The canonical URL, `og:url`, the title template, and absolute image URLs read them.

```ts
export default defineNuxtConfig({
  modules: ['nuxt-seo-utils'],
  site: { url: 'https://example.com', name: 'Acme', description: 'Default description', twitter: 'acme', defaultLocale: 'en-US' },
})
```

Crawlers read only the SSR response. The defaults render on the server.

## Automatic behaviour

With the defaults, every page gets:

- `<title>` from the template `%s %separator %siteName`. The separator is `|`. Change it with `site.separator`.
- A fallback title from the last path segment, title cased: `/blog/my-post` becomes `My Post`. `definePageMeta({ title })` sets it with a static string.
- `<link rel="canonical">` from `site.url` and the route path, lowercased, with only these query keys kept: `page`, `sort`, `filter`, `search`, `q`, `category`, `tag`.
- `og:url` (the resolved canonical, including one a page sets), `og:type` (`website`), `og:site_name` (`site.name`), and `description` from `site.description`.
- `og:title`, `og:description`, and `twitter:card` (`summary_large_image`), inferred from the title and description.
- `twitter:site` and `twitter:creator` from `site.twitter`. The `@` is added.
- `<html lang>` from `site.currentLocale` or `site.defaultLocale`, else `en`.
- `og:locale` only when the locale has a region. `en-US` renders `en_US`. `en` renders no tag.
- Relative `og:image` and `twitter:image` values become absolute against `site.url`.

Module tags use `tagPriority: 'low'`, so a page `useSeoMeta()` or `useHead()` call replaces them.
A page `ogImage` also drops the default image's width, height, type, and copied `twitter:image`. A page `ogImageAlt` alone keeps the default image.
A page canonical needs no extra priority:

```ts
useHead({ link: [{ rel: 'canonical', href: 'https://example.com/preferred' }] })
```

Set `seo: { automaticDefaults: false }` to drop the canonical, title template, `lang`, and Open Graph defaults together. No option drops only one of them.

## Metadata files

Files in `public/` become site-wide head tags at build time:

- `favicon.{ico,png,svg}`, `icon*.{ico,png,jpg,svg}`, `apple-icon*.png`, `apple-touch-icon*.png`: `<link rel="icon">` or `apple-touch-icon`, with `type` and `sizes` read from the file.
- `og-image.{png,jpg,jpeg,gif}` or `opengraph-image.*`: `og:image` with width, height, and type. It also fills `twitter:image` when no `twitter-image.*` file exists. The name must match exactly; `blog-og-image.png` is ignored.
- `<name>.alt.txt` next to an image sets `og:image:alt`.
- `-dark` or `-light` in an icon name adds a `prefers-color-scheme` media query.

The same file names inside `pages/<route>/` or `pages/<route>/_dir/` apply to that route and its children. They replace a `public/` image there.
An icon link in `app.head.link` with the same `rel` wins over the files.
Set `seo: { metaDataFiles: false }` to turn this off.

Generate icon files from one source image in `public/`:

```bash
pnpm exec nuxt-seo-utils icons --source logo.svg
```

It writes `favicon.ico`, `apple-touch-icon.png`, and `icon-{16,32,192,512}` PNGs to `public/`. Install the optional peer with `pnpm add -D sharp` before generating icons.

## Site-wide and per-route meta

`seo.meta` takes `useSeoMeta()` input and renders on every page:

```ts
export default defineNuxtConfig({
  seo: { meta: { author: 'Acme Team', themeColor: '#18181b' } },
})
```

`routeRules` accept `seoMeta` and `head`:

```ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { seoMeta: { author: 'Blog Team' }, head: { meta: [{ name: 'x-section', content: 'blog' }] } },
  },
})
```

Route rule tags override `seo.meta` and `app.head`. A page `useSeoMeta()` or `useHead()` call overrides them. The `tagPriority` option does not change this.
Route rule tags render on the server only. They do not update on client navigation.

## Breadcrumbs

`useBreadcrumbItems()` is auto-imported. It returns a ref of items from the route path, ready for Nuxt UI `<UBreadcrumb :items>`.

```ts
const items = useBreadcrumbItems({
  overrides: [undefined, undefined, { label: post.value.title }], // index matches the path segment; false removes it
})
```

- With `nuxt-schema-org` installed, it also adds a `BreadcrumbList` node. Without it, no JSON-LD renders and there is no warning.
- Labels come from `definePageMeta({ breadcrumb: { label } })`, then `title` in page meta, then the title-cased segment. The index route is `Home`.
- Call it in the page component, not the layout. The page has the data for the last items. Calls in parent components with the same `id` merge their options.

## Share links

```ts
const share = useShareLinks({ title: post.value.title, utm: { source: 'auto', campaign: 'launch' } })
// share.value.twitter, .facebook, .linkedin, .whatsapp, .telegram, .reddit, .pinterest, .email, .canonicalUrl
```

`title` defaults to `site.name`, not the page title. Pass it.
UTM defaults to `utm_source=<platform>` and `utm_medium=social` (or `email`). An object without `source: 'auto'` switches to manual mode: `{ campaign: 'launch' }` sends only `utm_campaign`.

## Redirects and gone URLs

`seo.redirects` lists old URLs, keyed by path without `app.baseURL`. Every entry needs `addedAt`.

```ts
export default defineNuxtConfig({
  seo: {
    redirects: {
      '/blog/old-post': { to: '/blog/new-post', addedAt: '2026-10-13' }, // 301; set statusCode: 308 to keep the method
      '/docs/v1/**': { to: '/docs/**', addedAt: '2026-10-13' }, // /docs/v1/setup -> /docs/setup
      '/spring-sale': { statusCode: 410, addedAt: '2026-10-13', reason: 'Sale ended' }, // error page with 410
    },
  },
})
```

- A moved URL has `to` (a path or an absolute URL) and `statusCode` 301 (default) or 308. A gone URL has no `to` and `statusCode` 410 or 404.
- The request query string is kept. A trailing slash does not change the match. An exact key beats a `/**` key.
- `reason` is a private note. Visitors never see it.
- Nothing expires. After `redirectReviewAfterDays` (365) from `addedAt` or `reviewedAt`, `nuxt dev` and `nuxt build` list the entry for review. To keep it, set `reviewedAt` to today. Delete it only when the old URL gets no visits and no backlinks.
- Chains flatten at build: `/a` to `/b` and `/b` to `/c` serve `/a` to `/c`. A loop or an invalid entry stops the build. A redirect to a gone URL warns.
- With an i18n prefix strategy, each unprefixed entry also applies under each locale prefix. A translated path needs its own entry.
- For a long list, import a file typed with `satisfies Redirects` (`import type { Redirects } from 'nuxt-seo-utils'`).
- Do not redirect deleted pages to the home page. Google can treat that as a soft 404. Use a gone URL.
- Use `routeRules` `redirect` for temporary 302 or 307 redirects. `seo.redirects` accepts only permanent ones.

## Traps

- **`htmlAttrs.lang` in `useHead()` or `app.head` is ignored.** The module sets `lang` from site config. Set `site.defaultLocale`, or use `@nuxtjs/i18n`.
- **Before nuxt-schema-org 6.4.1, the Schema.org WebPage URL keeps the route case.** `canonicalLowercase` lowercases the canonical, `og:url`, `useShareLinks()` URLs, and `BreadcrumbList` item URLs. nuxt-schema-org before 6.4.1 builds the WebPage `url` from the route path. There, `/blog/My-Post` gets a canonical of `/blog/my-post` and a WebPage URL of `/blog/My-Post`. From 6.4.1, the WebPage `url` follows the canonical. On an older version, upgrade nuxt-schema-org, use lowercase route paths, or set `canonicalLowercase: false`. Breadcrumb `to` links keep their case on purpose.
- **`definePageMeta({ title })` takes a static string only.** Use `useSeoMeta({ title })` for data.
- **Only some static presets get native redirects.** `nuxt generate` writes host rules for `netlify-static` (`_redirects`, 308 becomes 301), `vercel-static` (`config.json` routes), and `cloudflare-pages-static` (`_redirects`, no 410: gone URLs get the host 404 page). With `netlify-static` and an `app.baseURL`, and with every other preset, each exact moved URL becomes an instant meta refresh page, and gone URLs and `/**` keys need host rules. The build lists each entry the host cannot serve.
- **A catch-all page skips client-side redirects.** `seo.redirects` runs on the server. A `NuxtLink` to an old URL that a `[...slug].vue` page matches renders that page. Fix the link. Nuxt Link Checker flags a link to a 301 entry.

## Version limits

v9 breaking changes and their fixes:

- Nuxt `^4.6.0 || ^5.0.0`, Node `^22.22.3 || ^24.15.0 || >=26.0.0`, and Unhead `^3.4.2` are required. Upgrade Nuxt and Node first.
- Unhead v2 is not supported. Remove any `@unhead/vue` 2 pin or override.
- `nuxt-site-config` 5 is required. Change a pinned `nuxt-site-config` to `^5.0.0`.
- The module no longer installs Sharp. Run `pnpm add -D sharp` before `nuxt-seo-utils icons`.
- Runtime and prerender minification keep inline JavaScript unchanged. Put scripts that need minification in `app.head`.
- Only `#seo-utils/app` resolves. Replace `#seo-utils/types`, `#seo-utils/server/`, and `#seo-utils/shared/` imports with `#seo-utils/app` exports.
- The DevTools panel needs `nuxtseo-devtools-host` as a dev dependency.

Older limits:

- 8.5.2 and earlier: a page `ogImage` keeps the default image's dimensions and `twitter:image`, a page canonical leaves `og:url` on the route URL, a `pages/<route>/og-image.png` renders `og:image:alt` twice, and `public/opengraph-image.*` is ignored.
- 8.5.2 and earlier: `BreadcrumbList` item URLs keep the route case, and `useShareLinks()` lowercases without the site locale.
- 8.5.2 and earlier: `seo.meta` and `public/og-image.png` beat route rule `seoMeta` and `pages/**/og-image.png`. Put the route value in the page with `useSeoMeta()`.
- `automaticTwitterTags: false` (removes `twitter:card`) arrived in 8.5.0.

## Config

- `redirectToCanonicalSiteUrl` (`false`): in production, send a 301 from other hosts to the `site.url` host.
- `canonicalQueryWhitelist`: replaces the default list. It does not extend it.
- `redirects`: moved and gone URLs. See the section above.
- `redirectReviewAfterDays` (`365`): days until an entry is due for review. `false` turns the review off.
- `minify` (`{ build: true, runtime: false }`): minify static `app.head` JavaScript/CSS with parser-based tooling. Compact JSON/CSS in prerendered HTML. `runtime: true` compacts JSON/CSS per SSR request. Dynamic JavaScript and custom script types remain unchanged.
- `automaticOgAndTwitterTags` (`true`): the `og:title`, `og:description`, and `twitter:card` inference.
- Other options: https://nuxtseo.com/docs/seo-utils/api/config

## Debug

- On `nuxt dev` and `nuxt build`, the module warns about conflicts in `app.head`, such as a static `lang` or an `og:site_name` that differs from `site.name`. Fix the site config, not the head. Set `validateAppHead: false` to hide the check.
- `/__nuxt-seo-utils/debug.json` in dev, or in production with `seo: { debug: true }`.
- On `nuxt dev` and `nuxt build`, the module warns when SSR is off for indexable pages. Turn SSR on for public routes. For private routes, set `robots: false` with `@nuxtjs/robots`.
- Nuxt DevTools has an SEO Utils tab. The panel needs `nuxtseo-devtools-host` as a dev dependency.
